Compare commits
90 Commits
4454b8dd3a
...
production
| Author | SHA1 | Date | |
|---|---|---|---|
| 3eff5b6dfa | |||
| aa97d25302 | |||
| 24ae4ec15c | |||
| c3ee46ff62 | |||
| e9f8328404 | |||
| 6a9d142dc6 | |||
| 1d78633e17 | |||
| cbc61ad0f5 | |||
| 68d0aed468 | |||
| a9aa03d6a8 | |||
| e63b80fe47 | |||
| 4115f64fff | |||
| 7d892c2f6a | |||
| d757c8220c | |||
| b53d9facc9 | |||
| 5a004e5c88 | |||
| 5d83eb4e05 | |||
| 6126ecf023 | |||
| 31f0fc4860 | |||
| 0a2c5f913b | |||
| 2f040cab02 | |||
| 933cdb1094 | |||
| 4596296d97 | |||
| f2316eafe4 | |||
| 9c6149bbb9 | |||
| 8afc684098 | |||
| c17ad68c30 | |||
| 9378b7474c | |||
| 68d7647cb1 | |||
| c4712a00ac | |||
| 3b4562665d | |||
| 52c8965704 | |||
| 7fb2062721 | |||
| 797bfef964 | |||
| 6d8d681046 | |||
| b7d1fe13c7 | |||
| 67f57ce99f | |||
| c4cbe94ed7 | |||
| ed24f505d3 | |||
| 3c2a0edca8 | |||
| aed6258fb3 | |||
| e6d1d833ec | |||
| a8c52db544 | |||
| c0c7bb3ac5 | |||
| d0fe99492d | |||
| d38f50584c | |||
| 1c67d7798a | |||
| 2a7bb0592d | |||
| 9699833ac9 | |||
| b3e6066c90 | |||
| 148b8e4011 | |||
| 1ef5edf939 | |||
| 76dbee7d41 | |||
| 148a9e0493 | |||
| 851a1bbc4f | |||
| aa1d21a616 | |||
| 7147e9930f | |||
| 85208da1b5 | |||
| ea7414dc7f | |||
| a450d9a001 | |||
| 92c992249e | |||
| 6939780cd1 | |||
| e6cf90e3fa | |||
| e0203541bf | |||
| 8be98ae33e | |||
| c96377a674 | |||
| 89f5241680 | |||
| f1b0085181 | |||
| 60c91438ec | |||
| 172101d0ab | |||
| 1d8f34ae6c | |||
| 76b7211a5c | |||
| fe72774d08 | |||
| bdea2e8aef | |||
| 0386a2b1aa | |||
| 8f29fbaf71 | |||
| 7a62e3caac | |||
| 4eaca9bc4c | |||
| e541aec115 | |||
| c9affe62bd | |||
| 6147499f9c | |||
| 3409133757 | |||
| af51c02d3d | |||
| b9171d548b | |||
| 743403e4d4 | |||
| 44fd5f29d9 | |||
| e7bed65149 | |||
| 6fc00e134f | |||
| fac3d2c85e | |||
| 51c20116a7 |
@@ -0,0 +1,2 @@
|
||||
# Keep production config from current branch during merges — never overwrite
|
||||
src/BackOffice/wwwroot/appsettings.Production.json merge=ours
|
||||
@@ -1,46 +0,0 @@
|
||||
|
||||
name: Push nuget and docker image Actions Workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- stage-new
|
||||
jobs:
|
||||
Deploy:
|
||||
runs-on: windows
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://git.afrino.co/actions/checkout@v3
|
||||
- name: Setup dotnet
|
||||
uses: https://git.afrino.co/actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: 7.0.x
|
||||
|
||||
- name: Remove Package Source
|
||||
run: dotnet nuget remove source FourSat
|
||||
continue-on-error: true
|
||||
- name: Add Package Source
|
||||
run: dotnet nuget add source --name FourSat --username systemuser --password sZSA7PTiv3pUSQZ https://git.afrino.co/api/packages/FourSat/nuget/index.json --store-password-in-clear-text
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore ".\src\BackOffice\BackOffice.csproj"
|
||||
- name: Build
|
||||
run: dotnet build ".\src\BackOffice\BackOffice.csproj" --configuration Release --no-restore
|
||||
- name: Test
|
||||
run: dotnet test ".\src\BackOffice\BackOffice.csproj" --no-restore --verbosity normal
|
||||
- name: Recycle Apppool
|
||||
run: |
|
||||
& "C:\Windows\System32\inetsrv\appcmd.exe" recycle apppool /apppool.name:admin.kbs1.ir
|
||||
shell: powershell
|
||||
- name: Stop Website
|
||||
run: |
|
||||
& "C:\Windows\System32\inetsrv\appcmd.exe" stop site /site.name:admin.kbs1.ir
|
||||
shell: powershell
|
||||
- name: Publish
|
||||
run: dotnet publish ".\src\BackOffice\BackOffice.csproj" -c Release -o publish
|
||||
- name: Copy Publish To IIS Directory
|
||||
run: Get-ChildItem -Path "publish\*" | Copy-Item -Destination "E:\kbs1.ir\admin.kbs1.ir\" -Recurse -Force
|
||||
- name: Start Website
|
||||
run: |
|
||||
& "C:\Windows\System32\inetsrv\appcmd.exe" start site /site.name:admin.kbs1.ir
|
||||
shell: powershell
|
||||
@@ -67,8 +67,15 @@ 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 rollout restart deployment/backoffice
|
||||
kubectl rollout status deployment/backoffice --timeout=180s
|
||||
kubectl apply -f /tmp/backoffice-deployment.yaml &&
|
||||
kubectl rollout restart deployment/backoffice &&
|
||||
kubectl rollout status deployment/backoffice --timeout=180s &&
|
||||
rm -f /tmp/backoffice-deployment.yaml
|
||||
"
|
||||
echo "✅ Deployed!"
|
||||
|
||||
@@ -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:32082"]
|
||||
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
|
||||
}
|
||||
DAEMON
|
||||
echo "🚀 Starting Docker daemon..."
|
||||
dockerd &
|
||||
dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs &
|
||||
|
||||
for i in $(seq 1 90); do
|
||||
if docker info >/dev/null 2>&1; then
|
||||
@@ -48,21 +48,29 @@ 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 build -f BackOffice/Dockerfile \
|
||||
DOCKER_BUILDKIT=0 docker build --network host -f BackOffice/Dockerfile \
|
||||
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
|
||||
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod \
|
||||
.
|
||||
|
||||
- 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: |
|
||||
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"
|
||||
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!"
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
# سرویسهای BackOffice (پنل ادمین)
|
||||
|
||||
این سند تمام سرویسهایی را که **BackOffice** (فرانت ادمین) به **CMS** از طریق gRPC Gateway (`GwUrl`) فراخوانی میکند، به ترتیب **Use Case** فهرست میکند.
|
||||
|
||||
- **مسیر wrapper سرویسها:** `BackOffice/src/BackOffice/Services/`
|
||||
- **ثبت gRPC:** `BackOffice/src/BackOffice/Common/Configure/ConfigureService.cs`
|
||||
- **تاریخ:** ۱۴۰۵/۰۳/۱۷ (بروز: خرداد ۱۴۰۵ — گزارشهای read-only + proto 0.0.201؛ **بروز ۱۴۰۵/۰۳/۱۸ — دفتر تراکنشهای مالی proto 0.0.203**)
|
||||
|
||||
---
|
||||
|
||||
## ۱. ورود و احراز هویت ادمین
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 1.1 | `OtpTokenContract.CreateNewOtpToken` | `OtpTokenContract` | `/Login/` (`LoginPage`) | ارسال OTP ورود ادمین |
|
||||
| 1.2 | `UserContract.VerifyOtpToken` | `UserContract` | `/VerifyCodePage/{Mobile}` | تأیید OTP و دریافت JWT |
|
||||
| 1.3 | `OtpTokenContract.CreateNewOtpToken` (resend) | `OtpTokenContract` | `/VerifyCodePage/{Mobile}` | ارسال مجدد OTP |
|
||||
| 1.4 | `AuthorizationService.HasPermissionAsync` | — (JWT Role/Claim) | `NavMenu`, صفحات محافظتشده | کنترل نمایش منو بر اساس نقش (فعلاً role-based، نه API) |
|
||||
| 1.5 | `AppTokenProvider` / `TokenHandler` | — (localStorage) | سراسری | attach کردن Bearer token به همه gRPC callها |
|
||||
|
||||
---
|
||||
|
||||
## ۲. داشبورد
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 2.1 | `UserContract.GetAllUserByFilter` | `UserContract` | `/` (Index) | شمارش کاربران |
|
||||
| 2.2 | `ProductsContract.GetAllProductsByFilter` | `ProductsContract` | `/` | شمارش محصولات |
|
||||
| 2.3 | `PackageContract.GetAllPackageByFilter` | `PackageContract` | `/` | شمارش پکیجها |
|
||||
| 2.4 | `CategoryContract.GetAllCategoryByFilter` | `CategoryContract` | `/` | شمارش دستهبندیها |
|
||||
| 2.5 | `CommissionContract.GetAvailableWeeks` | `CommissionContract` | `/` | هفته جاری کمیسیون |
|
||||
| 2.6 | `CommissionContract.GetWeeklyCommissionPool` | `CommissionContract` | `/` | وضعیت pool هفته جاری |
|
||||
| 2.7 | `ClubMembershipContract.GetAllClubMemberships` | `ClubMembershipContract` | `/` | آمار اعضای باشگاه |
|
||||
| 2.8 | `UserOrderContract.GetAllUserOrderByFilter` | `UserOrderContract` | `/` | سفارشهای اخیر |
|
||||
| 2.9 | `IDiscountOrderService.GetOrdersAsync` | `DiscountOrderContract` | `Dashboard/DiscountShopWidget` | ویجت سفارشهای فروشگاه تخفیفی |
|
||||
| 2.10 | `CommissionContract.GetWithdrawalRequests` | `CommissionContract` | `NavMenu` | badge درخواستهای برداشت معلق |
|
||||
| 2.11 | `GlobalSearch` — `UserContract.GetAllUserByFilter` | `UserContract` | `MainLayout/GlobalSearch` | جستجوی سریع کاربر + ناوبری صفحات |
|
||||
|
||||
---
|
||||
|
||||
## ۳. مدیریت کاربران
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 3.1 | `UserContract.GetAllUserByFilter` | `UserContract` | `/UserPage/`, `UserAutoComplete`, GlobalSearch | لیست/جستجوی کاربران |
|
||||
| 3.2 | `UserContract.DeleteUser` | `UserContract` | `/UserPage/` | حذف کاربر |
|
||||
| 3.3 | Export users | `UserContract.GetAllUserByFilter` | `/UserPage/` | خروجی CSV کاربران |
|
||||
|
||||
---
|
||||
|
||||
## ۴. نقشها و دسترسیها
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 4.1 | `RoleContract.GetAllRoleByFilter` | `RoleContract` | `/RolePage/`, `RoleAutoComplete` | لیست نقشها |
|
||||
| 4.2 | `RoleContract.CreateNewRole` | `RoleContract` | `Role/Components/CreateDialog` | ایجاد نقش |
|
||||
| 4.3 | `RoleContract.UpdateRole` | `RoleContract` | `Role/Components/UpdateDialog` | ویرایش نقش |
|
||||
| 4.4 | `RoleContract.DeleteRole` | `RoleContract` | `/RolePage/` | حذف نقش |
|
||||
| 4.5 | `RolePermissionsDialog` | — (UI فقط) | `/RolePage/` | ویرایش permissionها — **هنوز به API وصل نشده** |
|
||||
|
||||
---
|
||||
|
||||
## ۵. نقش کاربر و آدرس
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 5.1 | `UserRoleContract.GetAllUserRoleByFilter` | `UserRoleContract` | `/UserRolePage/{UserId}` | نقشهای یک کاربر |
|
||||
| 5.2 | `UserRoleContract.CreateNewUserRole` | `UserRoleContract` | CreateDialog, `UserRoleDialog` | تخصیص نقش |
|
||||
| 5.3 | `UserRoleContract.UpdateUserRole` | `UserRoleContract` | UpdateDialog | ویرایش نقش کاربر |
|
||||
| 5.4 | `UserRoleContract.DeleteUserRole` | `UserRoleContract` | `/UserRolePage/{UserId}` | حذف نقش کاربر |
|
||||
| 5.5 | `RoleContract.GetAllRoleByFilter` | `RoleContract` | `UserRoleDialog`, `RoleTitleColumn` | نمایش عنوان نقش |
|
||||
| 5.6 | `UserAddressContract.GetAllUserAddressByFilter` | `UserAddressContract` | `/UserAddressPage/{UserId}` | آدرسهای کاربر |
|
||||
| 5.7 | `UserAddressContract.CreateNewUserAddress` | `UserAddressContract` | CreateDialog | ثبت آدرس |
|
||||
| 5.8 | `UserAddressContract.UpdateUserAddress` | `UserAddressContract` | UpdateDialog | ویرایش آدرس |
|
||||
| 5.9 | `UserAddressContract.DeleteUserAddress` | `UserAddressContract` | `/UserAddressPage/{UserId}` | حذف آدرس |
|
||||
|
||||
---
|
||||
|
||||
## ۶. پکیجها
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 6.1 | `PackageContract.GetAllPackageByFilter` | `PackageContract` | `/PackagePage/`, `PackageSelect`, ManualPaymentDialog | لیست پکیجها |
|
||||
| 6.2 | `PackageContract.CreateNewPackage` | `PackageContract` | `Package/Components/CreateDialog` | ایجاد پکیج |
|
||||
| 6.3 | `PackageContract.UpdatePackage` | `PackageContract` | `Package/Components/UpdateDialog` | ویرایش پکیج |
|
||||
| 6.4 | `PackageContract.DeletePackage` | `PackageContract` | `/PackagePage/` | حذف پکیج |
|
||||
| 6.5 | `ConfigurationContract.GetClubFeatures` | `ConfigurationContract` | Create/Update Dialog | انتخاب featureهای باشگاه مرتبط با پکیج |
|
||||
|
||||
---
|
||||
|
||||
## ۷. فروشگاه اصلی — محصولات و دستهبندی
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 7.1 | `ProductsContract.GetAllProductsByFilter` | `ProductsContract` | `/ProductsPage/`, BulkEdit, AutoComplete | لیست محصولات |
|
||||
| 7.2 | `ProductsContract.CreateNewProducts` | `ProductsContract` | `Products/Components/CreateDialog` | ایجاد محصول |
|
||||
| 7.3 | `ProductsContract.UpdateProducts` | `ProductsContract` | UpdateDialog | ویرایش محصول |
|
||||
| 7.4 | `ProductsContract.DeleteProducts` | `ProductsContract` | `/ProductsPage/` | حذف محصول |
|
||||
| 7.5 | `ProductsContract.ToggleProductStatus` | `ProductsContract` | `/ProductsPage/`, BulkEdit | فعال/غیرفعال |
|
||||
| 7.6 | `ProductsContract.BulkUpdateProductPrices` | `ProductsContract` | `/ProductsBulkEditPage/` | ویرایش گروهی قیمت |
|
||||
| 7.7 | `ProductsContract.GetProductGallery` / Add/Remove Image | `ProductsContract` | `GalleryDialog` | گالری تصاویر محصول |
|
||||
| 7.8 | `ProductsContract.GetProducts` / `UpdateProductCategories` | `ProductsContract` | `/ProductCategoriesPage/{ProductId}` | drag-drop دستهبندی محصول |
|
||||
| 7.9 | `CategoryContract.GetAllCategoryByFilter` | `CategoryContract` | `/CategoryPage/`, AutoComplete | لیست دستهها |
|
||||
| 7.10 | `CategoryContract.GetCategory` / Create / Update / Delete | `CategoryContract` | CategoryMainPage, Dialogs | CRUD دستهبندی |
|
||||
| 7.11 | `ProductsContract.GetProductsForCategory` / `UpdateCategoryProducts` | `ProductsContract` | `/CategoryProductsPage/{CategoryId}` | drag-drop محصولات یک دسته |
|
||||
|
||||
---
|
||||
|
||||
## ۸. انبارداری (Inventory)
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 8.1 | `InventoryContract.GetAllWarehouses` | `InventoryContract` | `/InventoryPage/`, Warehouses, LowStock | لیست انبارها |
|
||||
| 8.2 | `InventoryContract.GetAllInventoryItems` | `InventoryContract` | `/InventoryPage/` | موجودی کالا |
|
||||
| 8.3 | `InventoryContract.GetLowStockItems` | `InventoryContract` | `/InventoryPage/`, `/InventoryLowStockPage/` | اقلام کمموجود |
|
||||
| 8.4 | `InventoryContract.AddStock` | `InventoryContract` | `AddStockDialog` | ورود کالا به انبار |
|
||||
| 8.5 | `InventoryContract.AdjustStock` | `InventoryContract` | `AdjustStockDialog` | تعدیل موجودی |
|
||||
| 8.6 | `InventoryContract.RecordLoss` | `InventoryContract` | `RecordLossDialog` | ثبت ضایعات |
|
||||
| 8.7 | `InventoryContract.CreateWarehouse` | `InventoryContract` | `CreateWarehouseDialog` | ایجاد انبار |
|
||||
| 8.8 | `InventoryContract.UpdateInventorySettings` | `InventoryContract` | `InventorySettingsDialog` | تنظیمات انبار |
|
||||
| 8.9 | `InventoryContract.GetStockMovements` | `InventoryContract` | `/InventoryMovementsPage/` | تاریخچه جابجایی موجودی |
|
||||
|
||||
---
|
||||
|
||||
## ۹. سفارشهای فروشگاه اصلی
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 9.1 | `UserOrderContract.GetAllUserOrderByFilter` | `UserOrderContract` | `/OrdersPage/`, `/UserOrderPage/{UserId}`, SalesReports | لیست سفارشها |
|
||||
| 9.2 | `UserOrderContract.GetUserOrder` | `UserOrderContract` | OrderDetailsDialog, Index | جزئیات سفارش |
|
||||
| 9.3 | `UserOrderContract.UpdateUserOrder` | `UserOrderContract` | UserOrderDetailsDialog, ChangeOrderStatus | ویرایش سفارش / tracking |
|
||||
| 9.4 | `UserOrderContract.UpdateOrderStatus` | `UserOrderContract` | ChangeOrderStatusDialog | تغییر وضعیت تحویل |
|
||||
| 9.5 | `UserOrderContract.CancelOrder` | `UserOrderContract` | CancelOrderDialog | لغو سفارش |
|
||||
| 9.6 | `UserOrderContract.DeleteUserOrder` | `UserOrderContract` | `/UserOrderPage/{UserId}` | حذف سفارش |
|
||||
| 9.7 | `UserOrderContract.ApplyDiscountToOrder` | `UserOrderContract` | ApplyDiscountDialog | اعمال تخفیف روی سفارش |
|
||||
|
||||
---
|
||||
|
||||
## ۱۰. پرداخت دستی و تراکنشها
|
||||
|
||||
> **دو گزارش تراکنش — اشتباه نگیرید:**
|
||||
>
|
||||
> | مسیر | جدول DB | کاربرد |
|
||||
> |------|---------|--------|
|
||||
> | `/payment/transactions` | `PaymentTransactions` | ممیزی درگاه (Authority، ZarinPal، کارت) |
|
||||
> | `/payment/ledger-transactions` | `Transactions` | دفتر مالی داخلی (نوع تراکنش، وضعیت، RefId) |
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 10.1 | `ManualPaymentContract.GetAllManualPayments` | `ManualPaymentContract` | `/payment/manual-payments` | لیست پرداختهای دستی |
|
||||
| 10.2 | `ManualPaymentContract.CreateManualPayment` | `ManualPaymentContract` | `ManualPaymentDialog` | ثبت پرداخت دستی (مثلاً Daya) |
|
||||
| 10.3 | `ManualPaymentContract.ApproveManualPayment` | `ManualPaymentContract` | ManualPayments, Dialog | تأیید پرداخت |
|
||||
| 10.4 | `ManualPaymentContract.RejectManualPayment` | `ManualPaymentContract` | ManualPayments, Dialog | رد پرداخت |
|
||||
| 10.5 | `PaymentTransactionContract.GetAllPaymentTransactionByFilter` | `PaymentTransactionContract` | `/payment/transactions` | گزارش read-only تراکنش **درگاه** (`PaymentTransactions`) |
|
||||
| 10.5b | `PaymentTransactionContract.GetPaymentTransactionSummary` | `PaymentTransactionContract` | `/payment/transactions` | کارت خلاصه گزارش درگاه |
|
||||
| 10.6 | `TransactionsContract.GetAllTransactionsByFilter` | `TransactionsContract` | `/payment/ledger-transactions` | گزارش read-only **دفتر مالی** (`Transactions`) |
|
||||
| 10.7 | `TransactionsContract.GetTransactionSummary` | `TransactionsContract` | `/payment/ledger-transactions` | کارت خلاصه دفتر تراکنش |
|
||||
| 10.8 | `UserPackagePurchaseContract.GetAllUserPackagePurchaseByFilter` | `UserPackagePurchaseContract` | `/club/purchases` | گزارش خرید پکیج |
|
||||
| 10.9 | `ClubMembershipCycleContract.GetAllClubMembershipCycleByFilter` | `ClubMembershipCycleContract` | `/club/cycles` | گزارش دوره عضویت |
|
||||
| 10.10 | `ClubMembershipCycleHistoryContract.GetAllClubMembershipCycleHistoryByFilter` | `ClubMembershipCycleHistoryContract` | `/club/history` (زیرتب دوره) | تاریخچه تغییرات دوره |
|
||||
|
||||
---
|
||||
|
||||
## ۱۱. کیف پول و تاریخچه
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 11.1 | `UserWalletContract.GetAllUserWalletByFilter` | `UserWalletContract` | `/wallets` | لیست کیف پول کاربران |
|
||||
| 11.2 | `UserWalletHistoryContract.GetAllUserWalletHistoryByFilter` | `UserWalletHistoryContract` | `/wallets` (تب تاریخچه) | لاگ تغییرات کیف پول |
|
||||
| 11.3 | `UserWalletContract.AdminManualCreditCharge` | `UserWalletContract` | `/payment/manual-credit-charges` | شارژ دستی کیف پول اصلی (Balance) |
|
||||
| 11.4 | `UserWalletContract.GetManualCreditCharges` | `UserWalletContract` | `/payment/manual-credit-charges` | لیست شارژهای دستی (Type=17) |
|
||||
|
||||
---
|
||||
|
||||
## ۱۲. قراردادهای کاربران
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 12.1 | `UserContractContract.GetAllUserContractByFilter` | `UserContractContract` | `/contracts` | لیست قراردادهای امضاشده (اصلی / باشگاه) |
|
||||
| 12.2 | Download PDF | — (local JS) | `/contracts` | دانلود PDF قرارداد ذخیرهشده |
|
||||
|
||||
---
|
||||
|
||||
## ۱۳. کمیسیون و پاداش
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 13.1 | `CommissionContract.GetAvailableWeeks` | `CommissionContract` | `/commission/dashboard`, WeekNumberPicker | انتخاب هفته |
|
||||
| 13.2 | `CommissionContract.GetWeeklyCommissionPool` | `CommissionContract` | `/commission/dashboard` | جزئیات pool هفتگی |
|
||||
| 13.3 | `CommissionContract.TriggerWeeklyCalculation` | `CommissionContract` | Dashboard, WorkerControl | اجرای دستی محاسبه هفتگی |
|
||||
| 13.4 | `CommissionContract.GetAllWeeklyPools` | `CommissionContract` | `/commission/reports` | گزارش poolهای هفتگی |
|
||||
| 13.5 | `CommissionContract.GetUserCommissionPayouts` | `CommissionContract` | `/commission/payouts` | payoutهای کاربران |
|
||||
| 13.6 | `CommissionContract.ProcessWithdrawal` | `CommissionContract` | UserPayouts, WithdrawalRequests | پردازش/واریز برداشت |
|
||||
| 13.7 | `CommissionContract.BulkCreditPayouts` | `CommissionContract` | `/commission/payouts` | واریز گروهی payoutها |
|
||||
| 13.8 | `CommissionContract.GetWithdrawalRequests` | `CommissionContract` | `/commission/withdrawals`, NavMenu | درخواستهای برداشت کاربران |
|
||||
| 13.9 | `CommissionContract.ApproveWithdrawal` | `CommissionContract` | WithdrawalRequests | تأیید درخواست برداشت |
|
||||
| 13.10 | `CommissionContract.RejectWithdrawal` | `CommissionContract` | WithdrawalRequests | رد درخواست برداشت |
|
||||
| 13.11 | `CommissionContract.GetWithdrawalReports` | `CommissionContract` | `/commission/withdrawal-reports` | گزارش برداشتها |
|
||||
| 13.12 | `CommissionContract.GetUserWeeklyBalances` | `CommissionContract` | `/network/balances` | گزارش بالانس هفتگی کاربران |
|
||||
| 13.13 | `CommissionContract.GetWorkerStatus` | `CommissionContract` | `/system/worker-control` | وضعیت worker محاسبه |
|
||||
| 13.14 | `CommissionContract.GetWorkerExecutionLogs` | `CommissionContract` | `/system/worker-control` | لاگ اجرای worker |
|
||||
|
||||
---
|
||||
|
||||
## ۱۴. شبکه فروش (MLM)
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 14.1 | `NetworkMembershipContract.GetNetworkTree` | `NetworkMembershipContract` | `/network/tree` | درخت شبکه (admin view) |
|
||||
| 14.2 | `NetworkMembershipContract.GetUserNetwork` | `NetworkMembershipContract` | `/network/user-info/{UserId}` | اطلاعات شبکه یک کاربر |
|
||||
| 14.3 | `NetworkMembershipContract.GetNetworkMembershipHistory` | `NetworkMembershipContract` | UserNetworkInfo | تاریخچه عضویت شبکه |
|
||||
| 14.4 | `NetworkMembershipContract.ChangeNetworkParent` | `NetworkMembershipContract` | `ChangeParentDialog` | تغییر والد در درخت |
|
||||
| 14.5 | `NetworkMembershipContract.GetNetworkStatistics` | `NetworkMembershipContract` | `/network/statistics` | آمار کلی شبکه (admin) |
|
||||
|
||||
---
|
||||
|
||||
## ۱۵. باشگاه مشتریان
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 15.1 | `ClubMembershipContract.GetAllClubMemberships` | `ClubMembershipContract` | `/club/members`, Index | لیست اعضا |
|
||||
| 15.2 | `ClubMembershipContract.ActivateClubMembership` | `ClubMembershipContract` | `ActivateClubDialog` | فعالسازی دستی عضویت |
|
||||
| 15.3 | `ClubMembershipContract.DeactivateClubMembership` | `ClubMembershipContract` | `DeactivateClubDialog` | غیرفعالسازی |
|
||||
| 15.4 | `ClubMembershipContract.GetClubMembershipHistory` | `ClubMembershipContract` | `/club/history` (زیرتب عضویت) | گزارش تاریخچه عضویت |
|
||||
| 15.5 | `ClubMembershipContract.GetClubStatistics` | `ClubMembershipContract` | `/club/statistics` | آمار باشگاه |
|
||||
| 15.6 | `ConfigurationContract.GetClubFeatures` | `ConfigurationContract` | `/club/features` | لیست featureهای باشگاه |
|
||||
| 15.7 | `ClubMembershipContract.GetUserClubFeatures` | `ClubMembershipContract` | `/club/features` | featureهای یک کاربر |
|
||||
| 15.8 | `ClubMembershipContract.ToggleUserClubFeature` | `ClubMembershipContract` | `/club/features` | فعال/غیرفعال feature برای کاربر |
|
||||
|
||||
---
|
||||
|
||||
## ۱۶. فروشگاه تخفیفی (Discount Shop)
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 16.1 | `IDiscountProductService` — CRUD | `DiscountProductContract` | `/discount-products` | مدیریت محصولات تخفیفی |
|
||||
| 16.2 | `IDiscountCategoryService` — CRUD | `DiscountCategoryContract` | `/discount-categories` | مدیریت دستهبندی تخفیفی |
|
||||
| 16.3 | `IDiscountOrderService.GetOrdersAsync` | `DiscountOrderContract.GetAllDiscountOrders` | `/discount-orders`, Dashboard widget | لیست سفارشها (admin) |
|
||||
| 16.4 | `IDiscountOrderService.GetByIdAsync` | `DiscountOrderContract` | OrderDetailsDialog | جزئیات سفارش |
|
||||
| 16.5 | `IDiscountOrderService.UpdateStatusAsync` | `DiscountOrderContract` | Orders page, OrderDetailsDialog | تغییر وضعیت پرداخت/ارسال |
|
||||
| 16.6 | `IDiscountOrderService.GetSalesReportAsync` | `DiscountOrderContract` | `/discount-sales-reports` | گزارش فروش |
|
||||
| 16.7 | `DiscountProductContract.GetDiscountProducts` | `DiscountProductContract` | `DiscountProductsAutoComplete` | autocomplete محصول |
|
||||
|
||||
---
|
||||
|
||||
## ۱۷. تگ محصولات
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 17.1 | `ITagService.GetTagsAsync` | `TagContract` | `/tags` | لیست تگها |
|
||||
| 17.2 | `ITagService.CreateAsync` / `UpdateAsync` / `DeleteAsync` | `TagContract` | TagEditDialog, TagManagement | CRUD تگ |
|
||||
| 17.3 | `ITagService.GetProductTagsAsync` | `ProductTagContract` | AssignTagsDialog | تگهای یک محصول |
|
||||
| 17.4 | `ITagService.AssignToProductAsync` / `RemoveProductTagAsync` | `ProductTagContract` | AssignTagsDialog, Discount products | تخصیص/حذف تگ |
|
||||
|
||||
---
|
||||
|
||||
## ۱۸. پیامهای عمومی
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 18.1 | `IPublicMessageService.GetMessagesAsync` | `PublicMessageContract` | `/public-messages` | لیست پیامها |
|
||||
| 18.2 | `IPublicMessageService.GetByIdAsync` | `PublicMessageContract` | PublicMessages | جزئیات |
|
||||
| 18.3 | `IPublicMessageService.CreateAsync` / `UpdateAsync` | `PublicMessageContract` | MessageFormDialog | ایجاد/ویرایش |
|
||||
| 18.4 | `IPublicMessageService.PublishAsync` / `ArchiveAsync` | `PublicMessageContract` | PublicMessages | انتشار/بایگانی |
|
||||
| 18.5 | `IPublicMessageService.DeleteAsync` | `PublicMessageContract` | PublicMessages | حذف |
|
||||
|
||||
---
|
||||
|
||||
## ۱۹. بلاگ
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 19.1 | `IBlogPostService.GetAllAsync` | `BlogPostContract` | `/blog/posts` | لیست پستها |
|
||||
| 19.2 | `IBlogPostService.GetByIdAsync` | `BlogPostContract` | BlogPostEditDialog | جزئیات پست |
|
||||
| 19.3 | `IBlogPostService.CreateAsync` / `UpdateAsync` | `BlogPostContract` | BlogPostEditDialog | ایجاد/ویرایش |
|
||||
| 19.4 | `IBlogPostService.DeleteAsync` | `BlogPostContract` | BlogPostManagement | حذف |
|
||||
| 19.5 | `IBlogPostService.PublishAsync` / `ArchiveAsync` | `BlogPostContract` | BlogPostManagement | انتشار/بایگانی |
|
||||
| 19.6 | `IBlogCategoryService` — CRUD | `BlogCategoryContract` | `/blog/categories` | مدیریت دستهبندی بلاگ |
|
||||
|
||||
---
|
||||
|
||||
## ۲۰. مدیریت محتوای سایت
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 20.1 | `ISitePageSettingsService.GetAllAsync` | `SitePageSettingsContract` | `/content/page-settings` | لیست صفحات (landing, about, contact, licenses) |
|
||||
| 20.2 | `ISitePageSettingsService.GetByKeyAsync` | `SitePageSettingsContract` | PageSettings dialogs | جزئیات یک صفحه |
|
||||
| 20.3 | `ISitePageSettingsService.SaveSettingsAsync` | `SitePageSettingsContract` | PageSettingsEditDialog | ذخیره hero + JSON تنظیمات |
|
||||
| 20.4 | `ISitePageSettingsService.SaveImageAsync` / `DeleteImageAsync` | `SitePageSettingsContract` | PageImagesDialog | مدیریت تصاویر گروهبندیشده |
|
||||
| 20.5 | `ISitePageService` — CRUD sections | `SitePageContract` | `/content/pages` | صفحات داینامیک قدیمی (section-based) |
|
||||
|
||||
---
|
||||
|
||||
## ۲۱. تنظیمات سیستم و سلامت
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 21.1 | `ConfigurationContract.GetAllConfigurations` | `ConfigurationContract` | `/system/configuration` | خواندن key-value تنظیمات (کمیسیون، شبکه، باشگاه، VAT…) |
|
||||
| 21.2 | `ConfigurationContract.CreateOrUpdateConfiguration` | `ConfigurationContract` | `/system/configuration` | ذخیره تنظیمات |
|
||||
| 21.3 | `HealthContract.GetSystemHealth` | `HealthContract` | `/system/health` | وضعیت سلامت سرویسها |
|
||||
| 21.4 | `AlertsMonitoring` | — (mock) | `/system/alerts` | **TODO — API هشدار هنوز وصل نشده** |
|
||||
|
||||
---
|
||||
|
||||
## ۲۲. نسخه اپلیکیشن
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 22.1 | `IAppVersionService.GetAllAsync` | `AppVersionContract` | `/settings/app-versions` | لیست نسخه FrontOffice/App |
|
||||
| 22.2 | `IAppVersionService.GetByNameAsync` | `AppVersionContract` | AppVersions | جزئیات یک app |
|
||||
| 22.3 | `IAppVersionService.UpdateAsync` | `AppVersionContract.UpdateAppVersion` | AppVersions dialog | بهروزرسانی نسخه + force update |
|
||||
|
||||
---
|
||||
|
||||
## ۲۳. کامپوننتهای مشترک (AutoComplete / Hub)
|
||||
|
||||
| کامپوننت | Contract | کاربرد |
|
||||
|----------|----------|--------|
|
||||
| `UserAutoComplete` | `UserContract` | جستجوی کاربر در فرمها |
|
||||
| `ProductsAutoComplete` | `ProductsContract` | جستجوی محصول |
|
||||
| `CategoryAutoComplete` / MultiSelect | `CategoryContract` | انتخاب دسته |
|
||||
| `PackageSelect` | `PackageContract` | انتخاب پکیج |
|
||||
| `WeekNumberPicker` | `CommissionContract` | انتخاب هفته |
|
||||
| `DiscountProductsAutoComplete` | `DiscountProductContract` | جستجوی محصول تخفیفی |
|
||||
| Hub pages (`ClubHub`, `DiscountShopHub`, `SystemHub`, `OrdersHub`, `BlogHub`) | — | مسیریابی tab-based؛ RPC در child component |
|
||||
|
||||
---
|
||||
|
||||
## ۲۴. سرویسهای ثبتشده ولی بدون استفاده / ناقص
|
||||
|
||||
| سرویس / صفحه | وضعیت |
|
||||
|--------------|--------|
|
||||
| `IBlogPostImageService` (`BlogPostImageContract`) | ثبت در DI؛ **هیچ صفحهای inject نمیکند** |
|
||||
| `RolePermissionsDialog` | UI permission؛ **ذخیره در backend TODO** |
|
||||
| `AuthorizationService` | فقط JWT role؛ **Permission API از CMS/BFF TODO** |
|
||||
| `/system/alerts` | داده mock؛ **AlertContract وجود ندارد** |
|
||||
|
||||
---
|
||||
|
||||
## نمودار جریان ورود ادمین
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[LoginPage] -->|CreateNewOtpToken| B[SMS OTP]
|
||||
B --> C[VerifyCodePage]
|
||||
C -->|VerifyOtpToken| D[JWT in localStorage]
|
||||
D --> E[All gRPC via TokenHandler]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## فهرست gRPC Contractهای فعال BackOffice
|
||||
|
||||
| Contract | Wrapper / استفاده مستقیم |
|
||||
|----------|---------------------------|
|
||||
| `OtpTokenContract` | Login |
|
||||
| `UserContract` | Login, Users, Dashboard, Search |
|
||||
| `RoleContract` | Roles, UserRoles |
|
||||
| `UserRoleContract` | UserRole pages |
|
||||
| `UserAddressContract` | UserAddress pages |
|
||||
| `PackageContract` | Packages, ManualPayment |
|
||||
| `ProductsContract` | Products, Categories drag-drop |
|
||||
| `CategoryContract` | Categories |
|
||||
| `UserOrderContract` | Orders |
|
||||
| `ManualPaymentContract` | Manual payments |
|
||||
| `PaymentTransactionContract` | Gateway transaction report (`/payment/transactions`) |
|
||||
| `TransactionsContract` | Ledger transaction report (`/payment/ledger-transactions`) |
|
||||
| `UserPackagePurchaseContract` | Club package purchase report |
|
||||
| `ClubMembershipCycleContract` | Club cycle report |
|
||||
| `ClubMembershipCycleHistoryContract` | Club cycle history report |
|
||||
| `UserWalletContract` | Wallet management |
|
||||
| `UserWalletHistoryContract` | Wallet change log |
|
||||
| `UserContractContract` | Signed contracts |
|
||||
| `CommissionContract` | Commission, Network balances, Worker |
|
||||
| `NetworkMembershipContract` | Network tree/info |
|
||||
| `ClubMembershipContract` | Club members/features |
|
||||
| `ConfigurationContract` | System config, Club features, Package |
|
||||
| `DiscountProductContract` | DiscountProductService |
|
||||
| `DiscountCategoryContract` | DiscountCategoryService |
|
||||
| `DiscountOrderContract` | DiscountOrderService |
|
||||
| `TagContract` / `ProductTagContract` | TagService |
|
||||
| `PublicMessageContract` | PublicMessageService |
|
||||
| `InventoryContract` | Inventory pages |
|
||||
| `BlogPostContract` | BlogPostService |
|
||||
| `BlogCategoryContract` | BlogCategoryService |
|
||||
| `BlogPostImageContract` | BlogPostImageService *(unused)* |
|
||||
| `SitePageSettingsContract` | SitePageSettingsService |
|
||||
| `SitePageContract` | SitePageService |
|
||||
| `AppVersionContract` | AppVersionService |
|
||||
| `HealthContract` | Health dashboard |
|
||||
|
||||
---
|
||||
|
||||
## مقایسه با FrontOffice
|
||||
|
||||
| موضوع | FrontOffice (مشتری) | BackOffice (ادمین) |
|
||||
|-------|----------------------|-------------------|
|
||||
| Auth | OTP + JWT (UserContract) | OTP + JWT (همان flow) |
|
||||
| Orders | Customer RPC | Admin filter + status management |
|
||||
| Discount Shop | User orders + checkout | Full CRUD + admin order management |
|
||||
| Commission | Read-only (my payouts) | Full admin (calculation, approval, reports) |
|
||||
| Network | My tree only | Full tree + change parent |
|
||||
| Content | Read page settings | Edit page settings + blog + public messages |
|
||||
| Wallet | My balances | All users wallet + history log |
|
||||
|
||||
مرجع مشابه برای فرانت مشتری: [`FrontOffice/docs/CUSTOMER-SERVICES.md`](../FrontOffice/docs/CUSTOMER-SERVICES.md)
|
||||
|
||||
---
|
||||
|
||||
## یادداشت deploy (خرداد ۱۴۰۵)
|
||||
|
||||
| گام | جزئیات |
|
||||
|-----|--------|
|
||||
| Proto | `Foursat.CMSMicroservice.Protobuf` **0.0.203** (گزارش دفتر تراکنش) |
|
||||
| CMS | deploy WebApi (شامل `GetTransactionSummary` + فیلتر تاریخ) |
|
||||
| BackOffice | NuGet 0.0.203 یا ProjectReference در monorepo |
|
||||
| FrontOffice | **redeploy لازم نیست** برای گزارش ledger؛ برای شارژ کیف اصلی proto **0.0.202** لازم است |
|
||||
|
||||
گزارشهای read-only (بخش ۱۰ و ۱۵): فقط فیلتر + grid + summary + CSV — بدون CRUD یا detail dialog.
|
||||
|
||||
مستندات فنی کامل: [`totalDoc/technical/TECH-08-SESSION-LOG-202606.md`](../../totalDoc/technical/TECH-08-SESSION-LOG-202606.md) — §10 Ledger transactions
|
||||
+2
-1008
File diff suppressed because it is too large
Load Diff
@@ -1,751 +1,3 @@
|
||||
# 📝 BackOffice — Changelog
|
||||
# Docs moved to totalDoc
|
||||
|
||||
**آخرین بروزرسانی:** بهمن ۱۴۰۴ (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) |
|
||||
See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation.
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
# BackOffice UI Deployment (Blazor WASM + nginx)
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: backoffice
|
||||
namespace: default
|
||||
labels:
|
||||
app: backoffice
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: backoffice
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: backoffice
|
||||
spec:
|
||||
containers:
|
||||
- name: backoffice
|
||||
image: 194.5.195.53:30080/admin/backoffice:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
env:
|
||||
- name: BLAZOR_ENVIRONMENT
|
||||
value: "Staging"
|
||||
resources:
|
||||
requests:
|
||||
memory: "128Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
imagePullSecrets:
|
||||
- name: gitea-registry-secret
|
||||
|
||||
---
|
||||
# BackOffice UI Service
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: backoffice-svc
|
||||
namespace: default
|
||||
spec:
|
||||
selector:
|
||||
app: backoffice
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
type: ClusterIP
|
||||
|
||||
---
|
||||
# BackOffice UI Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: backoffice-ingress
|
||||
namespace: default
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
nginx.ingress.kubernetes.io/service-upstream: "true"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- backoffice.se.kbs1.ir
|
||||
secretName: backoffice-tls
|
||||
rules:
|
||||
- host: backoffice.se.kbs1.ir
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: backoffice-svc
|
||||
port:
|
||||
number: 80
|
||||
@@ -9,6 +9,8 @@
|
||||
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<!-- Suppress pre-existing warnings that do not affect runtime correctness -->
|
||||
<NoWarn>MUD0002;CA2022;BL0005</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -136,9 +138,12 @@
|
||||
<!-- This enables direct communication with CMS (no BFF needed) -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<!-- CMS Protobuf NuGet package (used for both local dev and Docker builds) -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.178" />
|
||||
<!-- CMS Protobuf: NuGet in CI/Docker; ProjectReference for local monorepo dev -->
|
||||
<ItemGroup Condition="Exists('../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj')">
|
||||
<ProjectReference Include="../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="!Exists('../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj')">
|
||||
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.208" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
|
||||
@@ -8,21 +8,46 @@ namespace BackOffice.Common.BaseComponents;
|
||||
public partial class DateRangePicker
|
||||
{
|
||||
private DateRange _dateRange = new();
|
||||
private MudDateRangePicker _picker = default!;
|
||||
private DateTime? _lastSyncedFrom;
|
||||
private DateTime? _lastSyncedTo;
|
||||
|
||||
private MudDateRangePicker _picker;
|
||||
[Parameter] public string Label { get; set; } = "انتخاب بازه زمانی";
|
||||
[Parameter] public DateTime? DefaultStart { get; set; }
|
||||
[Parameter] public DateTime? DefaultEnd { get; set; }
|
||||
[Parameter] public EventCallback<DateRange> OnChanged { get; set; }
|
||||
|
||||
[Parameter] public DateTime? From { get; set; }
|
||||
[Parameter] public EventCallback<DateTime?> FromChanged { get; set; }
|
||||
[Parameter] public DateTime? To { get; set; }
|
||||
[Parameter] public EventCallback<DateTime?> ToChanged { get; set; }
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
if (DefaultStart.HasValue)
|
||||
_dateRange.Start = DefaultStart.Value.Date;
|
||||
|
||||
if (DefaultEnd.HasValue)
|
||||
_dateRange.End = DefaultEnd.Value.Date;
|
||||
var start = From ?? DefaultStart;
|
||||
var end = To ?? DefaultEnd;
|
||||
ApplyExternalRange(start, end);
|
||||
_lastSyncedFrom = From;
|
||||
_lastSyncedTo = To;
|
||||
}
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
// فقط وقتی والد From/To را عوض کرده (مثلاً پاک کردن فیلتر)، نه هنگام انتخاب داخل پیکر
|
||||
if (From == _lastSyncedFrom && To == _lastSyncedTo)
|
||||
return;
|
||||
|
||||
_lastSyncedFrom = From;
|
||||
_lastSyncedTo = To;
|
||||
ApplyExternalRange(From, To);
|
||||
}
|
||||
|
||||
private void ApplyExternalRange(DateTime? start, DateTime? end)
|
||||
{
|
||||
_dateRange = new DateRange(start?.Date, end?.Date);
|
||||
}
|
||||
|
||||
public CultureInfo GetPersianCulture()
|
||||
{
|
||||
var culture = new CultureInfo("fa-IR");
|
||||
@@ -53,19 +78,35 @@ public partial class DateRangePicker
|
||||
culture.NumberFormat.NumberNegativePattern = 0;
|
||||
return culture;
|
||||
}
|
||||
|
||||
private async Task OnClickOK()
|
||||
{
|
||||
if (_picker.DateRange is not null)
|
||||
{
|
||||
await OnChanged.InvokeAsync(_picker.DateRange);
|
||||
}
|
||||
|
||||
var range = _picker.DateRange ?? _dateRange;
|
||||
_dateRange = range ?? new DateRange();
|
||||
await NotifyRangeChangedAsync(_dateRange);
|
||||
await _picker.CloseAsync();
|
||||
}
|
||||
|
||||
private async Task OnClickClear()
|
||||
{
|
||||
_dateRange = new DateRange();
|
||||
await OnChanged.InvokeAsync(_dateRange);
|
||||
await NotifyRangeChangedAsync(_dateRange);
|
||||
await _picker.CloseAsync();
|
||||
}
|
||||
|
||||
private async Task NotifyRangeChangedAsync(DateRange range)
|
||||
{
|
||||
var start = range.Start?.Date;
|
||||
var end = range.End?.Date;
|
||||
|
||||
_lastSyncedFrom = start;
|
||||
_lastSyncedTo = end;
|
||||
|
||||
if (From != start)
|
||||
await FromChanged.InvokeAsync(start);
|
||||
if (To != end)
|
||||
await ToChanged.InvokeAsync(end);
|
||||
|
||||
await OnChanged.InvokeAsync(range);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,15 @@ 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.UserWalletChangeLog;
|
||||
using CMSMicroservice.Protobuf.Protos.UserWalletHistory;
|
||||
using CMSMicroservice.Protobuf.Protos.PaymentTransaction;
|
||||
using CMSMicroservice.Protobuf.Protos.Transactions;
|
||||
using CMSMicroservice.Protobuf.Protos.UserPackagePurchase;
|
||||
using CMSMicroservice.Protobuf.Protos.ClubMembershipCycle;
|
||||
using CMSMicroservice.Protobuf.Protos.ClubMembershipCycleHistory;
|
||||
using BackOffice.Common.Utilities;
|
||||
using BackOffice.Services.DiscountProduct;
|
||||
using BackOffice.Services.DiscountCategory;
|
||||
@@ -91,6 +97,7 @@ public static class ConfigureServices
|
||||
services.AddScoped<IBlogCategoryService, BlogCategoryService>();
|
||||
services.AddScoped<IBlogPostImageService, BlogPostImageService>();
|
||||
services.AddScoped<ISitePageService, SitePageService>();
|
||||
services.AddScoped<ISitePageSettingsService, SitePageSettingsService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
@@ -158,13 +165,21 @@ 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 UserWalletChangeLogContract.UserWalletChangeLogContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new UserWalletHistoryContract.UserWalletHistoryContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
|
||||
// Admin Report Services
|
||||
services.AddTransient(sp => new PaymentTransactionContract.PaymentTransactionContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new TransactionsContract.TransactionsContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new UserPackagePurchaseContract.UserPackagePurchaseContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new ClubMembershipCycleContract.ClubMembershipCycleContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new ClubMembershipCycleHistoryContract.ClubMembershipCycleHistoryContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using BackOffice.Common.Utilities;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using BackOffice.Common.Utilities;
|
||||
using Blazored.LocalStorage;
|
||||
|
||||
namespace BackOffice.Common.Utilities;
|
||||
@@ -19,9 +20,41 @@ public class AppTokenProvider : ITokenProvider
|
||||
{
|
||||
var authorizationToken = await _localStorage.GetItemAsync<string>(GlobalConstants.JwtTokenKey);
|
||||
if (!string.IsNullOrEmpty(authorizationToken))
|
||||
_token = authorizationToken.ToString().Replace("Bearer ", "");
|
||||
{
|
||||
var raw = authorizationToken.ToString().Replace("Bearer ", "");
|
||||
// Don't send expired tokens — they cause 401 on anonymous endpoints like login
|
||||
if (IsTokenExpired(raw))
|
||||
{
|
||||
await _localStorage.RemoveItemAsync(GlobalConstants.JwtTokenKey);
|
||||
_token = null;
|
||||
return _token;
|
||||
}
|
||||
_token = raw;
|
||||
}
|
||||
}
|
||||
else if (IsTokenExpired(_token))
|
||||
{
|
||||
// Cached token has expired since last check
|
||||
await _localStorage.RemoveItemAsync(GlobalConstants.JwtTokenKey);
|
||||
_token = null;
|
||||
}
|
||||
|
||||
return _token;
|
||||
}
|
||||
|
||||
private static bool IsTokenExpired(string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var jwt = handler.ReadJwtToken(token);
|
||||
// Add 30-second buffer to avoid edge-case race
|
||||
return jwt.ValidTo < DateTime.UtcNow.AddSeconds(-30);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Malformed token — treat as expired
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
|
||||
namespace BackOffice.Common.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// خروجی سازگار با Excel فارسی:
|
||||
/// - XML Spreadsheet (.xls) برای ستونبندی مطمئن + متن فارسی
|
||||
/// - CSV با جداکننده ; بهعنوان جایگزین سبک
|
||||
/// </summary>
|
||||
public static class CsvExportHelper
|
||||
{
|
||||
/// <summary>جداکننده پیشفرض ویندوز فارسی برای CSV.</summary>
|
||||
public const char Separator = ';';
|
||||
|
||||
public static string Escape(string? value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return string.Empty;
|
||||
|
||||
// همیشه quote تا Excel فارسی ستون را نشکند
|
||||
return $"\"{value.Replace("\"", "\"\"")}\"";
|
||||
}
|
||||
|
||||
public static string Cell(object? value) => value switch
|
||||
{
|
||||
null => Escape(string.Empty),
|
||||
string s => Escape(s),
|
||||
bool b => b ? "1" : "0",
|
||||
byte or sbyte or short or ushort or int or uint or long or ulong or float or double or decimal
|
||||
=> Convert.ToString(value, CultureInfo.InvariantCulture) ?? "0",
|
||||
IFormattable f => Escape(f.ToString(null, CultureInfo.InvariantCulture) ?? string.Empty),
|
||||
_ => Escape(Convert.ToString(value, CultureInfo.InvariantCulture))
|
||||
};
|
||||
|
||||
public static string Row(params object?[] cells) =>
|
||||
string.Join(Separator, cells.Select(Cell));
|
||||
|
||||
/// <summary>
|
||||
/// نام نمایشی مشتری؛ اگر خالی باشد موبایل یا شناسه کاربر.
|
||||
/// </summary>
|
||||
public static string CustomerName(string? fullName, string? mobile = null, long? userId = null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(fullName))
|
||||
return fullName.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(mobile))
|
||||
return mobile.Trim();
|
||||
if (userId is > 0)
|
||||
return $"کاربر {userId}";
|
||||
return "—";
|
||||
}
|
||||
|
||||
public static string ToBase64(StringBuilder body)
|
||||
{
|
||||
// CRLF صریح — Excel روی ویندوز با \n لینوکس گاهی بههم میریزد
|
||||
var text = body.ToString().Replace("\r\n", "\n").Replace("\r", "\n").Replace("\n", "\r\n");
|
||||
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(text)).ToArray();
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// خروجی SpreadsheetML که Excel بهصورت ستونستون و با UTF-8 درست باز میکند.
|
||||
/// فایل را با پسوند .xls ذخیره کنید.
|
||||
/// </summary>
|
||||
public static string ToExcelXmlBase64(string sheetName, IReadOnlyList<string> headers, IEnumerable<object?[]> rows)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine(@"<?xml version=""1.0"" encoding=""UTF-8""?>");
|
||||
sb.AppendLine(@"<?mso-application progid=""Excel.Sheet""?>");
|
||||
sb.AppendLine(@"<Workbook xmlns=""urn:schemas-microsoft-com:office:spreadsheet""");
|
||||
sb.AppendLine(@" xmlns:o=""urn:schemas-microsoft-com:office:office""");
|
||||
sb.AppendLine(@" xmlns:x=""urn:schemas-microsoft-com:office:excel""");
|
||||
sb.AppendLine(@" xmlns:ss=""urn:schemas-microsoft-com:office:spreadsheet""");
|
||||
sb.AppendLine(@" xmlns:html=""http://www.w3.org/TR/REC-html40"">");
|
||||
sb.AppendLine(@"<Styles>");
|
||||
sb.AppendLine(@"<Style ss:ID=""Header""><Font ss:Bold=""1""/></Style>");
|
||||
sb.AppendLine(@"<Style ss:ID=""Text""><NumberFormat ss:Format=""@""/></Style>");
|
||||
sb.AppendLine(@"</Styles>");
|
||||
sb.Append("<Worksheet ss:Name=\"").Append(XmlEscape(sheetName)).AppendLine("\">");
|
||||
sb.AppendLine("<Table>");
|
||||
|
||||
sb.AppendLine("<Row>");
|
||||
foreach (var header in headers)
|
||||
{
|
||||
sb.Append("<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">")
|
||||
.Append(XmlEscape(header))
|
||||
.AppendLine("</Data></Cell>");
|
||||
}
|
||||
sb.AppendLine("</Row>");
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
sb.AppendLine("<Row>");
|
||||
foreach (var cell in row)
|
||||
AppendExcelCell(sb, cell);
|
||||
sb.AppendLine("</Row>");
|
||||
}
|
||||
|
||||
sb.AppendLine("</Table>");
|
||||
sb.AppendLine("</Worksheet>");
|
||||
sb.AppendLine("</Workbook>");
|
||||
|
||||
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
|
||||
private static void AppendExcelCell(StringBuilder sb, object? value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case null:
|
||||
sb.AppendLine(@"<Cell ss:StyleID=""Text""><Data ss:Type=""String""></Data></Cell>");
|
||||
break;
|
||||
case string s:
|
||||
sb.Append(@"<Cell ss:StyleID=""Text""><Data ss:Type=""String"">")
|
||||
.Append(XmlEscape(s))
|
||||
.AppendLine("</Data></Cell>");
|
||||
break;
|
||||
case bool b:
|
||||
sb.Append(@"<Cell><Data ss:Type=""Boolean"">")
|
||||
.Append(b ? "1" : "0")
|
||||
.AppendLine("</Data></Cell>");
|
||||
break;
|
||||
case byte or sbyte or short or ushort or int or uint or long or ulong:
|
||||
sb.Append(@"<Cell><Data ss:Type=""Number"">")
|
||||
.Append(Convert.ToString(value, CultureInfo.InvariantCulture))
|
||||
.AppendLine("</Data></Cell>");
|
||||
break;
|
||||
case float or double or decimal:
|
||||
sb.Append(@"<Cell><Data ss:Type=""Number"">")
|
||||
.Append(Convert.ToString(value, CultureInfo.InvariantCulture))
|
||||
.AppendLine("</Data></Cell>");
|
||||
break;
|
||||
default:
|
||||
sb.Append(@"<Cell ss:StyleID=""Text""><Data ss:Type=""String"">")
|
||||
.Append(XmlEscape(Convert.ToString(value, CultureInfo.InvariantCulture)))
|
||||
.AppendLine("</Data></Cell>");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static string XmlEscape(string? value) =>
|
||||
WebUtility.HtmlEncode(value ?? string.Empty);
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using DateTimeConverterCL;
|
||||
|
||||
namespace BackOffice.Common.Utilities;
|
||||
|
||||
/// <summary>ساخت HTML فاکتور برای چاپ مرورگر (jsHtmlToPdf).</summary>
|
||||
public static class OrderInvoicePrint
|
||||
{
|
||||
public sealed class InvoiceModel
|
||||
{
|
||||
public string ShopTitle { get; set; } = "فروشگاه";
|
||||
public string OrderId { get; set; } = "";
|
||||
public string? CustomerName { get; set; }
|
||||
public string? CustomerMobile { get; set; }
|
||||
public string? CustomerNationalCode { get; set; }
|
||||
public string? Address { get; set; }
|
||||
public string? PostalCode { get; set; }
|
||||
public string? PaymentStatus { get; set; }
|
||||
public string? PaymentMethod { get; set; }
|
||||
public string? TransactionId { get; set; }
|
||||
public string? TrackingCode { get; set; }
|
||||
public DateTime? Created { get; set; }
|
||||
public DateTime? PaymentDate { get; set; }
|
||||
public long Subtotal { get; set; }
|
||||
public long DiscountTotal { get; set; }
|
||||
public long CreditUsed { get; set; }
|
||||
public long GatewayAmount { get; set; }
|
||||
public long VatAmount { get; set; }
|
||||
public long TotalAmount { get; set; }
|
||||
public List<InvoiceLine> Lines { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class InvoiceLine
|
||||
{
|
||||
public string Title { get; set; } = "";
|
||||
public int Quantity { get; set; }
|
||||
public long UnitPrice { get; set; }
|
||||
public long Discount { get; set; }
|
||||
public long LineTotal { get; set; }
|
||||
}
|
||||
|
||||
public static string BuildHtml(InvoiceModel model)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("<!DOCTYPE html><html lang=\"fa\" dir=\"rtl\"><head><meta charset=\"utf-8\"/>");
|
||||
sb.Append("<title>").Append(Esc($"فاکتور {model.OrderId}")).Append("</title>");
|
||||
sb.Append("""
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: Tahoma, "Segoe UI", sans-serif; color: #111; margin: 24px; font-size: 13px; }
|
||||
h1 { font-size: 20px; margin: 0 0 4px; }
|
||||
.muted { color: #555; }
|
||||
.header { display: flex; justify-content: space-between; align-items: flex-start; border-bottom: 2px solid #222; padding-bottom: 12px; margin-bottom: 16px; }
|
||||
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 24px; margin-bottom: 16px; }
|
||||
.label { color: #666; font-size: 11px; }
|
||||
.value { font-weight: 600; }
|
||||
table { width: 100%; border-collapse: collapse; margin: 12px 0 16px; }
|
||||
th, td { border: 1px solid #ccc; padding: 8px; text-align: right; }
|
||||
th { background: #f3f3f3; }
|
||||
.num { text-align: left; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
||||
.totals { width: 320px; margin-right: auto; }
|
||||
.totals td { border: none; padding: 4px 0; }
|
||||
.totals .grand { font-size: 15px; font-weight: 700; border-top: 2px solid #222; padding-top: 8px; }
|
||||
.footer { margin-top: 28px; font-size: 11px; color: #666; border-top: 1px dashed #bbb; padding-top: 8px; }
|
||||
@media print {
|
||||
body { margin: 0; }
|
||||
.no-print { display: none !important; }
|
||||
}
|
||||
</style>
|
||||
""");
|
||||
sb.Append("</head><body>");
|
||||
sb.Append("<div class=\"header\"><div>");
|
||||
sb.Append("<h1>").Append(Esc(model.ShopTitle)).Append("</h1>");
|
||||
sb.Append("<div class=\"muted\">فاکتور فروش</div></div><div>");
|
||||
sb.Append("<div><span class=\"label\">شماره فاکتور: </span><span class=\"value\">").Append(Esc(model.OrderId)).Append("</span></div>");
|
||||
if (model.Created.HasValue)
|
||||
sb.Append("<div><span class=\"label\">تاریخ ثبت: </span>").Append(Esc(FormatDate(model.Created.Value))).Append("</div>");
|
||||
if (model.PaymentDate.HasValue)
|
||||
sb.Append("<div><span class=\"label\">تاریخ پرداخت: </span>").Append(Esc(FormatDate(model.PaymentDate.Value))).Append("</div>");
|
||||
sb.Append("</div></div>");
|
||||
|
||||
sb.Append("<div class=\"grid\">");
|
||||
AppendField(sb, "نام مشتری", model.CustomerName);
|
||||
AppendField(sb, "موبایل", model.CustomerMobile);
|
||||
AppendField(sb, "کد ملی", model.CustomerNationalCode);
|
||||
AppendField(sb, "کد پستی", model.PostalCode);
|
||||
AppendField(sb, "وضعیت پرداخت", model.PaymentStatus);
|
||||
AppendField(sb, "روش پرداخت", model.PaymentMethod);
|
||||
AppendField(sb, "کد تراکنش", model.TransactionId);
|
||||
AppendField(sb, "کد رهگیری", model.TrackingCode);
|
||||
if (!string.IsNullOrWhiteSpace(model.Address))
|
||||
{
|
||||
sb.Append("<div style=\"grid-column:1/-1\"><div class=\"label\">آدرس</div><div class=\"value\">")
|
||||
.Append(Esc(model.Address)).Append("</div></div>");
|
||||
}
|
||||
sb.Append("</div>");
|
||||
|
||||
sb.Append("<table><thead><tr>");
|
||||
sb.Append("<th>#</th><th>کالا</th><th>تعداد</th><th>قیمت واحد</th><th>تخفیف</th><th>مبلغ</th>");
|
||||
sb.Append("</tr></thead><tbody>");
|
||||
var i = 1;
|
||||
foreach (var line in model.Lines)
|
||||
{
|
||||
sb.Append("<tr>");
|
||||
sb.Append("<td class=\"num\">").Append(i++).Append("</td>");
|
||||
sb.Append("<td>").Append(Esc(line.Title)).Append("</td>");
|
||||
sb.Append("<td class=\"num\">").Append(line.Quantity).Append("</td>");
|
||||
sb.Append("<td class=\"num\">").Append(Money(line.UnitPrice)).Append("</td>");
|
||||
sb.Append("<td class=\"num\">").Append(Money(line.Discount)).Append("</td>");
|
||||
sb.Append("<td class=\"num\">").Append(Money(line.LineTotal)).Append("</td>");
|
||||
sb.Append("</tr>");
|
||||
}
|
||||
if (model.Lines.Count == 0)
|
||||
sb.Append("<tr><td colspan=\"6\" class=\"muted\">آیتمی ثبت نشده است</td></tr>");
|
||||
sb.Append("</tbody></table>");
|
||||
|
||||
sb.Append("<table class=\"totals\"><tbody>");
|
||||
if (model.Subtotal > 0)
|
||||
AppendTotalRow(sb, "جمع اقلام", model.Subtotal);
|
||||
if (model.DiscountTotal > 0)
|
||||
AppendTotalRow(sb, "تخفیف", model.DiscountTotal);
|
||||
if (model.VatAmount > 0)
|
||||
AppendTotalRow(sb, "مالیات بر ارزش افزوده", model.VatAmount);
|
||||
if (model.CreditUsed > 0)
|
||||
AppendTotalRow(sb, "پرداخت از کیف اعتباری", model.CreditUsed);
|
||||
if (model.GatewayAmount > 0)
|
||||
AppendTotalRow(sb, "پرداخت درگاه / نقدی", model.GatewayAmount);
|
||||
sb.Append("<tr class=\"grand\"><td>مبلغ قابل پرداخت</td><td class=\"num\">")
|
||||
.Append(Money(model.TotalAmount)).Append(" تومان</td></tr>");
|
||||
sb.Append("</tbody></table>");
|
||||
|
||||
sb.Append("<div class=\"footer\">چاپشده از پنل ادمین — ").Append(Esc(FormatDate(DateTime.Now))).Append("</div>");
|
||||
sb.Append("</body></html>");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static void AppendField(StringBuilder sb, string label, string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return;
|
||||
sb.Append("<div><div class=\"label\">").Append(Esc(label)).Append("</div><div class=\"value\">")
|
||||
.Append(Esc(value)).Append("</div></div>");
|
||||
}
|
||||
|
||||
private static void AppendTotalRow(StringBuilder sb, string label, long amount)
|
||||
{
|
||||
sb.Append("<tr><td>").Append(Esc(label)).Append("</td><td class=\"num\">")
|
||||
.Append(Money(amount)).Append(" تومان</td></tr>");
|
||||
}
|
||||
|
||||
private static string Money(long amount) => amount.ToString("N0", CultureInfo.InvariantCulture);
|
||||
|
||||
private static string FormatDate(DateTime dt)
|
||||
{
|
||||
try { return dt.ToLocalTime().MiladiToJalaliWithTime(); }
|
||||
catch { return dt.ToLocalTime().ToString("yyyy/MM/dd HH:mm"); }
|
||||
}
|
||||
|
||||
private static string Esc(string? s) => WebUtility.HtmlEncode(s ?? "");
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
namespace BackOffice.Common.Utilities;
|
||||
|
||||
using CMSMicroservice.Protobuf.Protos;
|
||||
using MudBlazor;
|
||||
|
||||
public static class ReportLabels
|
||||
{
|
||||
public static string GetPaymentStatusLabel(bool success) => success ? "موفق" : "در انتظار / ناموفق";
|
||||
|
||||
public static string GetTransactionPaymentStatusLabel(PaymentStatus? status) => status switch
|
||||
{
|
||||
PaymentStatus.Success => "موفق",
|
||||
PaymentStatus.Pending => "در انتظار",
|
||||
PaymentStatus.Reject => "رد شده",
|
||||
_ => "—"
|
||||
};
|
||||
|
||||
public static Color GetTransactionPaymentStatusColor(PaymentStatus? status) => status switch
|
||||
{
|
||||
PaymentStatus.Success => Color.Success,
|
||||
PaymentStatus.Pending => Color.Warning,
|
||||
PaymentStatus.Reject => Color.Error,
|
||||
_ => Color.Default
|
||||
};
|
||||
|
||||
public static string GetTransactionTypeLabel(TransactionType? type) => type switch
|
||||
{
|
||||
TransactionType.Buy => "خرید",
|
||||
TransactionType.DepositIpg => "واریز درگاه",
|
||||
TransactionType.DepositExternal1 => "واریز خارجی",
|
||||
TransactionType.Withdraw => "برداشت",
|
||||
TransactionType.NetworkCommission => "کمیسیون شبکه",
|
||||
TransactionType.ClubActivation => "فعالسازی باشگاه",
|
||||
TransactionType.DiscountWalletCharge => "شارژ کیف تخفیف",
|
||||
TransactionType.DiscountShopPurchase => "خرید فروشگاه تخفیف",
|
||||
TransactionType.MagicWalletDeposit => "واریز کیف جادویی",
|
||||
TransactionType.MagicWalletBonus => "بونوس کیف جادویی",
|
||||
TransactionType.CreditWalletCharge => "شارژ کیف اصلی",
|
||||
TransactionType.ManualCreditWalletCharge => "شارژ دستی کیف اصلی",
|
||||
_ => type?.ToString() ?? "—"
|
||||
};
|
||||
|
||||
public static string GetPurchaseMethodLabel(int method) => method switch
|
||||
{
|
||||
1 => "دایا",
|
||||
2 => "درگاه",
|
||||
3 => "دستی",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
|
||||
public static string GetClubMembershipActionLabel(int action) => action switch
|
||||
{
|
||||
0 => "فعالسازی",
|
||||
1 => "غیرفعالسازی",
|
||||
2 => "ویرایش",
|
||||
3 => "اصلاح دستی",
|
||||
_ => action.ToString()
|
||||
};
|
||||
|
||||
public static string GetClubCycleActionLabel(int action) => action switch
|
||||
{
|
||||
0 => "شروع دوره",
|
||||
1 => "ورود به حالت جادویی",
|
||||
2 => "پایان حالت جادویی",
|
||||
3 => "تکمیل دوره",
|
||||
4 => "لغو",
|
||||
5 => "اصلاح دستی",
|
||||
_ => action.ToString()
|
||||
};
|
||||
|
||||
public static string GetMagicStateLabel(int? magicStarted, DateTime? started, DateTime? completed)
|
||||
{
|
||||
if (completed.HasValue) return "جادویی تکمیلشده";
|
||||
if (started.HasValue) return "در حالت جادویی";
|
||||
return "عادی";
|
||||
}
|
||||
|
||||
public static string FormatActiveChange(bool oldActive, bool newActive) =>
|
||||
$"{(oldActive ? "فعال" : "غیرفعال")} → {(newActive ? "فعال" : "غیرفعال")}";
|
||||
|
||||
public static string FormatBoolChange(bool oldValue, bool newValue) =>
|
||||
$"{(oldValue ? "بله" : "خیر")} → {(newValue ? "بله" : "خیر")}";
|
||||
}
|
||||
+10
-10
@@ -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/library/nginx:alpine AS final
|
||||
FROM 194.5.195.53:32082/nginx:alpine AS final
|
||||
WORKDIR /usr/share/nginx/html
|
||||
|
||||
# Remove default nginx files
|
||||
@@ -25,14 +25,14 @@ RUN rm -rf /usr/share/nginx/html/*
|
||||
# Copy published wwwroot (Blazor WASM output)
|
||||
COPY --from=build /app/publish/wwwroot .
|
||||
|
||||
# Configure nginx for SPA routing
|
||||
RUN echo 'server { \
|
||||
listen 80; \
|
||||
server_name _; \
|
||||
location / { \
|
||||
root /usr/share/nginx/html; \
|
||||
try_files $uri $uri/ /index.html; \
|
||||
} \
|
||||
}' > /etc/nginx/conf.d/default.conf
|
||||
# Copy entrypoint script that sets blazor-environment header
|
||||
COPY ["BackOffice/docker-entrypoint.sh", "/docker-entrypoint.sh"]
|
||||
RUN chmod +x /docker-entrypoint.sh
|
||||
|
||||
# Default Blazor environment (override via K8s env var BLAZOR_ENVIRONMENT)
|
||||
ENV BLAZOR_ENVIRONMENT=Staging
|
||||
# Default gateway URL for gRPC calls (injected into appsettings at startup)
|
||||
ENV GW_URL=https://cms.kbs2.ir
|
||||
|
||||
EXPOSE 80
|
||||
CMD ["/docker-entrypoint.sh"]
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<!-- Local packages (dev builds) -->
|
||||
<!-- <add key="Local" value="/home/masoud/Apps/project/FourSat/nupkg" /> -->
|
||||
<!-- Nexus (hosts FourSat packages + proxies nuget.org) -->
|
||||
<add key="Nexus" value="http://194.5.195.53:32081/repository/nuget-all/index.json" allowInsecureConnections="true" />
|
||||
</packageSources>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.DiscountProduct
|
||||
|
||||
<MudAutocomplete T="DiscountProductDto"
|
||||
Label="@Label"
|
||||
Value="@_item"
|
||||
DebounceInterval="700"
|
||||
ValueChanged="@((e) => OnSelected(e))"
|
||||
ToStringFunc="@(e => e == null ? null : e.Title)"
|
||||
SearchFunc="@Search"
|
||||
Variant="Variant.Outlined"
|
||||
Clearable="true"
|
||||
ShowProgressIndicator="true"
|
||||
CoerceText="false"
|
||||
OnClearButtonClick="()=> OnSelected(null)" />
|
||||
@@ -0,0 +1,83 @@
|
||||
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace BackOffice.Pages.AutoComplete;
|
||||
|
||||
public partial class DiscountProductsAutoComplete
|
||||
{
|
||||
[Inject] public DiscountProductContract.DiscountProductContractClient DiscountProductService { get; set; } = default!;
|
||||
|
||||
[Parameter] public long? Value { get; set; }
|
||||
[Parameter] public string? Label { get; set; } = "محصول تخفیفی";
|
||||
[Parameter] public EventCallback<long?> ValueChanged { get; set; }
|
||||
|
||||
private List<DiscountProductDto> _items = new();
|
||||
private DiscountProductDto? _item;
|
||||
|
||||
protected override async void OnParametersSet()
|
||||
{
|
||||
await base.OnParametersSetAsync();
|
||||
|
||||
if (Value.HasValue && Value.Value > 0 && (_item == null || _item.Id != Value.Value))
|
||||
{
|
||||
try
|
||||
{
|
||||
var getAll = await DiscountProductService.GetDiscountProductsAsync(new GetDiscountProductsRequest
|
||||
{
|
||||
PageNumber = 1,
|
||||
PageSize = 50
|
||||
});
|
||||
|
||||
if (getAll?.Models != null)
|
||||
{
|
||||
_item = getAll.Models.FirstOrDefault(x => x.Id == Value.Value);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Silently handle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnSelected(DiscountProductDto? model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
_item = null;
|
||||
await ValueChanged.InvokeAsync(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
_item = model;
|
||||
await ValueChanged.InvokeAsync(model.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<DiscountProductDto>> Search(string value, CancellationToken token)
|
||||
{
|
||||
var request = new GetDiscountProductsRequest
|
||||
{
|
||||
PageNumber = 1,
|
||||
PageSize = 9
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
request.SearchQuery = value;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var getAll = await DiscountProductService.GetDiscountProductsAsync(request);
|
||||
_items = getAll?.Models?.ToList() ?? new List<DiscountProductDto>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_items = new List<DiscountProductDto>();
|
||||
}
|
||||
|
||||
return _items;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.Package
|
||||
|
||||
<MudSelect T="long?"
|
||||
Label="@Label"
|
||||
Value="@SelectedPackageId"
|
||||
ValueChanged="OnValueChanged"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Clearable="true"
|
||||
Style="min-width: 160px;">
|
||||
<MudSelectItem T="long?" Value="@((long?)null)">همه پکیجها</MudSelectItem>
|
||||
@foreach (var pkg in _packages)
|
||||
{
|
||||
<MudSelectItem T="long?" Value="@((long?)pkg.Id)">@pkg.Title</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
@@ -0,0 +1,46 @@
|
||||
using CMSMicroservice.Protobuf.Protos.Package;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace BackOffice.Pages.AutoComplete;
|
||||
|
||||
public partial class PackageSelect
|
||||
{
|
||||
[Inject] public PackageContract.PackageContractClient PackageClient { get; set; }
|
||||
|
||||
[Parameter] public string Label { get; set; } = "پکیج";
|
||||
[Parameter] public long? SelectedPackageId { get; set; }
|
||||
[Parameter] public EventCallback<long?> SelectedPackageIdChanged { get; set; }
|
||||
|
||||
private List<GetAllPackageByFilterResponseModel> _packages = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadPackages();
|
||||
}
|
||||
|
||||
private async Task LoadPackages()
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new GetAllPackageByFilterRequest
|
||||
{
|
||||
PaginationState = new() { PageNumber = 1, PageSize = 100 },
|
||||
Filter = new()
|
||||
};
|
||||
|
||||
var result = await PackageClient.GetAllPackageByFilterAsync(request);
|
||||
_packages = result.Models?.OrderBy(p => p.SortOrder).ToList()
|
||||
?? new List<GetAllPackageByFilterResponseModel>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_packages = new List<GetAllPackageByFilterResponseModel>();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnValueChanged(long? value)
|
||||
{
|
||||
SelectedPackageId = value;
|
||||
await SelectedPackageIdChanged.InvokeAsync(value);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,11 @@
|
||||
Required="true"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudTextField @bind-Value="Model.Slug"
|
||||
Label="نشانی URL (slug)"
|
||||
Variant="Variant.Outlined"
|
||||
HelperText="اختیاری — خالی بگذارید تا خودکار از عنوان ساخته شود. فقط حروف کوچک انگلیسی، عدد و خط تیره." />
|
||||
|
||||
<MudTextField @bind-Value="Model.Summary"
|
||||
Label="خلاصه"
|
||||
Variant="Variant.Outlined"
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
<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">
|
||||
|
||||
|
||||
@@ -1,17 +1,34 @@
|
||||
@page "/club"
|
||||
@page "/club/members"
|
||||
@page "/club/purchases"
|
||||
@page "/club/customer-packages"
|
||||
@page "/club/cycles"
|
||||
@page "/club/history"
|
||||
@page "/club/statistics"
|
||||
@attribute [Authorize]
|
||||
@using BackOffice.Pages.Club.Reports
|
||||
|
||||
<div>
|
||||
<MudText Typo="Typo.h5" Class="mb-3">باشگاه مشتریان</MudText>
|
||||
|
||||
<MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true"
|
||||
PanelClass="pt-4" @bind-ActivePanelIndex="_activeTab">
|
||||
<MudTabPanel Text="اعضای باشگاه" Icon="@Icons.Material.Filled.Groups">
|
||||
<MudTabPanel Text="اعضا" Icon="@Icons.Material.Filled.Groups">
|
||||
<ClubMembers />
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="آمار باشگاه" Icon="@Icons.Material.Filled.PieChart">
|
||||
<MudTabPanel Text="گزارش خرید پکیج" Icon="@Icons.Material.Filled.ShoppingBag">
|
||||
<ClubPackagePurchasesReport />
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="گزارش مشتریان پکیج" Icon="@Icons.Material.Filled.PeopleAlt">
|
||||
<CustomerPackagePurchasesReport />
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="گزارش دورهها" Icon="@Icons.Material.Filled.Loop">
|
||||
<ClubCyclesReport />
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="گزارش تاریخچه" Icon="@Icons.Material.Filled.History">
|
||||
<ClubHistoriesReport />
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="آمار" Icon="@Icons.Material.Filled.PieChart">
|
||||
<Statistics />
|
||||
</MudTabPanel>
|
||||
</MudTabs>
|
||||
@@ -25,7 +42,11 @@
|
||||
var uri = Navigation.ToBaseRelativePath(Navigation.Uri).ToLowerInvariant().TrimEnd('/');
|
||||
_activeTab = uri switch
|
||||
{
|
||||
"club/statistics" => 1,
|
||||
"club/purchases" => 1,
|
||||
"club/customer-packages" => 2,
|
||||
"club/cycles" => 3,
|
||||
"club/history" => 4,
|
||||
"club/statistics" => 5,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,11 +3,17 @@
|
||||
|
||||
@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"
|
||||
@@ -16,7 +22,8 @@
|
||||
<ToolBarContent>
|
||||
<MudText Typo="Typo.h6">لیست اعضا</MudText>
|
||||
<MudSpacer />
|
||||
<MudStack Row="true" Spacing="2">
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
||||
<UserAutoComplete Label="جستجوی کاربر" @bind-SelectedUserId="_filterUserId" />
|
||||
<MudSwitch T="bool" @bind-Value="_filterIsActive"
|
||||
Color="Color.Success"
|
||||
Label="فقط فعالها" />
|
||||
|
||||
@@ -16,6 +16,7 @@ public partial class ClubMembers
|
||||
|
||||
private MudDataGrid<ClubMembershipModel> _gridData;
|
||||
private bool _filterIsActive = true;
|
||||
private long? _filterUserId;
|
||||
|
||||
private async Task<GridData<ClubMembershipModel>> ServerReload(GridState<ClubMembershipModel> state)
|
||||
{
|
||||
@@ -30,6 +31,9 @@ public partial class ClubMembers
|
||||
// Always use the filter value since it's now a bool (not nullable)
|
||||
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,32 +1,39 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.ClubMembership
|
||||
@using CMSMicroservice.Protobuf.Protos.Package
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudForm @ref="_form" @bind-IsValid="_formValid">
|
||||
<MudStack Spacing="3">
|
||||
<MudNumericField @bind-Value="_model.UserId"
|
||||
Label="شناسه کاربر"
|
||||
Required="true"
|
||||
RequiredError="شناسه کاربر الزامی است"
|
||||
Variant="Variant.Outlined" />
|
||||
<UserAutoComplete Label="انتخاب کاربر" @bind-SelectedUserId="_model.UserId" />
|
||||
|
||||
<MudNumericField @bind-Value="_model.PackageId"
|
||||
Label="شناسه پکیج"
|
||||
Required="true"
|
||||
RequiredError="شناسه پکیج الزامی است"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudNumericField @bind-Value="_model.DurationMonths"
|
||||
Label="مدت زمان (ماه)"
|
||||
Required="true"
|
||||
RequiredError="مدت زمان الزامی است"
|
||||
Min="1"
|
||||
Max="12"
|
||||
Variant="Variant.Outlined" />
|
||||
@if (_isLoadingPackages)
|
||||
{
|
||||
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Height="56px" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudSelect T="long" @bind-Value="_model.PackageId"
|
||||
Label="انتخاب پکیج"
|
||||
Required="true"
|
||||
RequiredError="انتخاب پکیج الزامی است"
|
||||
Variant="Variant.Outlined"
|
||||
AnchorOrigin="Origin.BottomCenter">
|
||||
@foreach (var pkg in _packages)
|
||||
{
|
||||
<MudSelectItem T="long" Value="pkg.Id">
|
||||
@pkg.Title — @($"{pkg.Price:N0}") ریال
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
|
||||
<MudAlert Severity="Severity.Info" Dense="true">
|
||||
<MudText Typo="Typo.body2">
|
||||
با تایید، عضویت باشگاه برای کاربر فعال میشود.
|
||||
<br/>
|
||||
<small>عضویت باشگاه بدون محدودیت زمانی است.</small>
|
||||
</MudText>
|
||||
</MudAlert>
|
||||
</MudStack>
|
||||
@@ -37,7 +44,7 @@
|
||||
<MudButton Color="Color.Success"
|
||||
Variant="Variant.Filled"
|
||||
OnClick="Submit"
|
||||
Disabled="@(!_formValid || _isSubmitting)">
|
||||
Disabled="@(!_formValid || _isSubmitting || _isLoadingPackages)">
|
||||
@if (_isSubmitting)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" />
|
||||
@@ -54,12 +61,49 @@
|
||||
@code {
|
||||
[CascadingParameter] IMudDialogInstance MudDialog { get; set; }
|
||||
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubContract { get; set; }
|
||||
[Inject] public PackageContract.PackageContractClient PackageClient { get; set; }
|
||||
|
||||
private MudForm _form;
|
||||
private bool _formValid;
|
||||
private bool _isSubmitting;
|
||||
private bool _isLoadingPackages = true;
|
||||
private List<GetAllPackageByFilterResponseModel> _packages = new();
|
||||
private ActivateModel _model = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadPackages();
|
||||
}
|
||||
|
||||
private async Task LoadPackages()
|
||||
{
|
||||
_isLoadingPackages = true;
|
||||
try
|
||||
{
|
||||
var request = new GetAllPackageByFilterRequest
|
||||
{
|
||||
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
|
||||
{
|
||||
PageNumber = 1,
|
||||
PageSize = 50
|
||||
}
|
||||
};
|
||||
var result = await PackageClient.GetAllPackageByFilterAsync(request);
|
||||
if (result?.Models != null)
|
||||
{
|
||||
_packages = result.Models.ToList();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری پکیجها: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoadingPackages = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Cancel() => MudDialog.Cancel();
|
||||
|
||||
private async Task Submit()
|
||||
@@ -67,19 +111,31 @@
|
||||
await _form.Validate();
|
||||
if (!_formValid) return;
|
||||
|
||||
if (!_model.UserId.HasValue || _model.UserId.Value <= 0)
|
||||
{
|
||||
Snackbar.Add("لطفاً کاربر را انتخاب کنید.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_model.PackageId <= 0)
|
||||
{
|
||||
Snackbar.Add("لطفاً پکیج را انتخاب کنید.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
_isSubmitting = true;
|
||||
try
|
||||
{
|
||||
var request = new ActivateClubMembershipRequest
|
||||
{
|
||||
UserId = _model.UserId,
|
||||
UserId = _model.UserId.Value,
|
||||
PackageId = _model.PackageId,
|
||||
DurationMonths = _model.DurationMonths
|
||||
ForceActivation = true
|
||||
};
|
||||
|
||||
var response = await ClubContract.ActivateClubMembershipAsync(request);
|
||||
|
||||
Snackbar.Add($"عضویت با موفقیت فعال شد.", Severity.Success);
|
||||
Snackbar.Add("عضویت با موفقیت فعال شد.", Severity.Success);
|
||||
MudDialog.Close(DialogResult.Ok(true));
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -94,8 +150,7 @@
|
||||
|
||||
private class ActivateModel
|
||||
{
|
||||
public long UserId { get; set; }
|
||||
public long? UserId { get; set; }
|
||||
public long PackageId { get; set; }
|
||||
public int DurationMonths { get; set; } = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,23 +33,11 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>پکیج:</strong></td>
|
||||
<td>@Member.PackageName</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>کد فعالسازی:</strong></td>
|
||||
<td dir="ltr">@Member.ActivationCode</td>
|
||||
<td>@(string.IsNullOrEmpty(Member.PackageName) ? "—" : Member.PackageName)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>تاریخ فعالسازی:</strong></td>
|
||||
<td>@Member.ActivatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>تاریخ انقضا:</strong></td>
|
||||
<td>
|
||||
<MudText Color="@(Member.IsExpired ? Color.Error : Color.Success)">
|
||||
@Member.ExpiresAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()
|
||||
</MudText>
|
||||
</td>
|
||||
<td>@(Member.ActivatedAt != null ? Member.ActivatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() : "—")</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>وضعیت:</strong></td>
|
||||
@@ -61,60 +49,17 @@
|
||||
</MudChip>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>منقضی شده:</strong></td>
|
||||
<td>
|
||||
<MudChip T="string"
|
||||
Color="@(Member.IsExpired ? Color.Warning : Color.Default)"
|
||||
Size="Size.Small">
|
||||
@(Member.IsExpired ? "بله" : "خیر")
|
||||
</MudChip>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
</MudPaper>
|
||||
|
||||
<MudPaper Class="pa-3" Elevation="0">
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary">تاریخچه</MudText>
|
||||
<MudDivider Class="my-2" />
|
||||
<MudSimpleTable Dense="true">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>تاریخ ایجاد:</strong></td>
|
||||
<td>@Member.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
|
||||
@if (_historyLoading)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" Color="Color.Primary" Class="my-2" />
|
||||
}
|
||||
else if (_history.Any())
|
||||
{
|
||||
<MudTable T="ClubMembershipHistoryModel" Items="_history" Dense="true" Hover="true" Class="mt-2">
|
||||
<HeaderContent>
|
||||
<MudTh>عملیات</MudTh>
|
||||
<MudTh>تاریخ</MudTh>
|
||||
<MudTh>دلیل</MudTh>
|
||||
<MudTh>توسط</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="عملیات">
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Info">@context.Action</MudChip>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="تاریخ">@context.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</MudTd>
|
||||
<MudTd DataLabel="دلیل">@(string.IsNullOrEmpty(context.Reason) ? "-" : context.Reason)</MudTd>
|
||||
<MudTd DataLabel="توسط">@(string.IsNullOrEmpty(context.PerformedBy) ? "-" : context.PerformedBy)</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="mt-2">تاریخچهای یافت نشد.</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text">
|
||||
تاریخچه تغییرات عضویت در تب «گزارش تاریخچه» باشگاه قابل مشاهده است.
|
||||
</MudAlert>
|
||||
</MudStack>
|
||||
}
|
||||
</DialogContent>
|
||||
@@ -126,39 +71,5 @@
|
||||
@code {
|
||||
[CascadingParameter] IMudDialogInstance MudDialog { get; set; }
|
||||
[Parameter] public ClubMembershipModel Member { get; set; }
|
||||
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubClient { get; set; }
|
||||
|
||||
private List<ClubMembershipHistoryModel> _history = new();
|
||||
private bool _historyLoading;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadHistory();
|
||||
}
|
||||
|
||||
private async Task LoadHistory()
|
||||
{
|
||||
_historyLoading = true;
|
||||
try
|
||||
{
|
||||
var request = new GetClubMembershipHistoryRequest
|
||||
{
|
||||
UserId = Member.UserId,
|
||||
PageIndex = 0,
|
||||
PageSize = 50
|
||||
};
|
||||
var response = await ClubClient.GetClubMembershipHistoryAsync(request);
|
||||
_history = response.Models?.ToList() ?? new();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_history = new();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_historyLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Close() => MudDialog.Close();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
@using BackOffice.Common.BaseComponents
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
@using CMSMicroservice.Protobuf.Protos.ClubMembershipCycle
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
|
||||
@inject ClubMembershipCycleContract.ClubMembershipCycleContractClient CycleClient
|
||||
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3">
|
||||
گزارش دورههای عضویت باشگاه — هر خرید پکیج یک دور جدید ایجاد میکند.
|
||||
</MudAlert>
|
||||
|
||||
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
|
||||
<Filters>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_userId" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect T="bool?" Clearable="true" Label="دوره جاری" Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" @bind-Value="_isCurrent">
|
||||
<MudSelectItem T="bool?" Value="@((bool?)true)">فقط دوره جاری</MudSelectItem>
|
||||
<MudSelectItem T="bool?" Value="@((bool?)false)">دورههای قبلی</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect T="int?" Clearable="true" Label="وضعیت جادویی" Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" @bind-Value="_magicState">
|
||||
<MudSelectItem T="int?" Value="0">عادی</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="1">در حالت جادویی</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="2">جادویی تکمیلشده</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه خرید پکیج" />
|
||||
</MudItem>
|
||||
</Filters>
|
||||
<Content>
|
||||
<MudGrid Class="mb-3" Spacing="2">
|
||||
<MudItem xs="4">
|
||||
<MudPaper Class="pa-3" Outlined="true">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">کل دورهها</MudText>
|
||||
<MudText Typo="Typo.h6">@_totalCount.ToString("N0")</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="4">
|
||||
<MudPaper Class="pa-3" Outlined="true">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">دوره جاری</MudText>
|
||||
<MudText Typo="Typo.h6">@_currentCount.ToString("N0")</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="4">
|
||||
<MudPaper Class="pa-3" Outlined="true">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">جادویی فعال</MudText>
|
||||
<MudText Typo="Typo.h6">@_magicActive.ToString("N0")</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudDataGrid @ref="_grid" T="ClubMembershipCycleModel" ServerData="LoadData" ReadOnly="true"
|
||||
Hover="true" Dense="true" Height="calc(100vh - 420px)">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
<PropertyColumn Property="x => x.UserName" Title="کاربر" />
|
||||
<PropertyColumn Property="x => x.CycleNumber" Title="شماره دور" />
|
||||
<PropertyColumn Property="x => x.PackageName" Title="پکیج" />
|
||||
<TemplateColumn Title="مبلغ (ریال)">
|
||||
<CellTemplate>@context.Item.PackageAmount.ToString("N0")</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="روش خرید">
|
||||
<CellTemplate>@ReportLabels.GetPurchaseMethodLabel(context.Item.PurchaseMethod)</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="تاریخ خرید">
|
||||
<CellTemplate>@context.Item.PackagePurchasedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="وضعیت جادویی">
|
||||
<CellTemplate>
|
||||
@ReportLabels.GetMagicStateLabel(null,
|
||||
context.Item.MagicStartedAt?.ToDateTime(),
|
||||
context.Item.MagicCompletedAt?.ToDateTime())
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="دوره جاری">
|
||||
<CellTemplate>
|
||||
<MudChip T="string" Size="Size.Small" Color="@(context.Item.IsCurrentCycle ? Color.Success : Color.Default)">
|
||||
@(context.Item.IsCurrentCycle ? "بله" : "خیر")
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="ClubMembershipCycleModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</Content>
|
||||
</BasePageComponent>
|
||||
|
||||
@code {
|
||||
private BasePageComponent? _basePage;
|
||||
private MudDataGrid<ClubMembershipCycleModel>? _grid;
|
||||
private long? _userId;
|
||||
private bool? _isCurrent;
|
||||
private int? _magicState;
|
||||
private DateTime? _fromDate, _toDate;
|
||||
private long _totalCount, _currentCount, _magicActive;
|
||||
|
||||
private GetAllClubMembershipCycleByFilterFilter BuildFilter() => new()
|
||||
{
|
||||
UserId = _userId > 0 ? _userId : null,
|
||||
IsCurrentCycle = _isCurrent,
|
||||
MagicState = _magicState,
|
||||
PackagePurchasedFrom = _fromDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_fromDate.Value, DateTimeKind.Utc)) : null,
|
||||
PackagePurchasedTo = _toDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_toDate.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Utc)) : null
|
||||
};
|
||||
|
||||
private async Task<GridData<ClubMembershipCycleModel>> LoadData(GridState<ClubMembershipCycleModel> state)
|
||||
{
|
||||
try
|
||||
{
|
||||
var filter = BuildFilter();
|
||||
var summary = await CycleClient.GetClubMembershipCycleSummaryAsync(new GetClubMembershipCycleSummaryRequest { Filter = filter });
|
||||
_totalCount = summary.TotalCount;
|
||||
_currentCount = summary.CurrentCycleCount;
|
||||
_magicActive = summary.MagicActiveCount;
|
||||
|
||||
var result = await CycleClient.GetAllClubMembershipCycleByFilterAsync(new GetAllClubMembershipCycleByFilterRequest
|
||||
{
|
||||
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize },
|
||||
Filter = filter
|
||||
});
|
||||
return new GridData<ClubMembershipCycleModel>
|
||||
{
|
||||
Items = result?.Models?.ToList() ?? new(),
|
||||
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
return new GridData<ClubMembershipCycleModel>();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnFilterSubmit() { if (_grid != null) await _grid.ReloadServerData(); }
|
||||
private async Task OnFilterCleared()
|
||||
{
|
||||
_userId = null; _isCurrent = null; _magicState = null; _fromDate = _toDate = null;
|
||||
if (_grid != null) await _grid.ReloadServerData();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
@using BackOffice.Common.BaseComponents
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
@using CMSMicroservice.Protobuf.Protos.ClubMembership
|
||||
@using CMSMicroservice.Protobuf.Protos.ClubMembershipCycleHistory
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
|
||||
@inject ClubMembershipContract.ClubMembershipContractClient ClubClient
|
||||
@inject ClubMembershipCycleHistoryContract.ClubMembershipCycleHistoryContractClient CycleHistoryClient
|
||||
|
||||
<MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true" PanelClass="pt-4">
|
||||
<MudTabPanel Text="تغییرات عضویت" Icon="@Icons.Material.Filled.History">
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3">
|
||||
تاریخچه فعال/غیرفعال شدن عضویت باشگاه.
|
||||
</MudAlert>
|
||||
<MudStack Row="true" Spacing="2" Class="mb-3">
|
||||
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_memUserId" />
|
||||
<MudSelect T="int?" Clearable="true" Label="عملیات" Variant="Variant.Outlined" Margin="Margin.Dense"
|
||||
@bind-Value="_memAction" Style="max-width:200px;">
|
||||
<MudSelectItem T="int?" Value="0">فعالسازی</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="1">غیرفعالسازی</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="2">ویرایش</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="3">اصلاح دستی</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ReloadMembership">جستجو</MudButton>
|
||||
</MudStack>
|
||||
<MudDataGrid @ref="_memGrid" T="ClubMembershipHistoryModel" ServerData="LoadMembershipHistory" ReadOnly="true"
|
||||
Hover="true" Dense="true" Height="calc(100vh - 400px)">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
<PropertyColumn Property="x => x.UserName" Title="کاربر" />
|
||||
<TemplateColumn Title="وضعیت">
|
||||
<CellTemplate>@ReportLabels.FormatActiveChange(context.Item.OldIsActive, context.Item.NewIsActive)</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="مشارکت اولیه">
|
||||
<CellTemplate>
|
||||
@(context.Item.OldInitialContribution != null ? context.Item.OldInitialContribution.Value.ToString("N0") : "—")
|
||||
→ @(context.Item.NewInitialContribution != null ? context.Item.NewInitialContribution.Value.ToString("N0") : "—")
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="عملیات">
|
||||
<CellTemplate>@ReportLabels.GetClubMembershipActionLabel(context.Item.Action)</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<PropertyColumn Property="x => x.PerformedBy" Title="انجامدهنده" />
|
||||
<PropertyColumn Property="x => x.Reason" Title="دلیل" />
|
||||
<TemplateColumn Title="تاریخ">
|
||||
<CellTemplate>@context.Item.CreatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="ClubMembershipHistoryModel" PageSizeOptions="@(new int[] { 20, 50, 100 })" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudTabPanel>
|
||||
|
||||
<MudTabPanel Text="تغییرات دوره" Icon="@Icons.Material.Filled.Timeline">
|
||||
<MudAlert Severity="Severity.Warning" Variant="Variant.Text" Dense="true" Class="mb-3">
|
||||
ردیابی تغییرات دوره از زمان deploy فعال شد. رکوردهای قدیمیتر ممکن است خالی باشند.
|
||||
</MudAlert>
|
||||
<MudStack Row="true" Spacing="2" Class="mb-3">
|
||||
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_cycleUserId" />
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ReloadCycle">جستجو</MudButton>
|
||||
</MudStack>
|
||||
<MudDataGrid @ref="_cycleGrid" T="ClubMembershipCycleHistoryModel" ServerData="LoadCycleHistory" ReadOnly="true"
|
||||
Hover="true" Dense="true" Height="calc(100vh - 400px)">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
<PropertyColumn Property="x => x.UserName" Title="کاربر" />
|
||||
<PropertyColumn Property="x => x.CycleNumber" Title="دور" />
|
||||
<TemplateColumn Title="دوره جاری">
|
||||
<CellTemplate>@ReportLabels.FormatBoolChange(context.Item.OldIsCurrentCycle, context.Item.NewIsCurrentCycle)</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="عملیات">
|
||||
<CellTemplate>@ReportLabels.GetClubCycleActionLabel(context.Item.Action)</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<PropertyColumn Property="x => x.PerformedBy" Title="انجامدهنده" />
|
||||
<PropertyColumn Property="x => x.Reason" Title="دلیل" />
|
||||
<TemplateColumn Title="تاریخ">
|
||||
<CellTemplate>@context.Item.CreatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="ClubMembershipCycleHistoryModel" PageSizeOptions="@(new int[] { 20, 50, 100 })" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudTabPanel>
|
||||
</MudTabs>
|
||||
|
||||
@code {
|
||||
private MudDataGrid<ClubMembershipHistoryModel>? _memGrid;
|
||||
private MudDataGrid<ClubMembershipCycleHistoryModel>? _cycleGrid;
|
||||
private long? _memUserId, _cycleUserId;
|
||||
private int? _memAction;
|
||||
|
||||
private async Task<GridData<ClubMembershipHistoryModel>> LoadMembershipHistory(GridState<ClubMembershipHistoryModel> state)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await ClubClient.GetClubMembershipHistoryAsync(new GetClubMembershipHistoryRequest
|
||||
{
|
||||
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize },
|
||||
Filter = new GetClubMembershipHistoryFilter
|
||||
{
|
||||
UserId = _memUserId > 0 ? _memUserId : null,
|
||||
Action = _memAction
|
||||
}
|
||||
});
|
||||
return new GridData<ClubMembershipHistoryModel>
|
||||
{
|
||||
Items = result?.Models?.ToList() ?? new(),
|
||||
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
return new GridData<ClubMembershipHistoryModel>();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<GridData<ClubMembershipCycleHistoryModel>> LoadCycleHistory(GridState<ClubMembershipCycleHistoryModel> state)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await CycleHistoryClient.GetAllClubMembershipCycleHistoryByFilterAsync(new GetAllClubMembershipCycleHistoryByFilterRequest
|
||||
{
|
||||
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize },
|
||||
Filter = new GetAllClubMembershipCycleHistoryByFilterFilter
|
||||
{
|
||||
UserId = _cycleUserId > 0 ? _cycleUserId : null
|
||||
}
|
||||
});
|
||||
return new GridData<ClubMembershipCycleHistoryModel>
|
||||
{
|
||||
Items = result?.Models?.ToList() ?? new(),
|
||||
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
return new GridData<ClubMembershipCycleHistoryModel>();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReloadMembership() { if (_memGrid != null) await _memGrid.ReloadServerData(); }
|
||||
private async Task ReloadCycle() { if (_cycleGrid != null) await _cycleGrid.ReloadServerData(); }
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
@using BackOffice.Common.BaseComponents
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
@using CMSMicroservice.Protobuf.Protos.UserPackagePurchase
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
|
||||
@inject UserPackagePurchaseContract.UserPackagePurchaseContractClient PurchaseClient
|
||||
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3">
|
||||
گزارش read-only خریدهای پکیج — فقط مشاهده و فیلتر.
|
||||
</MudAlert>
|
||||
|
||||
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
|
||||
<Filters>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_userId" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect T="int?" Clearable="true" Label="روش خرید" Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" @bind-Value="_purchaseMethod">
|
||||
<MudSelectItem T="int?" Value="1">دایا</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="3">دستی</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="2">درگاه</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه خرید" />
|
||||
</MudItem>
|
||||
</Filters>
|
||||
<Content>
|
||||
<MudGrid Class="mb-3" Spacing="2">
|
||||
<MudItem xs="6" sm="4">
|
||||
<MudPaper Class="pa-3" Outlined="true">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">تعداد</MudText>
|
||||
<MudText Typo="Typo.h6">@_totalCount.ToString("N0")</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="6" sm="4">
|
||||
<MudPaper Class="pa-3" Outlined="true">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">جمع مبلغ (ریال)</MudText>
|
||||
<MudText Typo="Typo.h6">@_totalAmount.ToString("N0")</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudDataGrid @ref="_grid" T="UserPackagePurchaseModel" ServerData="LoadData" ReadOnly="true"
|
||||
Hover="true" Dense="true" Height="calc(100vh - 420px)">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
<TemplateColumn Title="کاربر">
|
||||
<CellTemplate>
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.body2">@context.Item.UserName</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">@context.Item.UserMobile</MudText>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<PropertyColumn Property="x => x.PackageName" Title="پکیج" />
|
||||
<TemplateColumn Title="روش خرید">
|
||||
<CellTemplate>@ReportLabels.GetPurchaseMethodLabel(context.Item.PurchaseMethod)</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="مبلغ (ریال)">
|
||||
<CellTemplate>@context.Item.Amount.ToString("N0")</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="تاریخ خرید">
|
||||
<CellTemplate>@context.Item.PurchasedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<PropertyColumn Property="x => x.OrderId" Title="OrderId" />
|
||||
<PropertyColumn Property="x => x.TransactionId" Title="TransactionId" />
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="UserPackagePurchaseModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</Content>
|
||||
</BasePageComponent>
|
||||
|
||||
@code {
|
||||
private BasePageComponent? _basePage;
|
||||
private MudDataGrid<UserPackagePurchaseModel>? _grid;
|
||||
private long? _userId;
|
||||
private int? _purchaseMethod;
|
||||
private DateTime? _fromDate, _toDate;
|
||||
private long _totalCount, _totalAmount;
|
||||
|
||||
private GetAllUserPackagePurchaseByFilterFilter BuildFilter() => new()
|
||||
{
|
||||
UserId = _userId > 0 ? _userId : null,
|
||||
PurchaseMethod = _purchaseMethod,
|
||||
PurchasedFrom = _fromDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_fromDate.Value, DateTimeKind.Utc)) : null,
|
||||
PurchasedTo = _toDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_toDate.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Utc)) : null
|
||||
};
|
||||
|
||||
private async Task<GridData<UserPackagePurchaseModel>> LoadData(GridState<UserPackagePurchaseModel> state)
|
||||
{
|
||||
try
|
||||
{
|
||||
var filter = BuildFilter();
|
||||
var summary = await PurchaseClient.GetUserPackagePurchaseSummaryAsync(new GetUserPackagePurchaseSummaryRequest { Filter = filter });
|
||||
_totalCount = summary.TotalCount;
|
||||
_totalAmount = summary.TotalAmount;
|
||||
|
||||
var result = await PurchaseClient.GetAllUserPackagePurchaseByFilterAsync(new GetAllUserPackagePurchaseByFilterRequest
|
||||
{
|
||||
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize },
|
||||
Filter = filter
|
||||
});
|
||||
return new GridData<UserPackagePurchaseModel>
|
||||
{
|
||||
Items = result?.Models?.ToList() ?? new(),
|
||||
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
return new GridData<UserPackagePurchaseModel>();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnFilterSubmit() { if (_grid != null) await _grid.ReloadServerData(); }
|
||||
private async Task OnFilterCleared()
|
||||
{
|
||||
_userId = null; _purchaseMethod = null; _fromDate = _toDate = null;
|
||||
if (_grid != null) await _grid.ReloadServerData();
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.UserPackagePurchase
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
@using DateTimeConverterCL
|
||||
|
||||
@inject UserPackagePurchaseContract.UserPackagePurchaseContractClient PurchaseClient
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6">
|
||||
جزئیات خرید پکیج — @UserName
|
||||
</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
@if (_loading)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" Color="Color.Primary" Class="my-4" />
|
||||
}
|
||||
else if (_items.Count == 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Dense="true">خریدی برای این کاربر یافت نشد.</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTable Items="_items" Dense="true" Hover="true" Breakpoint="Breakpoint.Sm">
|
||||
<HeaderContent>
|
||||
<MudTh>شناسه</MudTh>
|
||||
<MudTh>پکیج</MudTh>
|
||||
<MudTh>روش</MudTh>
|
||||
<MudTh>مبلغ (ریال)</MudTh>
|
||||
<MudTh>تاریخ خرید</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="شناسه">@context.Id</MudTd>
|
||||
<MudTd DataLabel="پکیج">@context.PackageName</MudTd>
|
||||
<MudTd DataLabel="روش">
|
||||
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined"
|
||||
Color="@GetMethodColor(context.PurchaseMethod)">
|
||||
@ReportLabels.GetPurchaseMethodLabel(context.PurchaseMethod)
|
||||
</MudChip>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="مبلغ">@context.Amount.ToString("N0")</MudTd>
|
||||
<MudTd DataLabel="تاریخ">
|
||||
@(context.PurchasedAt != null
|
||||
? context.PurchasedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()
|
||||
: "-")
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Close" Variant="Variant.Text">بستن</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
[Parameter] public long UserId { get; set; }
|
||||
[Parameter] public string UserName { get; set; } = string.Empty;
|
||||
|
||||
private bool _loading = true;
|
||||
private List<UserPackagePurchaseModel> _items = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await PurchaseClient.GetAllUserPackagePurchaseByFilterAsync(
|
||||
new GetAllUserPackagePurchaseByFilterRequest
|
||||
{
|
||||
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
|
||||
{
|
||||
PageNumber = 1,
|
||||
PageSize = 500
|
||||
},
|
||||
Filter = new GetAllUserPackagePurchaseByFilterFilter
|
||||
{
|
||||
UserId = UserId
|
||||
}
|
||||
});
|
||||
|
||||
_items = result?.Models?
|
||||
.OrderBy(x => x.PurchasedAt?.ToDateTime() ?? DateTime.MinValue)
|
||||
.ToList() ?? new();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری جزئیات: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Color GetMethodColor(int method) => method switch
|
||||
{
|
||||
1 => Color.Info, // دایا
|
||||
2 => Color.Primary, // درگاه
|
||||
3 => Color.Secondary, // دستی
|
||||
_ => Color.Default
|
||||
};
|
||||
|
||||
private void Close() => MudDialog.Close();
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
@using System.Text
|
||||
@using BackOffice.Common.BaseComponents
|
||||
@using BackOffice.Common.Utilities
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
@using BackOffice.Pages.Club.Reports.Components
|
||||
@using CMSMicroservice.Protobuf.Protos.UserPackagePurchase
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
@using DateTimeConverterCL
|
||||
@using Microsoft.JSInterop
|
||||
|
||||
@inject UserPackagePurchaseContract.UserPackagePurchaseContractClient PurchaseClient
|
||||
@inject IDialogService DialogService
|
||||
@inject IJSRuntime JsRuntime
|
||||
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3">
|
||||
گزارش تجمیعی مشتریان — اولین/آخرین پکیج، تفکیک دایا / دستی / درگاه. خروجی Excel مطابق فیلتر فعال است.
|
||||
</MudAlert>
|
||||
|
||||
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
|
||||
<Filters>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_userId" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect T="int?" Clearable="true" Label="روش خرید" Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" @bind-Value="_purchaseMethod">
|
||||
<MudSelectItem T="int?" Value="1">دایا</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="3">دستی</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="2">درگاه</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه خرید" />
|
||||
</MudItem>
|
||||
</Filters>
|
||||
<Content>
|
||||
<MudStack Row="true" Justify="Justify.FlexEnd" Class="mb-3">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Success"
|
||||
StartIcon="@Icons.Material.Filled.FileDownload"
|
||||
OnClick="ExportToExcel" Disabled="_exporting">
|
||||
خروجی Excel
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
<MudDataGrid @ref="_grid" T="CustomerPackagePurchaseRollupModel" ServerData="LoadData" ReadOnly="true"
|
||||
Hover="true" Dense="true" Height="calc(100vh - 420px)">
|
||||
<Columns>
|
||||
<TemplateColumn Title="مشتری">
|
||||
<CellTemplate>
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.body2">@context.Item.UserName</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">@context.Item.UserMobile</MudText>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="اولین پکیج">
|
||||
<CellTemplate>
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.body2">@context.Item.FirstPackageName</MudText>
|
||||
<MudText Typo="Typo.caption">@context.Item.FirstAmount.ToString("N0") ریال</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||
@(context.Item.FirstPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-")
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="آخرین پکیج">
|
||||
<CellTemplate>
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.body2">@context.Item.LastPackageName</MudText>
|
||||
<MudText Typo="Typo.caption">@context.Item.LastAmount.ToString("N0") ریال</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||
@(context.Item.LastPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-")
|
||||
</MudText>
|
||||
<MudChip T="string" Size="Size.Small" Class="mt-1" Variant="Variant.Filled"
|
||||
Color="@GetMethodColor(context.Item.LastPurchaseMethod)">
|
||||
@ReportLabels.GetPurchaseMethodLabel(context.Item.LastPurchaseMethod)
|
||||
</MudChip>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="تعداد / جمع">
|
||||
<CellTemplate>
|
||||
<MudStack Spacing="0">
|
||||
<MudText>@context.Item.PurchaseCount خرید</MudText>
|
||||
<MudText Typo="Typo.caption">@context.Item.TotalAmount.ToString("N0") ریال</MudText>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="دایا">
|
||||
<CellTemplate>
|
||||
<MudText Typo="Typo.caption">@context.Item.DayaCount عدد</MudText>
|
||||
<MudText Typo="Typo.caption">@context.Item.DayaAmount.ToString("N0")</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="دستی">
|
||||
<CellTemplate>
|
||||
<MudText Typo="Typo.caption">@context.Item.ManualCount عدد</MudText>
|
||||
<MudText Typo="Typo.caption">@context.Item.ManualAmount.ToString("N0")</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="درگاه">
|
||||
<CellTemplate>
|
||||
<MudText Typo="Typo.caption">@context.Item.GatewayCount عدد</MudText>
|
||||
<MudText Typo="Typo.caption">@context.Item.GatewayAmount.ToString("N0")</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="عملیات">
|
||||
<CellTemplate>
|
||||
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
|
||||
OnClick="() => OpenDetails(context.Item)">
|
||||
جزئیات
|
||||
</MudButton>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="CustomerPackagePurchaseRollupModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</Content>
|
||||
</BasePageComponent>
|
||||
|
||||
@code {
|
||||
private BasePageComponent? _basePage;
|
||||
private MudDataGrid<CustomerPackagePurchaseRollupModel>? _grid;
|
||||
private long? _userId;
|
||||
private int? _purchaseMethod;
|
||||
private DateTime? _fromDate, _toDate;
|
||||
private bool _exporting;
|
||||
|
||||
private GetCustomerPackagePurchaseRollupFilter BuildFilter()
|
||||
{
|
||||
var filter = new GetCustomerPackagePurchaseRollupFilter();
|
||||
if (_userId is > 0)
|
||||
filter.UserId = _userId;
|
||||
if (_purchaseMethod.HasValue)
|
||||
filter.PurchaseMethod = _purchaseMethod.Value;
|
||||
if (_fromDate.HasValue)
|
||||
filter.PurchasedFrom = Timestamp.FromDateTime(DateTime.SpecifyKind(_fromDate.Value.Date, DateTimeKind.Utc));
|
||||
if (_toDate.HasValue)
|
||||
filter.PurchasedTo = Timestamp.FromDateTime(DateTime.SpecifyKind(_toDate.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Utc));
|
||||
return filter;
|
||||
}
|
||||
|
||||
private async Task<GridData<CustomerPackagePurchaseRollupModel>> LoadData(GridState<CustomerPackagePurchaseRollupModel> state)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await PurchaseClient.GetCustomerPackagePurchaseRollupAsync(
|
||||
new GetCustomerPackagePurchaseRollupRequest
|
||||
{
|
||||
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
|
||||
{
|
||||
PageNumber = state.Page + 1,
|
||||
PageSize = state.PageSize
|
||||
},
|
||||
Filter = BuildFilter()
|
||||
});
|
||||
|
||||
return new GridData<CustomerPackagePurchaseRollupModel>
|
||||
{
|
||||
Items = result?.Models?.ToList() ?? new(),
|
||||
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
return new GridData<CustomerPackagePurchaseRollupModel>();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnFilterSubmit()
|
||||
{
|
||||
if (_grid != null) await _grid.ReloadServerData();
|
||||
}
|
||||
|
||||
private async Task OnFilterCleared()
|
||||
{
|
||||
_userId = null;
|
||||
_purchaseMethod = null;
|
||||
_fromDate = _toDate = null;
|
||||
if (_grid != null) await _grid.ReloadServerData();
|
||||
}
|
||||
|
||||
private async Task OpenDetails(CustomerPackagePurchaseRollupModel row)
|
||||
{
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ nameof(CustomerPackagePurchaseDetailsDialog.UserId), row.UserId },
|
||||
{ nameof(CustomerPackagePurchaseDetailsDialog.UserName), row.UserName }
|
||||
};
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true, CloseButton = true };
|
||||
await DialogService.ShowAsync<CustomerPackagePurchaseDetailsDialog>("جزئیات خرید پکیج", parameters, options);
|
||||
}
|
||||
|
||||
private async Task ExportToExcel()
|
||||
{
|
||||
_exporting = true;
|
||||
try
|
||||
{
|
||||
// PageSize=0 → همهٔ ردیفهای مطابق فیلتر، مستقل از صفحهبندی گرید
|
||||
var result = await PurchaseClient.GetCustomerPackagePurchaseRollupAsync(
|
||||
new GetCustomerPackagePurchaseRollupRequest
|
||||
{
|
||||
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
|
||||
{
|
||||
PageNumber = 1,
|
||||
PageSize = 0
|
||||
},
|
||||
Filter = BuildFilter()
|
||||
});
|
||||
|
||||
var all = result?.Models?.ToList() ?? new();
|
||||
if (all.Count == 0)
|
||||
{
|
||||
Snackbar.Add("دادهای برای خروجی با فیلتر فعلی وجود ندارد.", Severity.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
var headers = new[]
|
||||
{
|
||||
"شناسه کاربر", "نام مشتری", "موبایل",
|
||||
"اولین پکیج", "مبلغ اولین", "تاریخ اولین",
|
||||
"آخرین پکیج", "مبلغ آخرین", "تاریخ آخرین", "روش آخرین",
|
||||
"تعداد خرید", "جمع مبلغ",
|
||||
"تعداد دایا", "مبلغ دایا",
|
||||
"تعداد دستی", "مبلغ دستی",
|
||||
"تعداد درگاه", "مبلغ درگاه"
|
||||
};
|
||||
|
||||
var rows = all.Select(o => new object?[]
|
||||
{
|
||||
o.UserId,
|
||||
o.UserName,
|
||||
o.UserMobile,
|
||||
o.FirstPackageName,
|
||||
o.FirstAmount,
|
||||
o.FirstPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-",
|
||||
o.LastPackageName,
|
||||
o.LastAmount,
|
||||
o.LastPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-",
|
||||
ReportLabels.GetPurchaseMethodLabel(o.LastPurchaseMethod),
|
||||
o.PurchaseCount,
|
||||
o.TotalAmount,
|
||||
o.DayaCount,
|
||||
o.DayaAmount,
|
||||
o.ManualCount,
|
||||
o.ManualAmount,
|
||||
o.GatewayCount,
|
||||
o.GatewayAmount
|
||||
});
|
||||
|
||||
var filename = $"customer-packages-{DateTime.Now:yyyyMMddHHmmss}.xls";
|
||||
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
|
||||
CsvExportHelper.ToExcelXmlBase64("مشتریان پکیج", headers, rows));
|
||||
Snackbar.Add($"خروجی Excel آماده شد ({all.Count} مشتری).", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_exporting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Color GetMethodColor(int method) => method switch
|
||||
{
|
||||
1 => Color.Info,
|
||||
2 => Color.Primary,
|
||||
3 => Color.Secondary,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
@@ -41,11 +41,11 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
|
||||
@@ -5,116 +5,59 @@
|
||||
@using MudBlazor
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
|
||||
<div>
|
||||
<MudText Typo="Typo.h4" Class="mb-4">داشبورد کمیسیون</MudText>
|
||||
<MudStack Spacing="4">
|
||||
|
||||
@if (_isLoading)
|
||||
{
|
||||
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Pool Summary Section -->
|
||||
<MudGrid>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudCard Elevation="2">
|
||||
<MudCardContent>
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary">استخر هفتگی</MudText>
|
||||
<MudText Typo="Typo.h4">@(_poolData?.TotalPoolAmount.ToString("N0") ?? "0") ریال</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">@_currentWeekDisplayText</MudText>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudCard Elevation="2">
|
||||
<MudCardContent>
|
||||
<MudText Typo="Typo.h6" Color="Color.Success">تعداد تعادلها</MudText>
|
||||
<MudText Typo="Typo.h4">@(_poolData?.TotalBalances.ToString("N0") ?? "0")</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">مجموع تعادل های فعال</MudText>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudCard Elevation="2">
|
||||
<MudCardContent>
|
||||
<MudText Typo="Typo.h6" Color="Color.Warning">ارزش هر تعادل</MudText>
|
||||
<MudText Typo="Typo.h4">@(_poolData?.ValuePerBalance.ToString("N0") ?? "0") ریال</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">قیمت واحد تعادل</MudText>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudCard Elevation="2">
|
||||
<MudCardContent>
|
||||
<MudText Typo="Typo.h6" Color="@(_poolData?.IsCalculated == true ? Color.Success : Color.Error)">
|
||||
وضعیت محاسبه
|
||||
</MudText>
|
||||
<MudText Typo="Typo.h4">
|
||||
@(_poolData?.IsCalculated == true ? "محاسبه شده ✓" : "محاسبه نشده ✗")
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">
|
||||
@if (_poolData?.CalculatedAt != null)
|
||||
{
|
||||
var calculatedDate = _poolData.CalculatedAt.ToDateTime().ToLocalTime();
|
||||
var persianDate = PersianDateTime.ConvertToPersianDateTime(calculatedDate);
|
||||
@($"در تاریخ {persianDate}")
|
||||
}
|
||||
else
|
||||
{
|
||||
@("در انتظار محاسبه")
|
||||
}
|
||||
</MudText>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<!-- Week Selector -->
|
||||
<MudPaper Class="pa-4 mt-4" Elevation="2">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
|
||||
<MudText Typo="Typo.h6">انتخاب هفته:</MudText>
|
||||
@* ── عنوان صفحه ── *@
|
||||
<div>
|
||||
<MudText Typo="Typo.h5" Class="mb-1">داشبورد کمیسیون</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">
|
||||
مبلغ پورسانت هر هفته بین اعضاء تقسیم میشود. هر پکیج استخر مستقل دارد.
|
||||
</MudText>
|
||||
</div>
|
||||
|
||||
@* ── فیلتر (اول از همه) ── *@
|
||||
<MudPaper Elevation="1" Class="pa-4">
|
||||
<MudGrid AlignItems="AlignItems.End" Spacing="3">
|
||||
<MudItem xs="12" sm="5">
|
||||
@if (_allWeeks.Any())
|
||||
{
|
||||
<MudSelect T="long"
|
||||
Value="_currentWeekDefinitionId"
|
||||
Label="شماره هفته"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
AnchorOrigin="Origin.BottomCenter"
|
||||
Style="min-width: 400px;"
|
||||
ValueChanged="OnWeekChanged">
|
||||
Value="_currentWeekDefinitionId"
|
||||
Label="هفته"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
AnchorOrigin="Origin.BottomLeft"
|
||||
TransformOrigin="Origin.TopLeft"
|
||||
AdornmentIcon="@Icons.Material.Filled.CalendarToday"
|
||||
Adornment="Adornment.Start"
|
||||
ValueChanged="OnWeekChanged">
|
||||
|
||||
@if (_availableWeeks?.CurrentWeek != null)
|
||||
{
|
||||
<MudSelectItem T="long" Value="@_availableWeeks.CurrentWeek.WeekDefinitionId">
|
||||
<MudText>
|
||||
<MudIcon Icon="@Icons.Material.Filled.Today" Size="Size.Small" Class="ml-2" />
|
||||
@_availableWeeks.CurrentWeek.DisplayText
|
||||
</MudText>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Today" Size="Size.Small" Color="Color.Primary" />
|
||||
<span>@_availableWeeks.CurrentWeek.DisplayText</span>
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Primary" Variant="Variant.Text">جاری</MudChip>
|
||||
</MudStack>
|
||||
</MudSelectItem>
|
||||
<MudDivider />
|
||||
}
|
||||
|
||||
@if (_availableWeeks?.CalculatedWeeks?.Any() == true)
|
||||
{
|
||||
<MudListSubheader>هفتههای محاسبه شده</MudListSubheader>
|
||||
<MudListSubheader>تسویهشده</MudListSubheader>
|
||||
@foreach (var week in _availableWeeks.CalculatedWeeks)
|
||||
{
|
||||
<MudSelectItem T="long" Value="@week.WeekDefinitionId">
|
||||
<MudText>
|
||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" Class="ml-2" />
|
||||
@week.DisplayText
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Size="Size.Small" Color="Color.Success" />
|
||||
<span>@week.DisplayText</span>
|
||||
@if (week.TotalPoolAmount > 0)
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Success" Class="mr-2">
|
||||
@week.TotalPoolAmount.ToString("N0") ریال
|
||||
</MudChip>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">(@week.TotalPoolAmount.ToString("N0") T)</MudText>
|
||||
}
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudSelectItem>
|
||||
}
|
||||
<MudDivider />
|
||||
@@ -122,14 +65,14 @@
|
||||
|
||||
@if (_availableWeeks?.PendingWeeks?.Any() == true)
|
||||
{
|
||||
<MudListSubheader>هفتههای محاسبه نشده</MudListSubheader>
|
||||
<MudListSubheader>در انتظار تسویه</MudListSubheader>
|
||||
@foreach (var week in _availableWeeks.PendingWeeks)
|
||||
{
|
||||
<MudSelectItem T="long" Value="@week.WeekDefinitionId">
|
||||
<MudText>
|
||||
<MudIcon Icon="@Icons.Material.Filled.AccessTime" Color="Color.Warning" Size="Size.Small" Class="ml-2" />
|
||||
@week.DisplayText
|
||||
</MudText>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudIcon Icon="@Icons.Material.Filled.AccessTime" Size="Size.Small" Color="Color.Warning" />
|
||||
<span>@week.DisplayText</span>
|
||||
</MudStack>
|
||||
</MudSelectItem>
|
||||
}
|
||||
<MudDivider />
|
||||
@@ -141,10 +84,10 @@
|
||||
@foreach (var week in _availableWeeks.FutureWeeks)
|
||||
{
|
||||
<MudSelectItem T="long" Value="@week.WeekDefinitionId">
|
||||
<MudText>
|
||||
<MudIcon Icon="@Icons.Material.Filled.CalendarMonth" Color="Color.Info" Size="Size.Small" Class="ml-2" />
|
||||
@week.DisplayText
|
||||
</MudText>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudIcon Icon="@Icons.Material.Filled.CalendarMonth" Size="Size.Small" Color="Color.Tertiary" />
|
||||
<span>@week.DisplayText</span>
|
||||
</MudStack>
|
||||
</MudSelectItem>
|
||||
}
|
||||
}
|
||||
@@ -154,97 +97,222 @@
|
||||
{
|
||||
<MudText Color="Color.Warning">لیست هفتهها بارگذاری نشده</MudText>
|
||||
}
|
||||
</MudItem>
|
||||
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
OnClick="LoadPoolData"
|
||||
StartIcon="@Icons.Material.Filled.Refresh">
|
||||
بارگذاری
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
<MudItem xs="12" sm="5">
|
||||
<PackageSelect Label="پکیج"
|
||||
SelectedPackageId="_selectedPackageId"
|
||||
SelectedPackageIdChanged="OnPackageChanged" />
|
||||
</MudItem>
|
||||
|
||||
<!-- Pool Details -->
|
||||
@if (_poolData != null)
|
||||
{
|
||||
<MudPaper Class="pa-4 mt-4" Elevation="2">
|
||||
<MudText Typo="Typo.h6" Class="mb-3">جزئیات استخر</MudText>
|
||||
<MudSimpleTable Hover="true" Dense="true">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>شناسه استخر:</strong></td>
|
||||
<td>@_poolData.Id</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>شماره هفته:</strong></td>
|
||||
<td>@_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>
|
||||
}
|
||||
<MudItem xs="12" sm="2">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
FullWidth="true"
|
||||
OnClick="LoadPoolData"
|
||||
StartIcon="@Icons.Material.Filled.Search"
|
||||
Disabled="_isLoading">
|
||||
نمایش
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<MudPaper Class="pa-4 mt-4" Elevation="2">
|
||||
<MudText Typo="Typo.h6" Class="mb-3">عملیات سریع</MudText>
|
||||
<MudStack Row="true" Spacing="2">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Payments"
|
||||
Href="/commission/payouts">
|
||||
مشاهده پرداختها
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Secondary"
|
||||
StartIcon="@Icons.Material.Filled.AccountBalance"
|
||||
Href="/commission/withdrawals">
|
||||
درخواستهای برداشت
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Info"
|
||||
StartIcon="@Icons.Material.Filled.Calculate"
|
||||
Disabled="@(_poolData?.IsCalculated == true)"
|
||||
OnClick="TriggerCalculation">
|
||||
اجرای محاسبه دستی
|
||||
</MudButton>
|
||||
@* ── لودینگ ── *@
|
||||
@if (_isLoading)
|
||||
{
|
||||
<MudPaper Elevation="0" Class="pa-6 d-flex justify-center align-center">
|
||||
<MudStack AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Medium" />
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">در حال بارگذاری اطلاعات استخر…</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
</div>
|
||||
else
|
||||
{
|
||||
@* ── نوار وضعیت هفته / پکیج ── *@
|
||||
<MudPaper Elevation="0" Class="pa-3 mud-background-gray rounded">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Wrap="Wrap.Wrap">
|
||||
<MudIcon Icon="@Icons.Material.Filled.DateRange" Color="Color.Default" Size="Size.Small" />
|
||||
<MudText Typo="Typo.body1">@_currentWeekDisplayText</MudText>
|
||||
|
||||
@if (!string.IsNullOrEmpty(_poolData?.PackageTitle))
|
||||
{
|
||||
<MudDivider Vertical="true" FlexItem="true" />
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Primary" Icon="@Icons.Material.Filled.WorkspacePremium">
|
||||
@_poolData.PackageTitle
|
||||
</MudChip>
|
||||
}
|
||||
|
||||
<MudSpacer />
|
||||
|
||||
@if (_poolData != null && _poolData.Id > 0)
|
||||
{
|
||||
@if (_poolData.IsCalculated)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Success" Size="Size.Small"
|
||||
Icon="@Icons.Material.Filled.CheckCircle" Variant="Variant.Filled">
|
||||
تسویه انجام شده
|
||||
</MudChip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChip T="string" Color="Color.Warning" Size="Size.Small"
|
||||
Icon="@Icons.Material.Filled.Pending" Variant="Variant.Filled">
|
||||
در انتظار تسویه
|
||||
</MudChip>
|
||||
}
|
||||
}
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
@* ── ۳ کارت اصلی ── *@
|
||||
<MudGrid Spacing="3">
|
||||
@* کارت ۱: کل مبلغ استخر *@
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudPaper Elevation="2" Class="pa-4" Style="border-top: 4px solid var(--mud-palette-primary);">
|
||||
<MudStack Spacing="1">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Color="Color.Primary" Size="Size.Small" />
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">کل مبلغ استخر پورسانت</MudText>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.h5" Style="font-weight:700; direction:ltr; text-align:right;">
|
||||
@((_poolData?.TotalPoolAmount ?? 0).ToString("N0"))
|
||||
</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">تومان — جمع کارمزد فعالسازیهای این هفته</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
@* کارت ۲: تعداد واحدهای پورسانت *@
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudPaper Elevation="2" Class="pa-4" Style="border-top: 4px solid var(--mud-palette-success);">
|
||||
<MudStack Spacing="1">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudIcon Icon="@Icons.Material.Filled.People" Color="Color.Success" Size="Size.Small" />
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">واحدهای پورسانت (بالانس)</MudText>
|
||||
<MudTooltip Text="تعداد واحدهایی که پورسانت به آنها تعلق میگیرد. هر عضو بر اساس شبکهاش چند واحد دریافت میکند.">
|
||||
<MudIcon Icon="@Icons.Material.Filled.HelpOutline" Size="Size.Small" Color="Color.Tertiary" />
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.h5" Style="font-weight:700; direction:ltr; text-align:right;">
|
||||
@((_poolData?.TotalBalances ?? 0).ToString("N0"))
|
||||
</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">واحد — مجموع همه اعضاء فعال</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
@* کارت ۳: سهم هر واحد *@
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudPaper Elevation="2" Class="pa-4" Style="border-top: 4px solid var(--mud-palette-warning);">
|
||||
<MudStack Spacing="1">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudIcon Icon="@Icons.Material.Filled.LocalAtm" Color="Color.Warning" Size="Size.Small" />
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">سهم پورسانت هر واحد</MudText>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.h5" Style="font-weight:700; direction:ltr; text-align:right;">
|
||||
@((_poolData?.ValuePerBalance ?? 0).ToString("N0"))
|
||||
</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||
تومان — @((_poolData?.IsCalculated == true) ? "قطعی شده" : "قطعی نشده")
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
@* ── اعتبارسنجی مبلغ استخر ── *@
|
||||
@if (_poolData != null && _poolData.Id > 0)
|
||||
{
|
||||
@if (!_poolData.ValidationIsValid)
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Variant="Variant.Filled" Icon="@Icons.Material.Filled.ReportProblem" NoIcon="false">
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2">مبلغ استخر با تعداد فعالسازیهای دیتابیس مطابقت ندارد</MudText>
|
||||
<MudGrid Spacing="2">
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudText Typo="Typo.caption">مبلغ ثبتشده</MudText>
|
||||
<MudText Typo="Typo.body2"><strong>@_poolData.TotalPoolAmount.ToString("N0") تومان</strong></MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudText Typo="Typo.caption">مبلغ انتظاری (@_poolData.ValidationActivationCount فعالسازی)</MudText>
|
||||
<MudText Typo="Typo.body2"><strong>@_poolData.ValidationExpectedAmount.ToString("N0") تومان</strong></MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudText Typo="Typo.caption">اختلاف</MudText>
|
||||
<MudText Typo="Typo.body2" Style="color:var(--mud-palette-warning-lighten);">
|
||||
<strong>@_poolData.ValidationDiscrepancy.ToString("N0") تومان</strong>
|
||||
</MudText>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudAlert>
|
||||
}
|
||||
else if (_poolData.ValidationActivationCount > 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Success" Variant="Variant.Text" Dense="true" Icon="@Icons.Material.Filled.VerifiedUser">
|
||||
مبلغ استخر صحیح است — با @_poolData.ValidationActivationCount فعالسازی در این هفته مطابقت دارد.
|
||||
</MudAlert>
|
||||
}
|
||||
}
|
||||
|
||||
@* ── جزئیات تکمیلی (بدون تکرار اطلاعات کارتها) ── *@
|
||||
@if (_poolData != null && _poolData.Id > 0)
|
||||
{
|
||||
<MudExpansionPanels Elevation="1">
|
||||
<MudExpansionPanel Text="جزئیات بیشتر" Icon="@Icons.Material.Filled.ExpandMore">
|
||||
<MudSimpleTable Dense="true" Hover="true" Style="max-width:540px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:200px;"><MudText Typo="Typo.body2" Color="Color.Secondary">شناسه استخر</MudText></td>
|
||||
<td><MudText Typo="Typo.body2">#@_poolData.Id</MudText></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><MudText Typo="Typo.body2" Color="Color.Secondary">تاریخ ایجاد استخر</MudText></td>
|
||||
<td><MudText Typo="Typo.body2">@_poolData.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</MudText></td>
|
||||
</tr>
|
||||
@if (_poolData.CalculatedAt != null)
|
||||
{
|
||||
<tr>
|
||||
<td><MudText Typo="Typo.body2" Color="Color.Secondary">تاریخ تسویه</MudText></td>
|
||||
<td><MudText Typo="Typo.body2">@_poolData.CalculatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</MudText></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
</MudExpansionPanel>
|
||||
</MudExpansionPanels>
|
||||
}
|
||||
|
||||
@* ── دکمههای عملیاتی ── *@
|
||||
<MudPaper Elevation="0" Class="pa-3 mud-background-gray rounded">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Wrap="Wrap.Wrap">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Payments"
|
||||
Href="/commission/payouts"
|
||||
Size="Size.Medium">
|
||||
پرداختهای این هفته
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Default"
|
||||
StartIcon="@Icons.Material.Filled.AccountBalance"
|
||||
Href="/commission/withdrawals"
|
||||
Size="Size.Medium">
|
||||
درخواستهای برداشت
|
||||
</MudButton>
|
||||
<MudSpacer />
|
||||
@if (_poolData?.IsCalculated != true)
|
||||
{
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Warning"
|
||||
StartIcon="@Icons.Material.Filled.Calculate"
|
||||
OnClick="TriggerCalculation"
|
||||
Size="Size.Medium">
|
||||
اجرای تسویه دستی
|
||||
</MudButton>
|
||||
}
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
</MudStack>
|
||||
|
||||
@@ -22,6 +22,9 @@ 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();
|
||||
@@ -91,6 +94,12 @@ public partial class Dashboard
|
||||
await LoadPoolData();
|
||||
}
|
||||
|
||||
private async Task OnPackageChanged(long? packageId)
|
||||
{
|
||||
_selectedPackageId = packageId;
|
||||
await LoadPoolData();
|
||||
}
|
||||
|
||||
private async Task LoadPoolData()
|
||||
{
|
||||
if (_currentWeekDefinitionId <= 0)
|
||||
@@ -106,7 +115,8 @@ public partial class Dashboard
|
||||
|
||||
var request = new GetWeeklyCommissionPoolRequest
|
||||
{
|
||||
WeekDefinitionId = _currentWeekDefinitionId
|
||||
WeekDefinitionId = _currentWeekDefinitionId,
|
||||
PackageId = _selectedPackageId
|
||||
};
|
||||
|
||||
_poolData = await CommissionContract.GetWeeklyCommissionPoolAsync(request);
|
||||
@@ -126,24 +136,41 @@ public partial class Dashboard
|
||||
|
||||
private async Task TriggerCalculation()
|
||||
{
|
||||
var packageInfo = _selectedPackageId.HasValue
|
||||
? $" (فقط پکیج #{_selectedPackageId})"
|
||||
: " (همه پکیجها)";
|
||||
|
||||
var confirmed = await DialogService.ShowMessageBox(
|
||||
"تایید",
|
||||
$"آیا از اجرای محاسبه دستی برای هفته {_currentWeekDisplayName} مطمئن هستید؟",
|
||||
$"آیا از اجرای محاسبه دستی برای هفته {_currentWeekDisplayName}{packageInfo} مطمئن هستید؟",
|
||||
yesText: "بله", cancelText: "خیر");
|
||||
|
||||
if (confirmed == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await CommissionContract.TriggerWeeklyCalculationAsync(new TriggerWeeklyCalculationRequest()
|
||||
var req = new TriggerWeeklyCalculationRequest()
|
||||
{
|
||||
WeekDefinitionId = _currentWeekDefinitionId,
|
||||
ForceRecalculate = true,
|
||||
SkipBalances = false,
|
||||
SkipPayouts = false,
|
||||
SkipPool = false,
|
||||
});
|
||||
Snackbar.Add("محاسبه با موفقیت آغاز شد. این عملیات ممکن است چند دقیقه طول بکشد.", Severity.Info);
|
||||
};
|
||||
|
||||
req.PackageId = _selectedPackageId;
|
||||
|
||||
var response = await CommissionContract.TriggerWeeklyCalculationAsync(req);
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
Snackbar.Add("محاسبه با موفقیت انجام شد.", Severity.Success);
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add($"خطا در محاسبه: {response.Message}", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reload data after a delay
|
||||
await Task.Delay(2000);
|
||||
|
||||
@@ -19,4 +19,5 @@ public class WithdrawalRequestModel
|
||||
public string? BankTrackingCode { get; set; }
|
||||
public string? PaymentFailureReason { get; set; }
|
||||
public string? IbanNumber { get; set; }
|
||||
public string? PackageName { get; set; }
|
||||
}
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
<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"
|
||||
@@ -36,12 +41,29 @@
|
||||
<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>
|
||||
@@ -64,6 +86,14 @@
|
||||
</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,6 +17,8 @@ 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)
|
||||
{
|
||||
@@ -44,6 +46,11 @@ 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())
|
||||
@@ -119,7 +126,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)
|
||||
@@ -141,6 +148,50 @@ public partial class UserPayouts
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BulkCreditWallets()
|
||||
{
|
||||
if (!_filterWeekDefinitionId.HasValue)
|
||||
{
|
||||
Snackbar.Add("لطفاً ابتدا یک هفته انتخاب کنید", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var confirmed = await DialogService.ShowMessageBox(
|
||||
"واریز دستهجمعی به کیف پول",
|
||||
$"آیا از واریز کمیسیون همه پرداختهای «در انتظار» هفته انتخابشده به کیف پول کاربران مطمئن هستید؟ این عملیات برگشتپذیر نیست.",
|
||||
yesText: "بله، واریز کن", cancelText: "لغو");
|
||||
|
||||
if (confirmed != true)
|
||||
return;
|
||||
|
||||
_isCreditingWallets = true;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
var result = await CommissionContract.BulkCreditPayoutsAsync(new BulkCreditPayoutsRequest
|
||||
{
|
||||
WeekDefinitionId = _filterWeekDefinitionId.Value
|
||||
});
|
||||
|
||||
if (result.CreditedCount == 0)
|
||||
Snackbar.Add("هیچ پرداخت در انتظاری برای این هفته یافت نشد", Severity.Info);
|
||||
else
|
||||
Snackbar.Add(result.Message, Severity.Success);
|
||||
|
||||
await ApplyFilter();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در واریز: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isCreditingWallets = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RejectWithdrawal(UserCommissionPayoutModel payout)
|
||||
{
|
||||
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,7 +87,8 @@ public partial class WithdrawalRequests
|
||||
BankReferenceId = grpcModel.BankReferenceId,
|
||||
BankTrackingCode = grpcModel.BankTrackingCode,
|
||||
PaymentFailureReason = grpcModel.PaymentFailureReason,
|
||||
IbanNumber = grpcModel.IbanNumber
|
||||
IbanNumber = grpcModel.IbanNumber,
|
||||
PackageName = grpcModel.PackageName
|
||||
};
|
||||
}
|
||||
|
||||
@@ -163,7 +164,7 @@ public partial class WithdrawalRequests
|
||||
{
|
||||
var confirmed = await DialogService.ShowMessageBox(
|
||||
"تایید درخواست",
|
||||
$"آیا از تایید درخواست برداشت {request.Amount:N0} ریال برای {request.UserName} مطمئن هستید؟",
|
||||
$"آیا از تایید درخواست برداشت {request.Amount:N0} تومان برای {request.UserName} مطمئن هستید؟",
|
||||
yesText: "تایید", cancelText: "لغو");
|
||||
|
||||
if (confirmed == true)
|
||||
@@ -222,7 +223,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)
|
||||
@@ -269,7 +270,7 @@ public partial class WithdrawalRequests
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("شناسه,کاربر,نام کاربر,مبلغ,وضعیت,روش,شماره شبا,تاریخ درخواست,تاریخ پردازش,شماره مرجع بانکی");
|
||||
sb.AppendLine("شناسه,کاربر,نام کاربر,پکیج,مبلغ,وضعیت,روش,شماره شبا,تاریخ درخواست,تاریخ پردازش,شماره مرجع بانکی");
|
||||
|
||||
foreach (var r in result.Models)
|
||||
{
|
||||
@@ -278,6 +279,7 @@ public partial class WithdrawalRequests
|
||||
mapped.Id,
|
||||
mapped.UserId,
|
||||
EscapeCsv(mapped.UserName),
|
||||
EscapeCsv(mapped.PackageName ?? "-"),
|
||||
mapped.Amount,
|
||||
EscapeCsv(GetStatusText(mapped.Status)),
|
||||
EscapeCsv(GetMethodText(mapped.Method)),
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
@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();
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
@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();
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
@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();
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
@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>
|
||||
@@ -0,0 +1,112 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
@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>
|
||||
@@ -0,0 +1,133 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
@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>
|
||||
@@ -0,0 +1,151 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
@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>
|
||||
@@ -0,0 +1,119 @@
|
||||
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,6 +26,7 @@
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Processing">در حال آمادهسازی</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.ReadyForOfficePickup">آماده تحویل در دفتر</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
@@ -55,6 +56,15 @@
|
||||
</MudItem>
|
||||
}
|
||||
|
||||
@if (Model.Status == OrderStatus.ReadyForOfficePickup)
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<MudAlert Severity="Severity.Info">
|
||||
<MudText>سفارش برای تحویل حضوری در دفتر آماده است.</MudText>
|
||||
</MudAlert>
|
||||
</MudItem>
|
||||
}
|
||||
|
||||
@if (Model.Status == OrderStatus.Shipped)
|
||||
{
|
||||
<MudItem xs="12">
|
||||
@@ -126,8 +136,9 @@
|
||||
OrderStatus.Processing => Color.Primary,
|
||||
OrderStatus.Shipped => Color.Secondary,
|
||||
OrderStatus.Delivered => Color.Success,
|
||||
OrderStatus.Cancelled => Color.Error,
|
||||
OrderStatus.Returned => Color.Dark,
|
||||
OrderStatus.ReadyForOfficePickup => Color.Primary,
|
||||
OrderStatus.Cancelled => Color.Dark,
|
||||
OrderStatus.Returned => Color.Error,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
@@ -141,6 +152,7 @@
|
||||
OrderStatus.Processing => "در حال آمادهسازی",
|
||||
OrderStatus.Shipped => "ارسال شده",
|
||||
OrderStatus.Delivered => "تحویل داده شده",
|
||||
OrderStatus.ReadyForOfficePickup => "آماده تحویل در دفتر",
|
||||
OrderStatus.Cancelled => "لغو شده",
|
||||
OrderStatus.Returned => "مرجوع شده",
|
||||
_ => "نامشخص"
|
||||
|
||||
@@ -1,36 +1,72 @@
|
||||
@using BackOffice.Services.DiscountOrder
|
||||
@using BackOffice.Common.BaseComponents
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ShoppingCart" Class="ml-2" />
|
||||
جزئیات سفارش #@Order.OrderNumber
|
||||
</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudGrid>
|
||||
<!-- اطلاعات کاربر -->
|
||||
<MudItem xs="12">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" GutterBottom="true">اطلاعات سفارش</MudText>
|
||||
<MudGrid>
|
||||
<MudItem xs="6">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">شماره سفارش:</MudText>
|
||||
<MudText Typo="Typo.body1"><strong>@Order.OrderNumber</strong></MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="6">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">شناسه کاربر:</MudText>
|
||||
<MudText Typo="Typo.body1">@Order.UserId</MudText>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudStack Spacing="2">
|
||||
<MudText Typo="Typo.h6">جزئیات سفارش شماره @(string.IsNullOrWhiteSpace(Order.OrderNumber) ? Order.Id.ToString() : Order.OrderNumber)</MudText>
|
||||
<MudText Typo="Typo.body2">
|
||||
کاربر:
|
||||
@if (!string.IsNullOrWhiteSpace(Order.UserFullName))
|
||||
{
|
||||
@Order.UserFullName
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>#@Order.UserId</text>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(DisplayMobile))
|
||||
{
|
||||
<text> (@DisplayMobile)</text>
|
||||
}
|
||||
</MudText>
|
||||
|
||||
<!-- Timeline وضعیت -->
|
||||
<MudItem xs="12">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.subtitle2" GutterBottom="true">Timeline وضعیت</MudText>
|
||||
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center" Justify="Justify.Center">
|
||||
<MudPaper Class="pa-3">
|
||||
<MudStack Spacing="2">
|
||||
<MudText Typo="Typo.subtitle2">خلاصه سفارش</MudText>
|
||||
<MudDivider />
|
||||
<MudText Typo="Typo.body2">مبلغ کل: @Order.TotalPrice.ToString("N0") تومان</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Success">
|
||||
از کیف پول اعتباری: @Order.DiscountBalanceUsed.ToString("N0") تومان
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body2">
|
||||
مبلغ پرداختی از درگاه: @Order.GatewayAmount.ToString("N0") تومان
|
||||
</MudText>
|
||||
@if (!string.IsNullOrEmpty(Order.TransactionId))
|
||||
{
|
||||
<MudText Typo="Typo.body2">شناسه پرداخت: @Order.TransactionId</MudText>
|
||||
}
|
||||
@if (Order.Created.HasValue)
|
||||
{
|
||||
<MudText Typo="Typo.body2">
|
||||
تاریخ ثبت: @Order.Created.Value.MiladiToJalaliWithTime()
|
||||
</MudText>
|
||||
}
|
||||
<MudText Typo="Typo.body2">
|
||||
وضعیت پرداخت:
|
||||
<MudChip T="string"
|
||||
Color="@GetPaymentStatusColor(Order.PaymentStatusValue)"
|
||||
Size="Size.Small"
|
||||
Icon="@GetPaymentStatusIcon(Order.PaymentStatusValue)">
|
||||
@GetPaymentStatusText(Order.PaymentStatusValue)
|
||||
</MudChip>
|
||||
</MudText>
|
||||
|
||||
@if (Order.VatAmount > 0)
|
||||
{
|
||||
<MudDivider Class="my-2" />
|
||||
<MudText Typo="Typo.subtitle2">جزئیات مالیات بر ارزش افزوده</MudText>
|
||||
<MudText Typo="Typo.body2">
|
||||
مالیات: @Order.VatAmount.ToString("N0") تومان
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body2">
|
||||
مبلغ نهایی (شامل مالیات): @Order.TotalPrice.ToString("N0") تومان
|
||||
</MudText>
|
||||
}
|
||||
|
||||
<MudDivider Class="my-2" />
|
||||
<MudText Typo="Typo.subtitle2">Timeline وضعیت</MudText>
|
||||
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center">
|
||||
@foreach (var step in _timelineSteps)
|
||||
{
|
||||
<MudStack Spacing="0" AlignItems="AlignItems.Center">
|
||||
@@ -41,218 +77,155 @@
|
||||
</MudStack>
|
||||
}
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<!-- وضعیت سفارش -->
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" GutterBottom="true">وضعیت سفارش</MudText>
|
||||
<MudChip T="string" Color="@GetStatusColor(Order.Status)" Size="Size.Large">
|
||||
@GetStatusText(Order.Status)
|
||||
</MudChip>
|
||||
@if (Order.Created.HasValue)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mt-2">
|
||||
تاریخ ثبت: @Order.Created.Value.MiladiToJalaliWithTime()
|
||||
</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<!-- وضعیت پرداخت -->
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" GutterBottom="true">وضعیت پرداخت</MudText>
|
||||
@if (Order.PaymentCompleted)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Success" Size="Size.Large" Icon="@Icons.Material.Filled.CheckCircle">
|
||||
پرداخت شده
|
||||
</MudChip>
|
||||
@if (!string.IsNullOrEmpty(Order.TransactionId))
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mt-2">
|
||||
کد تراکنش: @Order.TransactionId
|
||||
</MudText>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChip T="string" Color="Color.Warning" Size="Size.Large" Icon="@Icons.Material.Filled.Schedule">
|
||||
در انتظار پرداخت
|
||||
</MudChip>
|
||||
}
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<!-- آدرس ارسال -->
|
||||
@if (Order.Address != null)
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" GutterBottom="true">
|
||||
<MudIcon Icon="@Icons.Material.Filled.LocationOn" Size="Size.Small" />
|
||||
آدرس ارسال
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body1"><strong>@Order.Address.Title</strong></MudText>
|
||||
<MudText Typo="Typo.body2">@Order.Address.Address</MudText>
|
||||
<MudText Typo="Typo.body2">کد پستی: @Order.Address.PostalCode</MudText>
|
||||
@if (!string.IsNullOrEmpty(Order.Address.Phone))
|
||||
{
|
||||
<MudText Typo="Typo.body2">تلفن: @Order.Address.Phone</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
}
|
||||
|
||||
<!-- آیتمهای سفارش -->
|
||||
<MudItem xs="12">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" GutterBottom="true">آیتمهای سفارش</MudText>
|
||||
<MudTable Items="@Order.Items" Hover="true" Dense="true" Elevation="0">
|
||||
<HeaderContent>
|
||||
<MudTh>محصول</MudTh>
|
||||
<MudTh>تعداد</MudTh>
|
||||
<MudTh>قیمت واحد</MudTh>
|
||||
<MudTh>تخفیف</MudTh>
|
||||
<MudTh>قیمت نهایی</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
@if (!string.IsNullOrWhiteSpace(context.ThumbnailPath))
|
||||
{
|
||||
<MudImage Src="@context.ThumbnailPath" Width="40" Height="40"
|
||||
ObjectFit="ObjectFit.Cover" Class="rounded" />
|
||||
}
|
||||
<MudText>@context.ProductTitle</MudText>
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
<MudTd>@context.Count</MudTd>
|
||||
<MudTd>@context.UnitPrice.ToString("N0") ریال</MudTd>
|
||||
<MudTd>
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Success">@context.MaxDiscountPercent%</MudChip>
|
||||
</MudTd>
|
||||
<MudTd><strong>@context.FinalPrice.ToString("N0") ریال</strong></MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<!-- خلاصه مالی -->
|
||||
<MudItem xs="12">
|
||||
<MudPaper Class="pa-4" Elevation="2">
|
||||
<MudText Typo="Typo.h6" GutterBottom="true">خلاصه مالی</MudText>
|
||||
<MudGrid>
|
||||
<MudItem xs="6">
|
||||
<MudText Color="Color.Secondary">مبلغ کل:</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="6" Class="text-left">
|
||||
<MudText>@Order.TotalPrice.ToString("N0") ریال</MudText>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="6">
|
||||
<MudText Color="Color.Success">از کیف پول تخفیف:</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="6" Class="text-left">
|
||||
<MudText Color="Color.Success">@Order.DiscountBalanceUsed.ToString("N0") ریال</MudText>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12"><MudDivider /></MudItem>
|
||||
|
||||
<MudItem xs="6">
|
||||
<MudText Typo="Typo.h6">مبلغ پرداختی از درگاه:</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="6" Class="text-left">
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary">
|
||||
<strong>@Order.GatewayAmount.ToString("N0") ریال</strong>
|
||||
</MudText>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<!-- ویرایش وضعیت ارسال -->
|
||||
<MudItem xs="12">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.subtitle2" GutterBottom="true">مدیریت وضعیت ارسال</MudText>
|
||||
<MudDivider Class="mb-3" />
|
||||
<MudGrid>
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudSelect T="OrderStatus"
|
||||
Label="وضعیت ارسال"
|
||||
Dense="true"
|
||||
Variant="Variant.Outlined"
|
||||
@bind-Value="_newStatus">
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Pending">در انتظار پرداخت</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Paid">پرداخت شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Processing">در حال آمادهسازی</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudTextField T="string"
|
||||
Label="کد رهگیری"
|
||||
Variant="Variant.Outlined"
|
||||
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>
|
||||
<MudDivider Class="my-2" />
|
||||
<MudText Typo="Typo.body2">
|
||||
وضعیت ارسال:
|
||||
<MudSelect T="OrderStatus"
|
||||
Label="وضعیت ارسال"
|
||||
Dense="true"
|
||||
Variant="Variant.Outlined"
|
||||
@bind-Value="_newStatus">
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Pending">در انتظار پرداخت</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Paid">پرداخت شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Processing">در حال آمادهسازی</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.ReadyForOfficePickup">آماده تحویل در دفتر</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudText>
|
||||
<MudTextField T="string"
|
||||
Label="کد رهگیری"
|
||||
Variant="Variant.Outlined"
|
||||
Dense="true"
|
||||
@bind-Value="_trackingCode" />
|
||||
<MudTextField T="string"
|
||||
Label="یادداشت ادمین"
|
||||
Variant="Variant.Outlined"
|
||||
Dense="true"
|
||||
Lines="3"
|
||||
HelperText="دلیل تغییر وضعیت یا توضیحات اضافی"
|
||||
@bind-Value="_adminNotes" />
|
||||
|
||||
@if (_newStatus == OrderStatus.Cancelled || _newStatus == OrderStatus.Returned)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Class="mt-2">
|
||||
<MudAlert Severity="Severity.Warning" Dense="true">
|
||||
توجه: در صورت لغو یا مرجوعی سفارش، موجودی محصولات به انبار بازگردانده میشود.
|
||||
</MudAlert>
|
||||
}
|
||||
@if (_newStatus == OrderStatus.ReadyForOfficePickup)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Dense="true">
|
||||
سفارش برای تحویل حضوری در دفتر آماده است.
|
||||
</MudAlert>
|
||||
}
|
||||
@if (_newStatus == OrderStatus.Shipped)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Class="mt-2">
|
||||
پس از تغییر وضعیت به "ارسال شده"، اطلاعرسانی به کاربر ارسال خواهد شد.
|
||||
<MudAlert Severity="Severity.Info" Dense="true">
|
||||
پس از تغییر وضعیت به «ارسال شده»، اطلاعرسانی به کاربر ارسال خواهد شد.
|
||||
</MudAlert>
|
||||
}
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<!-- یادداشتها -->
|
||||
@if (!string.IsNullOrEmpty(Order.Notes) || !string.IsNullOrEmpty(Order.AdminNotes))
|
||||
<MudPaper Class="pa-3">
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.subtitle2">آدرس تحویل</MudText>
|
||||
<MudDivider />
|
||||
@if (Order.Address != null && !string.IsNullOrWhiteSpace(Order.Address.Address))
|
||||
{
|
||||
@if (!string.IsNullOrWhiteSpace(Order.Address.Title))
|
||||
{
|
||||
<MudText Typo="Typo.body2"><strong>@Order.Address.Title</strong></MudText>
|
||||
}
|
||||
<MudText Typo="Typo.body2">@Order.Address.Address</MudText>
|
||||
@if (!string.IsNullOrWhiteSpace(Order.Address.PostalCode))
|
||||
{
|
||||
<MudText Typo="Typo.body2">کد پستی: @Order.Address.PostalCode</MudText>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(DisplayMobile))
|
||||
{
|
||||
<MudText Typo="Typo.body2">تلفن سفارشدهنده: @DisplayMobile</MudText>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.caption">آدرسی ثبت نشده است.</MudText>
|
||||
}
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<MudPaper Class="pa-3">
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.subtitle2">اقلام فاکتور</MudText>
|
||||
<MudDivider />
|
||||
@if (Order.Items.Count > 0)
|
||||
{
|
||||
<MudTable Items="Order.Items" Dense="true">
|
||||
<HeaderContent>
|
||||
<MudTh>تصویر</MudTh>
|
||||
<MudTh>محصول</MudTh>
|
||||
<MudTh>قیمت واحد</MudTh>
|
||||
<MudTh>تعداد</MudTh>
|
||||
<MudTh>تخفیف</MudTh>
|
||||
<MudTh>قیمت نهایی</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>
|
||||
@if (!string.IsNullOrWhiteSpace(context.ThumbnailPath))
|
||||
{
|
||||
<Image Src="@context.ThumbnailPath" Height="50" Width="50" />
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd>@context.ProductTitle</MudTd>
|
||||
<MudTd>@context.UnitPrice.ToString("N0")</MudTd>
|
||||
<MudTd>@context.Count</MudTd>
|
||||
<MudTd>
|
||||
@if (context.MaxDiscountPercent > 0)
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Success">@context.MaxDiscountPercent%</MudChip>
|
||||
}
|
||||
else if (context.DiscountAmount > 0)
|
||||
{
|
||||
@context.DiscountAmount.ToString("N0")
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>-</text>
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd>@context.FinalPrice.ToString("N0")</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.caption">آیتمی برای این سفارش ثبت نشده است.</MudText>
|
||||
}
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
@if (!string.IsNullOrEmpty(Order.Notes))
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
@if (!string.IsNullOrEmpty(Order.Notes))
|
||||
{
|
||||
<MudText Typo="Typo.h6" GutterBottom="true">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Comment" Size="Size.Small" />
|
||||
یادداشت کاربر
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mb-4">@Order.Notes</MudText>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Order.AdminNotes))
|
||||
{
|
||||
<MudText Typo="Typo.h6" GutterBottom="true">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Note" Size="Size.Small" />
|
||||
یادداشت ادمین
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body1">@Order.AdminNotes</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudPaper Class="pa-3">
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.subtitle2">یادداشت کاربر</MudText>
|
||||
<MudDivider />
|
||||
<MudText Typo="Typo.body2">@Order.Notes</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
</MudGrid>
|
||||
</MudStack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Secondary"
|
||||
StartIcon="@Icons.Material.Filled.Print"
|
||||
OnClick="PrintInvoiceAsync">
|
||||
چاپ فاکتور
|
||||
</MudButton>
|
||||
<MudSpacer />
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Default" OnClick="Close">
|
||||
بستن
|
||||
</MudButton>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using BackOffice.Common.Utilities;
|
||||
using BackOffice.Services.DiscountOrder;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.DiscountShop.Components;
|
||||
@@ -8,6 +10,7 @@ public partial class OrderDetailsDialog
|
||||
{
|
||||
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
[Inject] public IDiscountOrderService OrderService { get; set; } = default!;
|
||||
[Inject] private IJSRuntime JsRuntime { get; set; } = default!;
|
||||
|
||||
[Parameter] public DiscountOrderDetailsDto Order { get; set; } = null!;
|
||||
|
||||
@@ -17,6 +20,9 @@ public partial class OrderDetailsDialog
|
||||
private bool _isSaving;
|
||||
private List<TimelineStep> _timelineSteps = new();
|
||||
|
||||
private string? DisplayMobile =>
|
||||
!string.IsNullOrWhiteSpace(Order.UserMobile) ? Order.UserMobile : Order.Address?.Phone;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_newStatus = Order.Status;
|
||||
@@ -127,34 +133,69 @@ public partial class OrderDetailsDialog
|
||||
|
||||
private void Close() => MudDialog.Close();
|
||||
|
||||
private Color GetStatusColor(OrderStatus status)
|
||||
private Color GetPaymentStatusColor(int status) => status switch
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
OrderStatus.Pending => Color.Warning,
|
||||
OrderStatus.Paid => Color.Info,
|
||||
OrderStatus.Processing => Color.Primary,
|
||||
OrderStatus.Shipped => Color.Secondary,
|
||||
OrderStatus.Delivered => Color.Success,
|
||||
OrderStatus.Cancelled => Color.Error,
|
||||
OrderStatus.Returned => Color.Dark,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
0 => Color.Warning,
|
||||
1 => Color.Success,
|
||||
2 => Color.Error,
|
||||
3 => Color.Info,
|
||||
_ => Color.Default
|
||||
};
|
||||
|
||||
private string GetStatusText(OrderStatus status)
|
||||
private string GetPaymentStatusText(int status) => status switch
|
||||
{
|
||||
return status switch
|
||||
0 => "در انتظار پرداخت",
|
||||
1 => "پرداخت شده",
|
||||
2 => "ناموفق",
|
||||
3 => "بازپرداخت شده",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
|
||||
private string GetPaymentStatusIcon(int status) => status switch
|
||||
{
|
||||
1 => Icons.Material.Filled.CheckCircle,
|
||||
2 => Icons.Material.Filled.Cancel,
|
||||
3 => Icons.Material.Filled.Replay,
|
||||
_ => Icons.Material.Filled.Schedule
|
||||
};
|
||||
|
||||
private async Task PrintInvoiceAsync()
|
||||
{
|
||||
var lines = Order.Items.Select(i => new OrderInvoicePrint.InvoiceLine
|
||||
{
|
||||
OrderStatus.Pending => "در انتظار پرداخت",
|
||||
OrderStatus.Paid => "پرداخت شده",
|
||||
OrderStatus.Processing => "در حال آمادهسازی",
|
||||
OrderStatus.Shipped => "ارسال شده",
|
||||
OrderStatus.Delivered => "تحویل داده شده",
|
||||
OrderStatus.Cancelled => "لغو شده",
|
||||
OrderStatus.Returned => "مرجوع شده",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
Title = i.ProductTitle,
|
||||
Quantity = i.Count,
|
||||
UnitPrice = i.UnitPrice,
|
||||
Discount = i.DiscountAmount,
|
||||
LineTotal = i.FinalPrice > 0 ? i.FinalPrice : i.UnitPrice * i.Count
|
||||
}).ToList();
|
||||
|
||||
var mobile = !string.IsNullOrWhiteSpace(Order.UserMobile)
|
||||
? Order.UserMobile
|
||||
: Order.Address?.Phone;
|
||||
|
||||
var html = OrderInvoicePrint.BuildHtml(new OrderInvoicePrint.InvoiceModel
|
||||
{
|
||||
ShopTitle = "فروشگاه اعتباری — کارا بازار سلامت",
|
||||
OrderId = string.IsNullOrWhiteSpace(Order.OrderNumber) ? Order.Id.ToString() : Order.OrderNumber,
|
||||
CustomerName = Order.UserFullName,
|
||||
CustomerMobile = mobile,
|
||||
Address = Order.Address?.Address,
|
||||
PostalCode = Order.Address?.PostalCode,
|
||||
PaymentStatus = GetPaymentStatusText(Order.PaymentStatusValue),
|
||||
TransactionId = Order.TransactionId,
|
||||
TrackingCode = Order.TrackingCode,
|
||||
Created = Order.Created,
|
||||
Subtotal = lines.Sum(l => l.UnitPrice * l.Quantity),
|
||||
DiscountTotal = lines.Sum(l => l.Discount),
|
||||
CreditUsed = Order.DiscountBalanceUsed,
|
||||
GatewayAmount = Order.GatewayAmount,
|
||||
VatAmount = Order.VatAmount,
|
||||
TotalAmount = Order.TotalPrice,
|
||||
Lines = lines
|
||||
});
|
||||
|
||||
await JsRuntime.InvokeVoidAsync("jsHtmlToPdf", html, $"فاکتور سفارش {Order.Id}");
|
||||
}
|
||||
|
||||
private enum StepState
|
||||
|
||||
@@ -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))"
|
||||
Selected="@(_selectedParentId == null)">
|
||||
Class="@(_selectedParentId == null ? "mud-selected-item" : "")">
|
||||
<MudText Typo="Typo.body2">همه دستهبندیها</MudText>
|
||||
</MudListItem>
|
||||
@foreach (var node in _treeNodes)
|
||||
{
|
||||
<MudListItem T="string"
|
||||
OnClick="@(() => OnTreeNodeSelected(node.Id))"
|
||||
Selected="@(_selectedParentId == node.Id)">
|
||||
Class="@(_selectedParentId == node.Id ? "mud-selected-item" : "")">
|
||||
<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 T="DateTime?" Label="از تاریخ" @bind-Date="_fromDateFilter"
|
||||
<MudDatePicker Label="از تاریخ" @bind-Date="_fromDateFilter"
|
||||
Variant="Variant.Outlined" Margin="Margin.Dense"/>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudDatePicker T="DateTime?" Label="تا تاریخ" @bind-Date="_toDateFilter"
|
||||
<MudDatePicker Label="تا تاریخ" @bind-Date="_toDateFilter"
|
||||
Variant="Variant.Outlined" Margin="Margin.Dense"/>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
@@ -49,6 +49,8 @@
|
||||
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.ReadyForOfficePickup">آماده تحویل در دفتر</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
</Filters>
|
||||
@@ -97,20 +99,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,5 +1,6 @@
|
||||
using System.Text;
|
||||
using BackOffice.Common.BaseComponents;
|
||||
using BackOffice.Common.Utilities;
|
||||
using BackOffice.Services.DiscountOrder;
|
||||
using BackOffice.Pages.DiscountShop.Components;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
@@ -74,6 +75,15 @@ public partial class DiscountOrdersMainPage
|
||||
return;
|
||||
}
|
||||
|
||||
// تکمیل فیلدهای فاکتور از ردیف لیست (جزئیات API نام/موبایل/VAT ندارد)
|
||||
var listRow = _orders?.FirstOrDefault(o => o.Id == orderId);
|
||||
if (listRow != null)
|
||||
{
|
||||
order.UserFullName = listRow.UserFullName;
|
||||
order.UserMobile = listRow.UserMobile;
|
||||
order.VatAmount = listRow.VatAmount;
|
||||
}
|
||||
|
||||
var parameters = new DialogParameters<OrderDetailsDialog> { { x => x.Order, order } };
|
||||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Large, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<OrderDetailsDialog>("جزئیات سفارش", parameters, options);
|
||||
@@ -178,8 +188,9 @@ public partial class DiscountOrdersMainPage
|
||||
OrderStatus.Processing => Color.Primary,
|
||||
OrderStatus.Shipped => Color.Secondary,
|
||||
OrderStatus.Delivered => Color.Success,
|
||||
OrderStatus.Cancelled => Color.Error,
|
||||
OrderStatus.Returned => Color.Dark,
|
||||
OrderStatus.Cancelled => Color.Dark,
|
||||
OrderStatus.ReadyForOfficePickup => Color.Primary,
|
||||
OrderStatus.Returned => Color.Error,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
@@ -194,6 +205,7 @@ public partial class DiscountOrdersMainPage
|
||||
OrderStatus.Shipped => "ارسال شده",
|
||||
OrderStatus.Delivered => "تحویل داده شده",
|
||||
OrderStatus.Cancelled => "لغو شده",
|
||||
OrderStatus.ReadyForOfficePickup => "آماده تحویل در دفتر",
|
||||
OrderStatus.Returned => "مرجوع شده",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
@@ -233,44 +245,36 @@ public partial class DiscountOrdersMainPage
|
||||
|
||||
try
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("شناسه,نام کاربر,موبایل,مبلغ کل,تخفیف کیفپول,پرداخت درگاه,تعداد آیتم,وضعیت پرداخت,وضعیت ارسال,تاریخ پرداخت,تاریخ ثبت,آدرس");
|
||||
|
||||
foreach (var o in _orders)
|
||||
var headers = new[]
|
||||
{
|
||||
sb.AppendLine(string.Join(",",
|
||||
o.Id,
|
||||
EscapeCsv(o.UserFullName),
|
||||
EscapeCsv(o.UserMobile),
|
||||
o.TotalPrice.ToString("N0"),
|
||||
o.DiscountBalanceUsed.ToString("N0"),
|
||||
o.GatewayAmount.ToString("N0"),
|
||||
o.ItemsCount,
|
||||
EscapeCsv(GetPaymentStatusText(o.PaymentStatusValue)),
|
||||
EscapeCsv(GetStatusText(o.Status)),
|
||||
o.PaymentDate?.MiladiToJalaliWithTime() ?? "-",
|
||||
o.Created?.MiladiToJalaliWithTime() ?? "-",
|
||||
EscapeCsv(o.ShippingAddress ?? "-")));
|
||||
}
|
||||
"شناسه", "نام مشتری", "موبایل", "مبلغ کل", "کیف پول اعتباری", "پرداخت درگاه",
|
||||
"تعداد آیتم", "وضعیت پرداخت", "وضعیت ارسال", "تاریخ پرداخت", "تاریخ ثبت", "آدرس"
|
||||
};
|
||||
|
||||
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
var filename = $"discount-orders-{DateTime.Now:yyyyMMddHHmmss}.csv";
|
||||
var rows = _orders.Select(o => new object?[]
|
||||
{
|
||||
o.Id,
|
||||
CsvExportHelper.CustomerName(o.UserFullName, o.UserMobile, o.UserId),
|
||||
o.UserMobile ?? "",
|
||||
o.TotalPrice,
|
||||
o.DiscountBalanceUsed,
|
||||
o.GatewayAmount,
|
||||
o.ItemsCount,
|
||||
GetPaymentStatusText(o.PaymentStatusValue),
|
||||
GetStatusText(o.Status),
|
||||
o.PaymentDate?.MiladiToJalaliWithTime() ?? "-",
|
||||
o.Created?.MiladiToJalaliWithTime() ?? "-",
|
||||
o.ShippingAddress ?? "-"
|
||||
});
|
||||
|
||||
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
|
||||
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
|
||||
var filename = $"discount-orders-{DateTime.Now:yyyyMMddHHmmss}.xls";
|
||||
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
|
||||
CsvExportHelper.ToExcelXmlBase64("سفارشهای اعتباری", headers, rows));
|
||||
Snackbar.Add("خروجی Excel آماده شد.", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private static string EscapeCsv(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return "";
|
||||
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
|
||||
return $"\"{value.Replace("\"", "\"\"")}\"";
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,6 +48,7 @@
|
||||
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Processing">در حال پردازش</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Shipped">ارسال شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Delivered">تحویل شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="OrderStatus.ReadyForOfficePickup">آماده تحویل در دفتر</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Cancelled">لغو شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Returned">مرجوع شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
@@ -113,19 +114,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>
|
||||
@@ -242,12 +243,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="وضعیت">
|
||||
@@ -498,7 +499,8 @@
|
||||
OrderStatus.Processing => Color.Info,
|
||||
OrderStatus.Shipped => Color.Info,
|
||||
OrderStatus.Delivered => Color.Success,
|
||||
OrderStatus.Cancelled => Color.Error,
|
||||
OrderStatus.ReadyForOfficePickup => Color.Primary,
|
||||
OrderStatus.Cancelled => Color.Dark,
|
||||
OrderStatus.Returned => Color.Error,
|
||||
_ => Color.Default
|
||||
};
|
||||
@@ -513,6 +515,7 @@
|
||||
OrderStatus.Processing => "در حال پردازش",
|
||||
OrderStatus.Shipped => "ارسال شده",
|
||||
OrderStatus.Delivered => "تحویل شده",
|
||||
OrderStatus.ReadyForOfficePickup => "آماده تحویل در دفتر",
|
||||
OrderStatus.Cancelled => "لغو شده",
|
||||
OrderStatus.Returned => "مرجوع شده",
|
||||
_ => "نامشخص"
|
||||
@@ -527,28 +530,31 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("شناسه,نام کاربر,موبایل,تاریخ ثبت,تعداد آیتم,مبلغ نهایی,تخفیف,وضعیت");
|
||||
|
||||
foreach (var o in _orders.Where(o => o.Created.HasValue).OrderBy(o => o.Created))
|
||||
var headers = new[]
|
||||
{
|
||||
sb.AppendLine(string.Join(",",
|
||||
"شناسه", "نام مشتری", "موبایل", "تاریخ ثبت", "تعداد آیتم", "مبلغ نهایی", "تخفیف", "وضعیت"
|
||||
};
|
||||
|
||||
var rows = _orders
|
||||
.OrderBy(o => o.Created ?? o.PaymentDate ?? DateTime.MaxValue)
|
||||
.Select(o => new object?[]
|
||||
{
|
||||
o.Id,
|
||||
EscapeCsv(o.UserFullName),
|
||||
EscapeCsv(o.UserMobile),
|
||||
o.Created?.MiladiToJalaliWithTime() ?? "-",
|
||||
CsvExportHelper.CustomerName(o.UserFullName, o.UserMobile, o.UserId),
|
||||
o.UserMobile ?? "",
|
||||
o.Created?.MiladiToJalaliWithTime()
|
||||
?? o.PaymentDate?.MiladiToJalaliWithTime()
|
||||
?? "-",
|
||||
o.ItemsCount,
|
||||
o.GatewayAmount,
|
||||
o.DiscountBalanceUsed,
|
||||
GetStatusText(o.Status)));
|
||||
}
|
||||
GetStatusText(o.Status)
|
||||
});
|
||||
|
||||
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
var filename = $"discount-sales-{DateTime.Now:yyyyMMddHHmmss}.csv";
|
||||
|
||||
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
|
||||
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
|
||||
var filename = $"discount-sales-{DateTime.Now:yyyyMMddHHmmss}.xls";
|
||||
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
|
||||
CsvExportHelper.ToExcelXmlBase64("گزارش فروش اعتباری", headers, rows));
|
||||
Snackbar.Add("خروجی Excel آماده شد.", Severity.Success);
|
||||
}
|
||||
|
||||
private async Task ExportToPdf()
|
||||
@@ -566,18 +572,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} | {o.UserFullName} | {o.Created?.MiladiToJalaliWithTime() ?? "-"} | {o.ItemsCount} | {o.GatewayAmount:N0} | {o.DiscountBalanceUsed:N0} | {GetStatusText(o.Status)}");
|
||||
$"{o.Id} | {CsvExportHelper.CustomerName(o.UserFullName, o.UserMobile, o.UserId)} | {o.Created?.MiladiToJalaliWithTime() ?? "-"} | {o.ItemsCount} | {o.GatewayAmount:N0} | {o.DiscountBalanceUsed:N0} | {GetStatusText(o.Status)}");
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
|
||||
@@ -587,14 +593,4 @@
|
||||
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
|
||||
Snackbar.Add("خروجی PDF (نسخه متنی) آماده شد.", Severity.Success);
|
||||
}
|
||||
|
||||
private static string EscapeCsv(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return "";
|
||||
|
||||
var needsQuotes = value.Contains(',') || value.Contains('"') || value.Contains('\n');
|
||||
var escaped = value.Replace("\"", "\"\"");
|
||||
return needsQuotes ? $"\"{escaped}\"" : escaped;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.Inventory
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
@using InventoryProductType = CMSMicroservice.Protobuf.Protos.Inventory.ProductType
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
@@ -19,12 +20,16 @@
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudNumericField T="long" @bind-Value="_productId"
|
||||
Label="شناسه محصول"
|
||||
Variant="Variant.Outlined"
|
||||
Required="true"
|
||||
RequiredError="شناسه محصول الزامی است"
|
||||
Min="1" />
|
||||
@if (_productType == InventoryProductType.RegularProduct)
|
||||
{
|
||||
<ProductsAutoComplete @bind-Value="_productId"
|
||||
Label="انتخاب محصول عادی" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<DiscountProductsAutoComplete @bind-Value="_discountProductId"
|
||||
Label="انتخاب محصول تخفیفی" />
|
||||
}
|
||||
</MudItem>
|
||||
}
|
||||
else
|
||||
@@ -103,7 +108,8 @@
|
||||
private bool _isSubmitting;
|
||||
|
||||
private InventoryProductType _productType = InventoryProductType.RegularProduct;
|
||||
private long _productId;
|
||||
private long? _productId;
|
||||
private long? _discountProductId;
|
||||
private long? _warehouseId;
|
||||
private int _quantity = 1;
|
||||
private string _referenceNumber = string.Empty;
|
||||
@@ -111,9 +117,12 @@
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
if (ProductIdParam.HasValue)
|
||||
if (ProductIdParam.HasValue && ProductTypeParam.HasValue)
|
||||
{
|
||||
_productId = ProductIdParam.Value;
|
||||
if (ProductTypeParam.Value == InventoryProductType.DiscountProduct)
|
||||
_discountProductId = ProductIdParam.Value;
|
||||
else
|
||||
_productId = ProductIdParam.Value;
|
||||
}
|
||||
if (ProductTypeParam.HasValue)
|
||||
{
|
||||
@@ -144,9 +153,19 @@
|
||||
_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 = ProductIdParam ?? _productId,
|
||||
ProductId = selectedProductId.Value,
|
||||
ProductType = ProductTypeParam ?? _productType,
|
||||
Quantity = _quantity,
|
||||
ReferenceNumber = _referenceNumber ?? string.Empty,
|
||||
|
||||
@@ -33,10 +33,8 @@
|
||||
Min="0" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="2">
|
||||
<MudNumericField @bind-Value="_maxBalance"
|
||||
Label="حداکثر موجودی"
|
||||
Variant="Variant.Outlined"
|
||||
Min="0" />
|
||||
<PackageSelect Label="پکیج"
|
||||
@bind-SelectedPackageId="_filterPackageId" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="2" Class="d-flex align-center">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
@@ -72,6 +70,14 @@
|
||||
<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">
|
||||
@@ -129,7 +135,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="وضعیت">
|
||||
@@ -193,6 +199,7 @@
|
||||
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;
|
||||
@@ -222,6 +229,11 @@ 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
|
||||
@@ -238,7 +250,8 @@ private MudDataGrid <UserWeeklyBalanceModel> _grid { get; set; }
|
||||
MatchedBalance = b.TotalBalances,
|
||||
PoolContribution = b.WeeklyPoolContribution,
|
||||
CalculatedAt = b.CalculatedAt?.ToDateTime(),
|
||||
IsExpired = b.IsExpired
|
||||
IsExpired = b.IsExpired,
|
||||
PackageTitle = b.PackageTitle
|
||||
}).ToList();
|
||||
|
||||
// Apply balance range filter if specified
|
||||
@@ -308,5 +321,6 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.NetworkMembership
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6">
|
||||
<MudIcon Icon="@Icons.Material.Filled.SwapHoriz" Class="ml-1" />
|
||||
تغییر والد در شبکه
|
||||
</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudAlert Severity="Severity.Warning" Dense="true" Class="mb-4">
|
||||
⚠️ این عملیات حساس است و ساختار درخت شبکه را تغییر میدهد.
|
||||
تاریخچه تغییر ثبت خواهد شد.
|
||||
</MudAlert>
|
||||
|
||||
<MudForm @ref="_form">
|
||||
<MudStack Spacing="3">
|
||||
<MudTextField T="string"
|
||||
Label="کاربر فعلی"
|
||||
Value="@UserDisplayName"
|
||||
ReadOnly="true"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Person" />
|
||||
|
||||
<MudNumericField T="long"
|
||||
@bind-Value="_newParentId"
|
||||
Label="شناسه والد جدید"
|
||||
Required="true"
|
||||
RequiredError="شناسه والد جدید الزامی است"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.PersonSearch"
|
||||
Min="1" />
|
||||
|
||||
<MudSelect T="int"
|
||||
@bind-Value="_newLeg"
|
||||
Label="ساق جدید"
|
||||
Required="true"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.AccountTree">
|
||||
<MudSelectItem T="int" Value="0">چپ (تیم اول)</MudSelectItem>
|
||||
<MudSelectItem T="int" Value="1">راست (تیم دوم)</MudSelectItem>
|
||||
</MudSelect>
|
||||
|
||||
<MudTextField T="string"
|
||||
@bind-Value="_reason"
|
||||
Label="دلیل تغییر"
|
||||
Required="true"
|
||||
RequiredError="دلیل تغییر الزامی است"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="3"
|
||||
Placeholder="دلیل جابجایی کاربر در شبکه را وارد کنید..."
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Description" />
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel" Color="Color.Default">انصراف</MudButton>
|
||||
<MudButton OnClick="Submit"
|
||||
Color="Color.Error"
|
||||
Variant="Variant.Filled"
|
||||
Disabled="_isSubmitting"
|
||||
StartIcon="@Icons.Material.Filled.SwapHoriz">
|
||||
@if (_isSubmitting)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="ml-2" />
|
||||
<span>در حال انجام...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>تغییر والد</span>
|
||||
}
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,66 @@
|
||||
using CMSMicroservice.Protobuf.Protos.NetworkMembership;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Network.Components;
|
||||
|
||||
public partial class ChangeParentDialog
|
||||
{
|
||||
[Parameter] public long UserId { get; set; }
|
||||
[Parameter] public string UserDisplayName { get; set; } = "";
|
||||
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Inject] public NetworkMembershipContract.NetworkMembershipContractClient NetworkContract { get; set; } = default!;
|
||||
|
||||
private MudForm _form = default!;
|
||||
private long _newParentId;
|
||||
private int _newLeg;
|
||||
private string _reason = "";
|
||||
private bool _isSubmitting;
|
||||
|
||||
private async Task Submit()
|
||||
{
|
||||
await _form.Validate();
|
||||
if (!_form.IsValid)
|
||||
return;
|
||||
|
||||
if (_newParentId == UserId)
|
||||
{
|
||||
Snackbar.Add("والد جدید نمیتواند خود کاربر باشد.", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
_isSubmitting = true;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
var request = new ChangeNetworkParentRequest
|
||||
{
|
||||
UserId = UserId,
|
||||
NewParentId = _newParentId,
|
||||
NewLeg = _newLeg,
|
||||
Reason = _reason
|
||||
};
|
||||
|
||||
await NetworkContract.ChangeNetworkParentAsync(request);
|
||||
Snackbar.Add("والد کاربر با موفقیت تغییر کرد.", Severity.Success);
|
||||
MudDialog.Close(DialogResult.Ok(true));
|
||||
}
|
||||
catch (Grpc.Core.RpcException ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Status.Detail}", Severity.Error);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در تغییر والد: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSubmitting = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void Cancel() => MudDialog.Cancel();
|
||||
}
|
||||
@@ -169,6 +169,42 @@
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
|
||||
<TemplateColumn Title="نوع فعالسازی" Sortable="false">
|
||||
<CellTemplate>
|
||||
@if (context.Item.IsNewActivation == null)
|
||||
{
|
||||
<span style="color: #bbb;">-</span>
|
||||
}
|
||||
else if (context.Item.IsNewActivation == true)
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Success" Icon="@Icons.Material.Filled.FiberNew">
|
||||
عضو جدید
|
||||
</MudChip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Secondary" Icon="@Icons.Material.Filled.Refresh">
|
||||
خرید مجدد
|
||||
</MudChip>
|
||||
}
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="پکیج" Sortable="false">
|
||||
<CellTemplate>
|
||||
@if (!string.IsNullOrEmpty(context.Item.PackageName))
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Dark">
|
||||
@context.Item.PackageName
|
||||
</MudChip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span style="color: #bbb;">-</span>
|
||||
}
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="عملیات" Sortable="false">
|
||||
<CellTemplate>
|
||||
<MudButtonGroup Size="Size.Small" Variant="Variant.Text">
|
||||
@@ -337,7 +373,9 @@
|
||||
isClubActive = n.IsClubActive,
|
||||
isActivatedInTargetWeek = n.IsActivatedInTargetWeek,
|
||||
activationWeekDefinitionId = n.ActivationWeekDefinitionId,
|
||||
clubActivatedAt = n.ClubActivatedAt?.ToDateTime().ToLocalTime().MiladiToJalali() ?? ""
|
||||
clubActivatedAt = n.ClubActivatedAt?.ToDateTime().ToLocalTime().MiladiToJalali() ?? "",
|
||||
isNewActivation = n.IsNewActivation,
|
||||
packageName = n.PackageName ?? ""
|
||||
}).ToArray();
|
||||
|
||||
var options = new { filterWeek = _activationWeekFilter };
|
||||
|
||||
@@ -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.HasPurchasedGoldenPackage)
|
||||
@if (_userInfo.HasPurchasedPackage)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Success" Size="Size.Small" Icon="@Icons.Material.Filled.CheckCircle">
|
||||
خریداری شده
|
||||
@@ -434,6 +434,12 @@
|
||||
OnClick="@(() => NavigationManager.NavigateTo($"/commission/payouts?userId={UserId}"))">
|
||||
پرداختهای کاربر
|
||||
</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"
|
||||
@@ -537,4 +543,28 @@
|
||||
_ => "نامشخص"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task OpenChangeParentDialog()
|
||||
{
|
||||
var displayName = $"{_userInfo.UserName} (ID: {UserId})";
|
||||
var parameters = new DialogParameters<Network.Components.ChangeParentDialog>
|
||||
{
|
||||
{ x => x.UserId, UserId },
|
||||
{ x => x.UserDisplayName, displayName }
|
||||
};
|
||||
var options = new DialogOptions
|
||||
{
|
||||
MaxWidth = MaxWidth.Small,
|
||||
FullWidth = true,
|
||||
CloseOnEscapeKey = true
|
||||
};
|
||||
var dialog = await DialogService.ShowAsync<Network.Components.ChangeParentDialog>("تغییر والد شبکه", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false })
|
||||
{
|
||||
await LoadUserInfo();
|
||||
await LoadNetworkHistory();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.Package
|
||||
@using CMSMicroservice.Protobuf.Protos.Configuration
|
||||
@using BackOffice.Common.BaseComponents
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Tizzani.MudBlazor.HtmlEditor
|
||||
@@ -41,12 +42,77 @@
|
||||
<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" /> <!-- This will exclude the "insert image" toolbar option -->
|
||||
<MudHtmlToolbarOptions InsertImage="false" />
|
||||
</MudHtmlEditor>
|
||||
</MudItem>
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,4 +1,5 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.Package
|
||||
@using CMSMicroservice.Protobuf.Protos.Configuration
|
||||
@using BackOffice.Common.BaseComponents
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Tizzani.MudBlazor.HtmlEditor
|
||||
@@ -42,12 +43,77 @@
|
||||
<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" /> <!-- This will exclude the "insert image" toolbar option -->
|
||||
<MudHtmlToolbarOptions InsertImage="false" />
|
||||
</MudHtmlEditor>
|
||||
</MudItem>
|
||||
</MudStack>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -12,13 +12,21 @@
|
||||
</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 style="width: 58px;" />
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
<col style="width: 80px;" />
|
||||
</ColGroup>
|
||||
<ToolBarContent>
|
||||
<MudText>مدیریت پکیج</MudText>
|
||||
@@ -51,31 +59,42 @@
|
||||
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<PropertyColumn Property="x => x.Description" Title="توضیحات" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
|
||||
<PropertyColumn Property="x => x.Price" Title="قیمت" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
|
||||
<CellTemplate>
|
||||
@if (string.IsNullOrWhiteSpace(context.Item.Description))
|
||||
<MudText Typo="Typo.inherit">@context.Item.Price.ToString("N0")</MudText>
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<PropertyColumn Property="x => x.SortOrder" Title="ترتیب" />
|
||||
<PropertyColumn Property="x => x.IsActive" Title="وضعیت" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
|
||||
<CellTemplate>
|
||||
@if (context.Item.IsActive)
|
||||
{
|
||||
<MudText Typo="Typo.inherit">-</MudText>
|
||||
<MudChip T="string" Color="Color.Success" Variant="Variant.Filled" Size="Size.Small">فعال</MudChip>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
<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>
|
||||
<MudChip T="string" Color="Color.Default" Variant="Variant.Filled" Size="Size.Small">غیرفعال</MudChip>
|
||||
}
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
|
||||
<TemplateColumn StickyLeft="true" Title="عملیات" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
|
||||
<PropertyColumn Property="x => x.IsBasePackage" Title="نوع" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
|
||||
<CellTemplate>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
@if (context.Item.IsBasePackage)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Primary" Variant="Variant.Filled" Size="Size.Small">پایه</MudChip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChip T="string" Color="Color.Info" Variant="Variant.Outlined" Size="Size.Small">عادی</MudChip>
|
||||
}
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<TemplateColumn Title="">
|
||||
<CellTemplate>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudTooltip Text="ویرایش">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.EditNote" Size="Size.Small" ButtonType="ButtonType.Button" OnClick="@(() => Update(context.Item.Adapt<DataModel>()))" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Small" ButtonType="ButtonType.Button" OnClick="@(() => Update(context.Item))" />
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip 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.Small });
|
||||
var dialog = await DialogService.ShowAsync<UpdateDialog>($"ویرایش پکیج", parameters, new DialogOptions() { CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.Medium });
|
||||
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() } }, new DialogOptions() { CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.Small });
|
||||
var dialog = await DialogService.ShowAsync<CreateDialog>($"افزودن پکیج", new DialogParameters<CreateDialog>() { { x => x.Model, new CreateNewPackageRequest() { IsActive = true, SupportsDirectPurchase = true, DiscountMultiplier = 2.0, MagicWalletMultiplier = 2.5, MaxBalancesPerLeg = 300, MaxNetworkLevel = 15, MagicWalletMaxDeposit = 1_000_000_000, MagicWalletMaxCredit = 2_500_000_000 } } }, new DialogOptions() { CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.Medium });
|
||||
var result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
@using CMSMicroservice.Protobuf.Protos.UserWallet
|
||||
|
||||
@inject UserWalletContract.UserWalletContractClient WalletClient
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudStack Spacing="3">
|
||||
<MudAlert Severity="Severity.Warning" Dense="true" Variant="Variant.Outlined">
|
||||
مبلغ مستقیماً به کیف پول اصلی (Balance) کاربر اضافه میشود و در دفتر مالی با نوع «شارژ دستی کیف اصلی» ثبت میگردد.
|
||||
</MudAlert>
|
||||
|
||||
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_userId" />
|
||||
|
||||
<MudNumericField T="long" @bind-Value="_amount"
|
||||
Label="مبلغ (تومان)"
|
||||
Variant="Variant.Outlined"
|
||||
Min="10000"
|
||||
HelperText="حداقل ۱۰,۰۰۰ تومان" />
|
||||
|
||||
<MudTextField T="string" @bind-Value="_note"
|
||||
Label="توضیحات ادمین (اختیاری)"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="2" />
|
||||
</MudStack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel" Disabled="_isSubmitting">انصراف</MudButton>
|
||||
<MudButton Color="Color.Primary" Variant="Variant.Filled"
|
||||
OnClick="Submit" Disabled="_isSubmitting || !_userId.HasValue || _amount < 10000">
|
||||
@if (_isSubmitting)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
|
||||
}
|
||||
ثبت شارژ
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
private long? _userId;
|
||||
private long _amount = 10000;
|
||||
private string? _note;
|
||||
private bool _isSubmitting;
|
||||
|
||||
private void Cancel() => MudDialog.Cancel();
|
||||
|
||||
private async Task Submit()
|
||||
{
|
||||
if (!_userId.HasValue || _userId.Value <= 0)
|
||||
{
|
||||
Snackbar.Add("لطفاً کاربر را انتخاب کنید", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_amount < 10000)
|
||||
{
|
||||
Snackbar.Add("حداقل مبلغ شارژ ۱۰,۰۰۰ تومان است", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
bool? confirm = await DialogService.ShowMessageBox(
|
||||
"تأیید شارژ دستی",
|
||||
$"آیا از شارژ {_amount:N0} تومان به کیف پول اصلی کاربر #{_userId.Value} مطمئن هستید؟",
|
||||
yesText: "بله، شارژ شود", cancelText: "لغو");
|
||||
|
||||
if (confirm != true)
|
||||
return;
|
||||
|
||||
_isSubmitting = true;
|
||||
try
|
||||
{
|
||||
var request = new AdminManualCreditChargeRequest
|
||||
{
|
||||
UserId = _userId.Value,
|
||||
Amount = _amount
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(_note))
|
||||
request.Note = _note.Trim();
|
||||
|
||||
var response = await WalletClient.AdminManualCreditChargeAsync(request);
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
Snackbar.Add(
|
||||
$"شارژ موفق — تراکنش #{response.TransactionId}، موجودی جدید: {response.NewBalance:N0}",
|
||||
Severity.Success);
|
||||
MudDialog.Close(DialogResult.Ok(true));
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add(response.Message ?? "شارژ ناموفق بود", Severity.Error);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSubmitting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.ManualPayment
|
||||
@using CMSMicroservice.Protobuf.Protos.Package
|
||||
@using BackOffice.Pages.Payment.Components
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@@ -13,21 +14,36 @@
|
||||
<UserAutoComplete Label="انتخاب کاربر"
|
||||
@bind-SelectedUserId="_selectedUserId" />
|
||||
|
||||
<MudNumericField T="long"
|
||||
Label="مبلغ"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.End"
|
||||
AdornmentText="تومان"
|
||||
Disabled="true"
|
||||
@bind-Value="_createModel.Amount" />
|
||||
<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>
|
||||
}
|
||||
|
||||
<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>
|
||||
@@ -83,7 +99,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,4 +1,5 @@
|
||||
using CMSMicroservice.Protobuf.Protos.ManualPayment;
|
||||
using CMSMicroservice.Protobuf.Protos.Package;
|
||||
using Google.Protobuf;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
@@ -24,19 +25,23 @@ 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 = 56000000,
|
||||
};
|
||||
private ManualPaymentModel _createModel = new(){ Amount = 0 };
|
||||
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 = "عضویت دستی باشگاه مشتریان";
|
||||
@@ -48,6 +53,46 @@ 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)
|
||||
@@ -81,6 +126,12 @@ public partial class ManualPaymentDialog
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_selectedPackageId.HasValue || _selectedPackageId.Value <= 0)
|
||||
{
|
||||
Snackbar.Add("لطفاً پکیج را انتخاب کنید.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_selectedType <= 0)
|
||||
{
|
||||
Snackbar.Add("لطفاً نوع پرداخت را انتخاب کنید.", Severity.Warning);
|
||||
@@ -101,7 +152,8 @@ public partial class ManualPaymentDialog
|
||||
var request = new CreateManualPaymentRequest
|
||||
{
|
||||
UserId = _selectedUserId.Value,
|
||||
Amount = _createModel.Amount,
|
||||
Amount = 0, // deprecated — مبلغ از پکیج خوانده میشود
|
||||
PackageId = _selectedPackageId.Value,
|
||||
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>
|
||||
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
@page "/payment/ledger-transactions"
|
||||
@using BackOffice.Common.BaseComponents
|
||||
@using CMSMicroservice.Protobuf.Protos
|
||||
@using CMSMicroservice.Protobuf.Protos.Transactions
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
|
||||
@inject TransactionsContract.TransactionsContractClient TransactionsClient
|
||||
|
||||
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
|
||||
<Filters>
|
||||
<MudItem xs="12">
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true">
|
||||
گزارش read-only دفتر تراکنشهای مالی (جدول Transactions). برای جزئیات درگاه پرداخت به «گزارش تراکنش درگاه» مراجعه کنید.
|
||||
</MudAlert>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField T="long?" Label="شناسه" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_id" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField T="string" Label="RefId" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_refId" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField T="string" Label="توضیحات" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_description" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect T="PaymentStatus?" Clearable="true" Label="وضعیت پرداخت" Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" @bind-Value="_paymentStatus">
|
||||
<MudSelectItem T="PaymentStatus?" Value="PaymentStatus.Success">موفق</MudSelectItem>
|
||||
<MudSelectItem T="PaymentStatus?" Value="PaymentStatus.Pending">در انتظار</MudSelectItem>
|
||||
<MudSelectItem T="PaymentStatus?" Value="PaymentStatus.Reject">رد شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect T="TransactionType?" Clearable="true" Label="نوع تراکنش" Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" @bind-Value="_type">
|
||||
<MudSelectItem T="TransactionType?" Value="TransactionType.Buy">خرید</MudSelectItem>
|
||||
<MudSelectItem T="TransactionType?" Value="TransactionType.DepositIpg">واریز درگاه</MudSelectItem>
|
||||
<MudSelectItem T="TransactionType?" Value="TransactionType.DepositExternal1">واریز خارجی</MudSelectItem>
|
||||
<MudSelectItem T="TransactionType?" Value="TransactionType.Withdraw">برداشت</MudSelectItem>
|
||||
<MudSelectItem T="TransactionType?" Value="TransactionType.NetworkCommission">کمیسیون شبکه</MudSelectItem>
|
||||
<MudSelectItem T="TransactionType?" Value="TransactionType.ClubActivation">فعالسازی باشگاه</MudSelectItem>
|
||||
<MudSelectItem T="TransactionType?" Value="TransactionType.DiscountWalletCharge">شارژ کیف تخفیف</MudSelectItem>
|
||||
<MudSelectItem T="TransactionType?" Value="TransactionType.DiscountShopPurchase">خرید فروشگاه تخفیف</MudSelectItem>
|
||||
<MudSelectItem T="TransactionType?" Value="TransactionType.MagicWalletDeposit">واریز کیف جادویی</MudSelectItem>
|
||||
<MudSelectItem T="TransactionType?" Value="TransactionType.MagicWalletBonus">بونوس کیف جادویی</MudSelectItem>
|
||||
<MudSelectItem T="TransactionType?" Value="TransactionType.CreditWalletCharge">شارژ کیف اصلی</MudSelectItem>
|
||||
<MudSelectItem T="TransactionType?" Value="TransactionType.ManualCreditWalletCharge">شارژ دستی کیف اصلی</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<DateRangePicker @bind-From="_createdFrom" @bind-To="_createdTo" Label="بازه ثبت" />
|
||||
</MudItem>
|
||||
</Filters>
|
||||
<Content>
|
||||
<MudGrid Class="mb-3" Spacing="2">
|
||||
<MudItem xs="6" sm="3">
|
||||
<MudPaper Class="pa-3" Outlined="true">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">تعداد</MudText>
|
||||
<MudText Typo="Typo.h6">@_summaryTotalCount.ToString("N0")</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="6" sm="3">
|
||||
<MudPaper Class="pa-3" Outlined="true">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">جمع مبلغ (تومان)</MudText>
|
||||
<MudText Typo="Typo.h6">@_summaryTotalAmount.ToString("N0")</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="6" sm="2">
|
||||
<MudPaper Class="pa-3" Outlined="true">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">موفق</MudText>
|
||||
<MudText Typo="Typo.h6" Color="Color.Success">@_summarySuccess.ToString("N0")</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="6" sm="2">
|
||||
<MudPaper Class="pa-3" Outlined="true">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">در انتظار</MudText>
|
||||
<MudText Typo="Typo.h6" Color="Color.Warning">@_summaryPending.ToString("N0")</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="6" sm="2">
|
||||
<MudPaper Class="pa-3" Outlined="true">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">رد شده</MudText>
|
||||
<MudText Typo="Typo.h6" Color="Color.Error">@_summaryReject.ToString("N0")</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudDataGrid @ref="_dataGrid" T="GetAllTransactionsByFilterResponseModel"
|
||||
ServerData="LoadData" ReadOnly="true"
|
||||
Hover="true" Dense="true" Height="calc(100vh - 400px)">
|
||||
<ToolBarContent>
|
||||
<MudText Typo="Typo.subtitle1">دفتر تراکنشهای مالی</MudText>
|
||||
<MudSpacer />
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Info" Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.FileDownload" OnClick="ExportCsv">CSV</MudButton>
|
||||
</ToolBarContent>
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
<TemplateColumn Title="نوع">
|
||||
<CellTemplate>
|
||||
<MudChip T="string" Size="Size.Small" Variant="Variant.Text">
|
||||
@ReportLabels.GetTransactionTypeLabel(context.Item.HasType ? context.Item.Type : null)
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="مبلغ (تومان)">
|
||||
<CellTemplate>@context.Item.Amount.ToString("N0")</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<PropertyColumn Property="x => x.Description" Title="توضیحات" />
|
||||
<TemplateColumn Title="وضعیت">
|
||||
<CellTemplate>
|
||||
<MudChip T="string" Size="Size.Small" Color="@ReportLabels.GetTransactionPaymentStatusColor(context.Item.HasPaymentStatus ? context.Item.PaymentStatus : null)">
|
||||
@ReportLabels.GetTransactionPaymentStatusLabel(context.Item.HasPaymentStatus ? context.Item.PaymentStatus : null)
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<PropertyColumn Property="x => x.RefId" Title="RefId" />
|
||||
<TemplateColumn Title="تاریخ پرداخت">
|
||||
<CellTemplate>
|
||||
@(context.Item.PaymentDate != null
|
||||
? context.Item.PaymentDate.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()
|
||||
: "—")
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="تاریخ ثبت">
|
||||
<CellTemplate>@context.Item.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">
|
||||
با فیلتر فعلی موردی یافت نشد — بازه تاریخ یا فیلترها را تغییر دهید.
|
||||
</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="GetAllTransactionsByFilterResponseModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</Content>
|
||||
</BasePageComponent>
|
||||
|
||||
@code {
|
||||
private BasePageComponent? _basePage;
|
||||
private MudDataGrid<GetAllTransactionsByFilterResponseModel>? _dataGrid;
|
||||
private long? _id;
|
||||
private string? _refId, _description;
|
||||
private PaymentStatus? _paymentStatus;
|
||||
private TransactionType? _type;
|
||||
private DateTime? _createdFrom, _createdTo;
|
||||
private long _summaryTotalCount, _summaryTotalAmount, _summarySuccess, _summaryPending, _summaryReject;
|
||||
|
||||
private GetAllTransactionsByFilterFilter BuildFilter()
|
||||
{
|
||||
var filter = new GetAllTransactionsByFilterFilter
|
||||
{
|
||||
Id = _id > 0 ? _id : null,
|
||||
RefId = string.IsNullOrWhiteSpace(_refId) ? null : _refId,
|
||||
Description = string.IsNullOrWhiteSpace(_description) ? null : _description,
|
||||
CreatedFrom = _createdFrom.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_createdFrom.Value, DateTimeKind.Utc)) : null,
|
||||
CreatedTo = _createdTo.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_createdTo.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Utc)) : null
|
||||
};
|
||||
|
||||
if (_paymentStatus.HasValue)
|
||||
filter.PaymentStatus = _paymentStatus.Value;
|
||||
else
|
||||
filter.ClearPaymentStatusItem();
|
||||
|
||||
if (_type.HasValue)
|
||||
filter.Type = _type.Value;
|
||||
else
|
||||
filter.ClearTypeItem();
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
private async Task LoadSummary()
|
||||
{
|
||||
try
|
||||
{
|
||||
var s = await TransactionsClient.GetTransactionSummaryAsync(new GetTransactionSummaryRequest { Filter = BuildFilter() });
|
||||
_summaryTotalCount = s.TotalCount;
|
||||
_summaryTotalAmount = s.TotalAmount;
|
||||
_summarySuccess = s.SuccessCount;
|
||||
_summaryPending = s.PendingCount;
|
||||
_summaryReject = s.RejectCount;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private async Task<GridData<GetAllTransactionsByFilterResponseModel>> LoadData(GridState<GetAllTransactionsByFilterResponseModel> state)
|
||||
{
|
||||
try
|
||||
{
|
||||
await LoadSummary();
|
||||
var result = await TransactionsClient.GetAllTransactionsByFilterAsync(new GetAllTransactionsByFilterRequest
|
||||
{
|
||||
PaginationState = new PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize },
|
||||
SortBy = "-Created",
|
||||
Filter = BuildFilter()
|
||||
});
|
||||
return new GridData<GetAllTransactionsByFilterResponseModel>
|
||||
{
|
||||
Items = result?.Models?.ToList() ?? new(),
|
||||
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
return new GridData<GetAllTransactionsByFilterResponseModel>();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnFilterSubmit() { if (_dataGrid != null) await _dataGrid.ReloadServerData(); }
|
||||
private async Task OnFilterCleared()
|
||||
{
|
||||
_id = null; _refId = _description = null;
|
||||
_paymentStatus = null; _type = null;
|
||||
_createdFrom = _createdTo = null;
|
||||
if (_dataGrid != null) await _dataGrid.ReloadServerData();
|
||||
}
|
||||
|
||||
private async Task ExportCsv()
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await TransactionsClient.GetAllTransactionsByFilterAsync(new GetAllTransactionsByFilterRequest
|
||||
{
|
||||
PaginationState = new PaginationState { PageNumber = 1, PageSize = 5000 },
|
||||
SortBy = "-Created",
|
||||
Filter = BuildFilter()
|
||||
});
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine("Id,Type,Amount,Description,Status,RefId,PaymentDate,Created");
|
||||
foreach (var r in result.Models)
|
||||
{
|
||||
var type = r.HasType ? ReportLabels.GetTransactionTypeLabel(r.Type) : "";
|
||||
var status = r.HasPaymentStatus ? ReportLabels.GetTransactionPaymentStatusLabel(r.PaymentStatus) : "";
|
||||
var paymentDate = r.PaymentDate != null ? r.PaymentDate.ToDateTime().ToString("yyyy-MM-dd HH:mm") : "";
|
||||
sb.AppendLine($"{r.Id},{type},{r.Amount},\"{r.Description}\",{status},{r.RefId},{paymentDate},{r.Created.ToDateTime():yyyy-MM-dd HH:mm}");
|
||||
}
|
||||
await jsRuntime.InvokeVoidAsync("jsSaveAsFile", "ledger-transactions.csv", sb.ToString());
|
||||
}
|
||||
catch (Exception ex) { Snackbar.Add($"خطا در export: {ex.Message}", Severity.Error); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
@page "/payment/manual-credit-charges"
|
||||
@attribute [Authorize(Roles = "Administrator")]
|
||||
|
||||
@using BackOffice.Common.BaseComponents
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
@using BackOffice.Pages.Payment.Components
|
||||
@using CMSMicroservice.Protobuf.Protos
|
||||
@using CMSMicroservice.Protobuf.Protos.UserWallet
|
||||
@using DateTimeConverterCL
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
|
||||
@inject UserWalletContract.UserWalletContractClient WalletClient
|
||||
|
||||
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
|
||||
<Filters>
|
||||
<MudItem xs="12">
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true">
|
||||
شارژ دستی کیف پول اصلی (Balance) — بدون درگاه. جدا از «پرداختهای دستی» عضویت باشگاه.
|
||||
</MudAlert>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<UserAutoComplete Label="جستجوی کاربر" @bind-SelectedUserId="_userIdFilter" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField T="long?" Label="شناسه تراکنش" Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" @bind-Value="_transactionIdFilter" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField T="string" Label="RefId" Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" @bind-Value="_refIdFilter" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<DateRangePicker @bind-From="_createdFrom" @bind-To="_createdTo" Label="بازه ثبت" />
|
||||
</MudItem>
|
||||
</Filters>
|
||||
<Content>
|
||||
<MudDataGrid @ref="_dataGrid" T="ManualCreditChargeModel"
|
||||
ServerData="LoadData"
|
||||
Hover="true" Dense="true"
|
||||
Height="calc(100vh - 240px)">
|
||||
<ToolBarContent>
|
||||
<MudText Typo="Typo.subtitle1">شارژ دستی کیف پول اصلی</MudText>
|
||||
<MudSpacer />
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.Add" OnClick="OpenCreateDialog">
|
||||
شارژ دستی جدید
|
||||
</MudButton>
|
||||
</ToolBarContent>
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.TransactionId" Title="شناسه تراکنش" />
|
||||
<PropertyColumn Property="x => x.UserId" Title="شناسه کاربر" />
|
||||
<PropertyColumn Property="x => x.UserName" Title="نام کاربر" />
|
||||
<PropertyColumn Property="x => x.Amount" Title="مبلغ">
|
||||
<CellTemplate>
|
||||
@context.Item.Amount.ToString("N0")
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<PropertyColumn Property="x => x.NewBalance" Title="موجودی بعد از شارژ">
|
||||
<CellTemplate>
|
||||
@context.Item.NewBalance.ToString("N0")
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<PropertyColumn Property="x => x.RefId" Title="RefId" />
|
||||
<PropertyColumn Property="x => x.Description" Title="توضیحات" />
|
||||
<TemplateColumn Title="تاریخ">
|
||||
<CellTemplate>
|
||||
@(context.Item.Created?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-")
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="ManualCreditChargeModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</Content>
|
||||
</BasePageComponent>
|
||||
|
||||
@code {
|
||||
private BasePageComponent? _basePage;
|
||||
private MudDataGrid<ManualCreditChargeModel>? _dataGrid;
|
||||
private long? _userIdFilter;
|
||||
private long? _transactionIdFilter;
|
||||
private string? _refIdFilter;
|
||||
private DateTime? _createdFrom;
|
||||
private DateTime? _createdTo;
|
||||
|
||||
private async Task<GridData<ManualCreditChargeModel>> LoadData(GridState<ManualCreditChargeModel> state)
|
||||
{
|
||||
var request = new GetManualCreditChargesRequest
|
||||
{
|
||||
PaginationState = new PaginationState
|
||||
{
|
||||
PageNumber = state.Page + 1,
|
||||
PageSize = state.PageSize
|
||||
},
|
||||
Filter = new GetManualCreditChargesFilter()
|
||||
};
|
||||
|
||||
if (_userIdFilter.HasValue)
|
||||
request.Filter.UserId = _userIdFilter.Value;
|
||||
if (_transactionIdFilter.HasValue)
|
||||
request.Filter.TransactionId = _transactionIdFilter.Value;
|
||||
if (!string.IsNullOrWhiteSpace(_refIdFilter))
|
||||
request.Filter.RefId = _refIdFilter.Trim();
|
||||
if (_createdFrom.HasValue)
|
||||
request.Filter.CreatedFrom = Timestamp.FromDateTime(DateTime.SpecifyKind(_createdFrom.Value, DateTimeKind.Utc));
|
||||
if (_createdTo.HasValue)
|
||||
request.Filter.CreatedTo = Timestamp.FromDateTime(DateTime.SpecifyKind(_createdTo.Value, DateTimeKind.Utc));
|
||||
|
||||
var response = await WalletClient.GetManualCreditChargesAsync(request);
|
||||
|
||||
return new GridData<ManualCreditChargeModel>
|
||||
{
|
||||
Items = response.Models.ToList(),
|
||||
TotalItems = (int)(response.MetaData?.TotalCount ?? 0)
|
||||
};
|
||||
}
|
||||
|
||||
private async Task OnFilterSubmit()
|
||||
{
|
||||
if (_dataGrid != null)
|
||||
await _dataGrid.ReloadServerData();
|
||||
}
|
||||
|
||||
private async Task OnFilterCleared()
|
||||
{
|
||||
_userIdFilter = null;
|
||||
_transactionIdFilter = null;
|
||||
_refIdFilter = null;
|
||||
_createdFrom = null;
|
||||
_createdTo = null;
|
||||
if (_dataGrid != null)
|
||||
await _dataGrid.ReloadServerData();
|
||||
}
|
||||
|
||||
private async Task OpenCreateDialog()
|
||||
{
|
||||
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
var dialog = DialogService.Show<ManualCreditChargeDialog>("شارژ دستی کیف پول اصلی", options);
|
||||
var result = await dialog.Result;
|
||||
if (result is { Canceled: false } && _dataGrid != null)
|
||||
await _dataGrid.ReloadServerData();
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,12 @@
|
||||
|
||||
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
|
||||
<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>
|
||||
@@ -26,8 +32,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>
|
||||
@@ -219,8 +225,8 @@
|
||||
return type switch
|
||||
{
|
||||
1 => "واریز نقدی",
|
||||
2 => "شارژ کیفپول تخفیف",
|
||||
3 => "شارژ کیفپول شبکه",
|
||||
2 => "شارژ کیف پول اعتباری",
|
||||
3 => "شارژ پاداش های دریافتی",
|
||||
4 => "تسویه حساب",
|
||||
5 => "اصلاح خطا",
|
||||
6 => "بازگشت وجه",
|
||||
@@ -257,7 +263,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,6 +45,7 @@ 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)),
|
||||
|
||||
@@ -1,78 +1,115 @@
|
||||
@page "/payment/transactions"
|
||||
@using BackOffice.Common.BaseComponents
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
@using CMSMicroservice.Protobuf.Protos.PaymentTransaction
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
|
||||
@inject PaymentTransactionContract.PaymentTransactionContractClient PaymentTxClient
|
||||
|
||||
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
|
||||
<Filters>
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<BackOffice.Common.BaseComponents.DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه تاریخی" />
|
||||
<MudItem xs="12">
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true">
|
||||
گزارش read-only تراکنشهای درگاه پرداخت. برای ثبت پرداخت دستی به «پرداختهای دستی» مراجعه کنید.
|
||||
</MudAlert>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudSelect T="int?" Clearable="true" Label="وضعیت پرداخت" Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" @bind-Value="_statusFilter">
|
||||
<MudSelectItem T="int?" Value="@(0)">در انتظار</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(1)">موفق</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(2)">ناموفق</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(3)">بازگشت وجه</MudSelectItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_userId" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField T="string" Label="موبایل" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_mobile" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField T="string" Label="Authority" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_authority" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField T="string" Label="RefId" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_refId" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField T="string" Label="OrderId" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_orderId" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField T="string" Label="درگاه" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_gateway" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect T="bool?" Clearable="true" Label="وضعیت پرداخت" Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" @bind-Value="_paymentStatus">
|
||||
<MudSelectItem T="bool?" Value="@((bool?)true)">موفق</MudSelectItem>
|
||||
<MudSelectItem T="bool?" Value="@((bool?)false)">ناموفق / در انتظار</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudSelect T="int?" Clearable="true" Label="نوع تراکنش" Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" @bind-Value="_typeFilter">
|
||||
<MudSelectItem T="int?" Value="@(0)">نامشخص</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(1)">خرید پکیج</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(2)">پرداخت دستی</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(3)">شارژ کیفپول</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(4)">بازگشت وجه</MudSelectItem>
|
||||
</MudSelect>
|
||||
<DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه تاریخ" />
|
||||
</MudItem>
|
||||
</Filters>
|
||||
<Content>
|
||||
<MudDataGrid @ref="_dataGrid" T="TransactionModel"
|
||||
ServerData="LoadData"
|
||||
Hover="true" Dense="true"
|
||||
Height="calc(100vh - 240px)">
|
||||
<MudGrid Class="mb-3" Spacing="2">
|
||||
<MudItem xs="6" sm="3">
|
||||
<MudPaper Class="pa-3" Outlined="true">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">تعداد</MudText>
|
||||
<MudText Typo="Typo.h6">@_summaryTotalCount.ToString("N0")</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="6" sm="3">
|
||||
<MudPaper Class="pa-3" Outlined="true">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">جمع مبلغ (تومان)</MudText>
|
||||
<MudText Typo="Typo.h6">@_summaryTotalAmount.ToString("N0")</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="6" sm="3">
|
||||
<MudPaper Class="pa-3" Outlined="true">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">موفق</MudText>
|
||||
<MudText Typo="Typo.h6" Color="Color.Success">@_summarySuccessful.ToString("N0")</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="6" sm="3">
|
||||
<MudPaper Class="pa-3" Outlined="true">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">در انتظار</MudText>
|
||||
<MudText Typo="Typo.h6" Color="Color.Warning">@_summaryPending.ToString("N0")</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudDataGrid @ref="_dataGrid" T="PaymentTransactionModel"
|
||||
ServerData="LoadData" ReadOnly="true"
|
||||
Hover="true" Dense="true" Height="calc(100vh - 380px)">
|
||||
<ToolBarContent>
|
||||
<MudText Typo="Typo.subtitle1">گزارش تراکنش درگاه</MudText>
|
||||
<MudSpacer />
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Info" Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.FileDownload" OnClick="ExportCsv">CSV</MudButton>
|
||||
</ToolBarContent>
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
<PropertyColumn Property="x => x.RefId" Title="شماره مرجع" />
|
||||
<PropertyColumn Property="x => x.Amount" Title="مبلغ">
|
||||
<CellTemplate>
|
||||
@context.Item.Amount.ToString("N0")
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<PropertyColumn Property="x => x.UserId" Title="کاربر" />
|
||||
<PropertyColumn Property="x => x.Mobile" Title="موبایل" />
|
||||
<PropertyColumn Property="x => x.GatewayProvider" Title="درگاه" />
|
||||
<TemplateColumn Title="مبلغ (تومان)">
|
||||
<CellTemplate>@context.Item.Amount.ToString("N0")</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<PropertyColumn Property="x => x.Authority" Title="Authority" />
|
||||
<PropertyColumn Property="x => x.RefId" Title="RefId" />
|
||||
<PropertyColumn Property="x => x.CardPan" Title="کارت" />
|
||||
<TemplateColumn Title="وضعیت">
|
||||
<CellTemplate>
|
||||
<MudChip T="string"
|
||||
Size="Size.Small"
|
||||
Color="@GetStatusColor(context.Item.PaymentStatus)">
|
||||
@GetStatusText(context.Item.PaymentStatus)
|
||||
<MudChip T="string" Size="Size.Small" Color="@(context.Item.PaymentStatus ? Color.Success : Color.Warning)">
|
||||
@ReportLabels.GetPaymentStatusLabel(context.Item.PaymentStatus)
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="تاریخ پرداخت">
|
||||
<CellTemplate>
|
||||
@context.Item.PaymentDate.ToLocalTime().MiladiToJalaliWithTime()
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="نوع">
|
||||
<CellTemplate>
|
||||
@GetTypeText(context.Item.Type)
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="عملیات">
|
||||
<CellTemplate>
|
||||
<MudTooltip Text="جزئیات">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Info"
|
||||
Size="Size.Small"
|
||||
OnClick="@(() => OpenDetails(context.Item))" />
|
||||
</MudTooltip>
|
||||
</CellTemplate>
|
||||
<PropertyColumn Property="x => x.VerificationStatusCode" Title="کد Verify" />
|
||||
<PropertyColumn Property="x => x.OrderId" Title="OrderId" />
|
||||
<TemplateColumn Title="تاریخ">
|
||||
<CellTemplate>@context.Item.CreatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">
|
||||
با فیلتر فعلی موردی یافت نشد — بازه تاریخ یا کاربر را تغییر دهید.
|
||||
</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="TransactionModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
<MudDataGridPager T="PaymentTransactionModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
@@ -80,3 +117,87 @@
|
||||
</Content>
|
||||
</BasePageComponent>
|
||||
|
||||
@code {
|
||||
private BasePageComponent? _basePage;
|
||||
private MudDataGrid<PaymentTransactionModel>? _dataGrid;
|
||||
private long? _userId;
|
||||
private string? _mobile, _authority, _refId, _orderId, _gateway;
|
||||
private bool? _paymentStatus;
|
||||
private DateTime? _fromDate, _toDate;
|
||||
private long _summaryTotalCount, _summaryTotalAmount, _summarySuccessful, _summaryPending;
|
||||
|
||||
private GetAllPaymentTransactionByFilterFilter BuildFilter() => new()
|
||||
{
|
||||
UserId = _userId > 0 ? _userId : null,
|
||||
Mobile = string.IsNullOrWhiteSpace(_mobile) ? null : _mobile,
|
||||
Authority = string.IsNullOrWhiteSpace(_authority) ? null : _authority,
|
||||
RefId = string.IsNullOrWhiteSpace(_refId) ? null : _refId,
|
||||
OrderId = string.IsNullOrWhiteSpace(_orderId) ? null : _orderId,
|
||||
GatewayProvider = string.IsNullOrWhiteSpace(_gateway) ? null : _gateway,
|
||||
PaymentStatus = _paymentStatus,
|
||||
CreatedFrom = _fromDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_fromDate.Value, DateTimeKind.Utc)) : null,
|
||||
CreatedTo = _toDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_toDate.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Utc)) : null
|
||||
};
|
||||
|
||||
private async Task LoadSummary()
|
||||
{
|
||||
try
|
||||
{
|
||||
var s = await PaymentTxClient.GetPaymentTransactionSummaryAsync(new GetPaymentTransactionSummaryRequest { Filter = BuildFilter() });
|
||||
_summaryTotalCount = s.TotalCount;
|
||||
_summaryTotalAmount = s.TotalAmount;
|
||||
_summarySuccessful = s.SuccessfulCount;
|
||||
_summaryPending = s.PendingCount;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private async Task<GridData<PaymentTransactionModel>> LoadData(GridState<PaymentTransactionModel> state)
|
||||
{
|
||||
try
|
||||
{
|
||||
await LoadSummary();
|
||||
var result = await PaymentTxClient.GetAllPaymentTransactionByFilterAsync(new GetAllPaymentTransactionByFilterRequest
|
||||
{
|
||||
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize },
|
||||
Filter = BuildFilter()
|
||||
});
|
||||
return new GridData<PaymentTransactionModel>
|
||||
{
|
||||
Items = result?.Models?.ToList() ?? new(),
|
||||
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
return new GridData<PaymentTransactionModel>();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnFilterSubmit() { if (_dataGrid != null) await _dataGrid.ReloadServerData(); }
|
||||
private async Task OnFilterCleared()
|
||||
{
|
||||
_userId = null; _mobile = _authority = _refId = _orderId = _gateway = null;
|
||||
_paymentStatus = null; _fromDate = _toDate = null;
|
||||
if (_dataGrid != null) await _dataGrid.ReloadServerData();
|
||||
}
|
||||
|
||||
private async Task ExportCsv()
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await PaymentTxClient.GetAllPaymentTransactionByFilterAsync(new GetAllPaymentTransactionByFilterRequest
|
||||
{
|
||||
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = 1, PageSize = 5000 },
|
||||
Filter = BuildFilter()
|
||||
});
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine("Id,UserId,Mobile,Gateway,Amount,Authority,RefId,Status,OrderId,Created");
|
||||
foreach (var r in result.Models)
|
||||
sb.AppendLine($"{r.Id},{r.UserId},{r.Mobile},{r.GatewayProvider},{r.Amount},{r.Authority},{r.RefId},{r.PaymentStatus},{r.OrderId},{r.CreatedAt.ToDateTime():yyyy-MM-dd HH:mm}");
|
||||
await jsRuntime.InvokeVoidAsync("jsSaveAsFile", "payment-transactions.csv", sb.ToString());
|
||||
}
|
||||
catch (Exception ex) { Snackbar.Add($"خطا در export: {ex.Message}", Severity.Error); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
using BackOffice.Common.BaseComponents;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Payment;
|
||||
|
||||
public partial class Transactions
|
||||
{
|
||||
private BasePageComponent? _basePage;
|
||||
private MudDataGrid<TransactionModel>? _dataGrid;
|
||||
|
||||
private DateTime? _fromDate;
|
||||
private DateTime? _toDate;
|
||||
private int? _statusFilter;
|
||||
private int? _typeFilter;
|
||||
|
||||
private async Task<GridData<TransactionModel>> LoadData(GridState<TransactionModel> state)
|
||||
{
|
||||
// TODO: Connect to BackOffice.BFF Transactions when API is ready
|
||||
await Task.CompletedTask;
|
||||
|
||||
return new GridData<TransactionModel>
|
||||
{
|
||||
Items = Array.Empty<TransactionModel>(),
|
||||
TotalItems = 0
|
||||
};
|
||||
}
|
||||
|
||||
private async Task OnFilterSubmit()
|
||||
{
|
||||
if (_dataGrid != null)
|
||||
await _dataGrid.ReloadServerData();
|
||||
}
|
||||
|
||||
private async Task OnFilterCleared()
|
||||
{
|
||||
_fromDate = null;
|
||||
_toDate = null;
|
||||
_statusFilter = null;
|
||||
_typeFilter = null;
|
||||
if (_dataGrid != null)
|
||||
await _dataGrid.ReloadServerData();
|
||||
}
|
||||
|
||||
private void OpenDetails(TransactionModel model)
|
||||
{
|
||||
// TODO: Open TransactionDetailsDialog with model
|
||||
}
|
||||
|
||||
private static string GetStatusText(int status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
0 => "در انتظار",
|
||||
1 => "موفق",
|
||||
2 => "ناموفق",
|
||||
3 => "بازگشت وجه",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
}
|
||||
|
||||
private static Color GetStatusColor(int status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
0 => Color.Warning,
|
||||
1 => Color.Success,
|
||||
2 => Color.Error,
|
||||
3 => Color.Info,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetTypeText(int type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
1 => "خرید پکیج",
|
||||
2 => "پرداخت دستی",
|
||||
3 => "شارژ کیفپول",
|
||||
4 => "بازگشت وجه",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
}
|
||||
|
||||
private class TransactionModel
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string? RefId { get; set; }
|
||||
public long Amount { get; set; }
|
||||
public int PaymentStatus { get; set; }
|
||||
public DateTime PaymentDate { get; set; }
|
||||
public int Type { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,8 +174,7 @@
|
||||
<MudItem xs="12">
|
||||
<MudSwitch T="bool" @bind-Value="_networkConfig.SpilloverEnabled"
|
||||
Color="Color.Primary"
|
||||
Label="Spillover فعال باشد"
|
||||
HelperText="اعضای اضافی به پایین درخت منتقل شوند" />
|
||||
Label="Spillover فعال باشد — اعضای اضافی به پایین درخت منتقل شوند" />
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
@@ -340,8 +339,7 @@
|
||||
<MudItem xs="12">
|
||||
<MudSwitch @bind-Value="_systemConfig.MaintenanceMode"
|
||||
Color="Color.Warning"
|
||||
Label="حالت تعمیر و نگهداری"
|
||||
HelperText="غیرفعال کردن موقت سیستم برای بهروزرسانی" />
|
||||
Label="حالت تعمیر و نگهداری — غیرفعال کردن موقت سیستم برای بهروزرسانی" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12">
|
||||
|
||||
@@ -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,6 +27,8 @@
|
||||
<MudSelectItem T="int" Value="2">تحویل پست</MudSelectItem>
|
||||
<MudSelectItem T="int" Value="3">تحویل به مشتری</MudSelectItem>
|
||||
<MudSelectItem T="int" Value="4">مرجوع شده</MudSelectItem>
|
||||
<MudSelectItem T="int" Value="5">لغو شده</MudSelectItem>
|
||||
<MudSelectItem T="int" Value="6">آماده تحویل در دفتر</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
|
||||
@@ -56,6 +58,24 @@
|
||||
</MudItem>
|
||||
}
|
||||
|
||||
@if (_newStatus == 5)
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<MudAlert Severity="Severity.Warning">
|
||||
توجه: وضعیت «لغو شده» یعنی ارسال این سفارش متوقف شده است.
|
||||
</MudAlert>
|
||||
</MudItem>
|
||||
}
|
||||
|
||||
@if (_newStatus == 6)
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<MudAlert Severity="Severity.Info">
|
||||
سفارش برای تحویل حضوری در دفتر آماده است.
|
||||
</MudAlert>
|
||||
</MudItem>
|
||||
}
|
||||
|
||||
@if (_newStatus == 2)
|
||||
{
|
||||
<MudItem xs="12">
|
||||
|
||||
@@ -37,6 +37,8 @@ public partial class ChangeOrderStatusDialog
|
||||
2 => "تحویل پست",
|
||||
3 => "تحویل به مشتری",
|
||||
4 => "مرجوع شده",
|
||||
5 => "لغو شده",
|
||||
6 => "آماده تحویل در دفتر",
|
||||
_ => "بدون ارسال / نامشخص"
|
||||
};
|
||||
}
|
||||
@@ -49,6 +51,8 @@ public partial class ChangeOrderStatusDialog
|
||||
2 => Color.Info,
|
||||
3 => Color.Success,
|
||||
4 => Color.Error,
|
||||
5 => Color.Dark,
|
||||
6 => Color.Primary,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user