Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e89d5e064 | |||
| 8bd961ade1 | |||
| 750d4c6b97 | |||
| 276a4c20f2 | |||
| 98a68074bc |
@@ -1,2 +0,0 @@
|
||||
# Keep production config from current branch during merges — never overwrite
|
||||
src/FrontOffice.Main/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\FrontOffice.Main\FrontOffice.Main.csproj"
|
||||
- name: Build
|
||||
run: dotnet build ".\src\FrontOffice.Main\FrontOffice.Main.csproj" --configuration Release --no-restore
|
||||
- name: Test
|
||||
run: dotnet test ".\src\FrontOffice.Main\FrontOffice.Main.csproj" --no-restore --verbosity normal
|
||||
- name: Recycle Apppool
|
||||
run: |
|
||||
& "C:\Windows\System32\inetsrv\appcmd.exe" recycle apppool /apppool.name:kbs1.ir
|
||||
shell: powershell
|
||||
- name: Stop Website
|
||||
run: |
|
||||
& "C:\Windows\System32\inetsrv\appcmd.exe" stop site /site.name:kbs1.ir
|
||||
shell: powershell
|
||||
- name: Publish
|
||||
run: dotnet publish ".\src\FrontOffice.Main\FrontOffice.Main.csproj" -c Release -o publish
|
||||
- name: Copy Publish To IIS Directory
|
||||
run: Get-ChildItem -Path "publish\*" | Copy-Item -Destination "E:\kbs1.ir\kbs1.ir\" -Recurse -Force
|
||||
- name: Start Website
|
||||
run: |
|
||||
& "C:\Windows\System32\inetsrv\appcmd.exe" start site /site.name:kbs1.ir
|
||||
shell: powershell
|
||||
@@ -1,82 +0,0 @@
|
||||
name: Build and Deploy to Kubernetes
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- kub-stage
|
||||
|
||||
env:
|
||||
REGISTRY: 194.5.195.53:30080
|
||||
IMAGE_NAME: admin/frontoffice
|
||||
K8S_SERVER: 194.5.195.53
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: 194.5.195.53:32082/docker-sshpass:latest
|
||||
options: --privileged
|
||||
steps:
|
||||
- name: Start Docker daemon
|
||||
run: |
|
||||
mkdir -p /etc/docker
|
||||
cat > /etc/docker/daemon.json << 'DAEMON'
|
||||
{
|
||||
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
|
||||
}
|
||||
DAEMON
|
||||
echo "🚀 Starting Docker daemon..."
|
||||
dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs &
|
||||
|
||||
# Wait up to 3 minutes for Docker to be ready
|
||||
for i in $(seq 1 90); do
|
||||
if docker info >/dev/null 2>&1; then
|
||||
echo "✅ Docker daemon is ready (attempt $i)"
|
||||
docker version
|
||||
break
|
||||
else
|
||||
echo "⏳ Waiting for Docker daemon... (attempt $i/90)"
|
||||
sleep 2
|
||||
fi
|
||||
done
|
||||
|
||||
# Final check
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
echo "❌ Docker daemon failed to start after 3 minutes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout code
|
||||
run: |
|
||||
git clone --depth 1 --branch kub-stage http://gitea-svc:3000/admin/FrontOffice.git .
|
||||
|
||||
- name: Login to Docker registries
|
||||
run: |
|
||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 194.5.195.53:32082 -u admin --password-stdin
|
||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
|
||||
|
||||
- name: Build Docker Image
|
||||
run: |
|
||||
cd src
|
||||
DOCKER_BUILDKIT=0 docker build --network host -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest -f FrontOffice.Main/Dockerfile .
|
||||
|
||||
- name: Push to Registry
|
||||
run: |
|
||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
- name: Deploy to Kubernetes
|
||||
run: |
|
||||
export SSHPASS="${{ secrets.SERVER_PASSWORD }}"
|
||||
|
||||
# Copy K8s manifests to server
|
||||
sshpass -e scp -o StrictHostKeyChecking=no k8s/staging/frontoffice-deployment.yaml root@${{ env.K8S_SERVER }}:/tmp/frontoffice-deployment.yaml
|
||||
|
||||
# Apply manifests and restart
|
||||
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
|
||||
kubectl apply -f /tmp/frontoffice-deployment.yaml &&
|
||||
crictl rmi ${REGISTRY}/${IMAGE_NAME}:latest 2>/dev/null || true &&
|
||||
kubectl rollout restart deployment/frontoffice &&
|
||||
kubectl rollout status deployment/frontoffice --timeout=180s &&
|
||||
rm -f /tmp/frontoffice-deployment.yaml
|
||||
"
|
||||
echo "✅ Deployed!"
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
env:
|
||||
REGISTRY: 194.5.195.53:30080
|
||||
IMAGE_NAME: admin/frontoffice
|
||||
K8S_SERVER: 194.5.195.53
|
||||
K8S_SERVER: 45.149.79.127
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
done
|
||||
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
echo "❌ Docker daemon failed to start after 3 minutes"
|
||||
echo "❌ Docker daemon failed to start"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -56,9 +56,10 @@ jobs:
|
||||
- name: Build Docker Image
|
||||
run: |
|
||||
cd src
|
||||
DOCKER_BUILDKIT=0 docker build --network host -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
|
||||
DOCKER_BUILDKIT=0 docker build --network host -f FrontOffice.Main/Dockerfile \
|
||||
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
|
||||
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod \
|
||||
-f FrontOffice.Main/Dockerfile .
|
||||
.
|
||||
|
||||
- name: Push to Registry
|
||||
run: |
|
||||
@@ -68,15 +69,8 @@ jobs:
|
||||
- name: Deploy to Production
|
||||
run: |
|
||||
export SSHPASS="${{ secrets.SERVER_PASSWORD }}"
|
||||
|
||||
# Copy K8s manifests to server
|
||||
sshpass -e scp -o StrictHostKeyChecking=no k8s/production/frontoffice-deployment.yaml root@${{ env.K8S_SERVER }}:/tmp/frontoffice-deployment.yaml
|
||||
|
||||
# Apply manifests and restart
|
||||
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
|
||||
kubectl apply -f /tmp/frontoffice-deployment.yaml &&
|
||||
kubectl rollout restart deployment/frontoffice &&
|
||||
kubectl rollout status deployment/frontoffice --timeout=300s &&
|
||||
rm -f /tmp/frontoffice-deployment.yaml
|
||||
kubectl set image deployment/frontoffice frontoffice=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
|
||||
kubectl rollout status deployment/frontoffice --timeout=300s
|
||||
"
|
||||
echo "✅ Deployed to Production!"
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
|
||||
+310
-3
@@ -1,4 +1,311 @@
|
||||
# Docs moved to totalDoc
|
||||
# 📋 تاریخچه تغییرات FrontOffice — کارا بازار سلامت
|
||||
|
||||
See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation.
|
||||
|
||||
> **آخرین بروزرسانی**: اسفند ۱۴۰۴ (فوریه ۲۰۲۶)
|
||||
> **فریمورک**: Blazor Server + MudBlazor 8.14 + .NET 9
|
||||
|
||||
---
|
||||
|
||||
## 🔖 نسخه ۲.۷.۰ — اسفند ۱۴۰۴ (February 17, 2026)
|
||||
|
||||
### ۱. اجبار تخفیف ۱۰۰٪ و حذف اسلایدر
|
||||
|
||||
- **حذف** `MudSlider` و `MudNumericField` از `Checkout.razor` — کاربر دیگه درصد تخفیف انتخاب نمیکنه
|
||||
- **حذف** کامل بخش نمایش موجودی تخفیفی
|
||||
- بکند همیشه حداکثر تخفیف (`MaxDiscountPercent`) رو اعمال میکنه
|
||||
|
||||
### ۲. رفع متن Badge تخفیف
|
||||
|
||||
- **مشکل:** Badge روی محصولات "۱۰۰٪ تخفیفی" نشون میداد — گمراهکننده
|
||||
- **رفع:** نمایش درصد واقعی محصول (مثلاً "۳۰٪ تخفیف")
|
||||
- **فایلها:** `Products.razor`, `Cart.razor`, `ProductDetail.razor`
|
||||
|
||||
### ۳. نمایش جدول مالیات (VAT) در Checkout
|
||||
|
||||
- جمع کل، تخفیف، مبلغ پس از تخفیف، مالیات ۹٪، مبلغ قابل پرداخت
|
||||
- استفاده از `VatCalculator` برای محاسبه VAT
|
||||
|
||||
### ۴. نمایش وضعیت پرداخت در سفارشات
|
||||
|
||||
- **فیلد جدید** `payment_status` در proto `discountorder.proto` (v0.0.179)
|
||||
- **صفحه Orders:** نمایش Chip رنگی بر اساس PaymentStatus (موفق/ناموفق/در انتظار)
|
||||
- **صفحه OrderDetail:** Alert برای سفارشات ناموفق + دکمه بازگشت + جزئیات وضعیت
|
||||
- **Helper متدها:** `GetPaymentStatusText()`, `GetPaymentStatusColor()`, `GetDeliveryStatusText()`
|
||||
|
||||
### ۵. فیکس مپینگ PaymentStatus و DeliveryStatus
|
||||
|
||||
- **مشکل:** Domain enum مقادیر متفاوتی از Proto داشت — مستقیم cast میشد
|
||||
- **رفع:** `MapPaymentStatus()` و `MapDeliveryStatus()` در `DiscountOrderService.cs` (WebApi)
|
||||
- سفارشات ناموفق حالا `DeliveryStatus=Cancelled` دارن (بجای "در حال پردازش")
|
||||
|
||||
### ۶. فیکس Proto به NuGet v0.0.179
|
||||
|
||||
- `FrontOffice.Main.csproj`: `PackageReference` به `Foursat.CMSMicroservice.Protobuf` v0.0.179
|
||||
|
||||
---
|
||||
|
||||
## 🔖 نسخه ۲.۶.۰ — بهمن ۱۴۰۴ (February 16, 2026)
|
||||
|
||||
### ۱. درگاه پرداخت زرینپال
|
||||
|
||||
- **یکپارچهسازی ZarinPal** — پرداخت مستقیم بدون PYMS واسط
|
||||
- صفحه checkout فروشگاه تخفیفی: ریدایرکت به ZarinPal → callback → تأیید
|
||||
- صفحه checkout فروشگاه عادی: همان جریان
|
||||
- پشتیبانی از sandbox و production
|
||||
|
||||
### ۲. جدول PaymentTransaction
|
||||
|
||||
- **Entity جدید** `PaymentTransaction` — ذخیره جزئیات سطح درگاه (Authority, CardPan, CardHash, RefId)
|
||||
- جدا از جدول Transaction اصلی
|
||||
- Migration: `AddPaymentTransactionTable`
|
||||
|
||||
### ۳. فیکس نمایش وضعیت پرداخت
|
||||
|
||||
- **مشکل:** سفارشات تخفیفی «در انتظار پرداخت» نشان میدادند حتی بعد از پرداخت موفق
|
||||
- **علت:** Mapster نمیتونست `PaymentStatus` (enum) رو به `payment_completed` (bool) مپ کنه
|
||||
- **رفع:** مپینگ دستی در `DiscountOrderService`
|
||||
|
||||
### ۴. فیکس DeliveryStatus
|
||||
|
||||
- **مشکل:** فروشگاه تخفیفی بعد از پرداخت `InTransit` ست میکرد
|
||||
- **رفع:** تغییر به `Pending` — ادمین باید وضعیت پست رو مشخص کنه
|
||||
|
||||
### ۵. تغییر به NuGet Package (Docker build fix)
|
||||
|
||||
- `ProjectReference` به CMS Protobuf → `PackageReference` (v0.0.178)
|
||||
- Docker build context فقط `src/` داره → مسیر `../../../CMS` قابل دسترسی نیست
|
||||
|
||||
---
|
||||
|
||||
## 🔖 نسخه ۲.۵.۰ — بهمن ۱۴۰۴
|
||||
|
||||
### ۱. محافظت صفحات نیازمند احراز هویت (Auth Guard)
|
||||
|
||||
**فایلهای تغییریافته:**
|
||||
- `Shared/MainLayout.razor.cs`
|
||||
|
||||
**شرح:**
|
||||
قبلاً هیچ محافظتی در سطح مسیریابی وجود نداشت — کاربر غیرلاگین میتوانست مستقیماً به `/profile/*`، `/commission/*`، `/cart` و... دسترسی پیدا کند.
|
||||
|
||||
**تغییرات:**
|
||||
- متد `EnforceAuthGuardAsync()` اضافه شد — در هر تغییر مسیر و اولین بار رندر اجرا میشود
|
||||
- متد `IsProtectedRoute(path)` مسیرهای محافظتشده را تشخیص میدهد
|
||||
- کاربر غیرلاگین → ریدایرکت به `/` (صفحه اصلی)
|
||||
|
||||
**مسیرهای محافظتشده:**
|
||||
| گروه | مسیرها |
|
||||
|---|---|
|
||||
| پروفایل | `/profile/*` |
|
||||
| کمیسیون | `/commission/*` |
|
||||
| شبکه | `/network/*` |
|
||||
| باشگاه | `/club/*` |
|
||||
| سبد خرید | `/cart`, `/checkout*`, `/orders`, `/order/*`, `/order-tracking/*` |
|
||||
| پکیجها | `/my-packages` |
|
||||
| دروازه | `/my-orders`, `/my-cart` |
|
||||
| فروشگاه تخفیفی | `/discount-store/cart`, `/discount-store/checkout`, `/discount-store/orders`, `/discount-store/order/*` |
|
||||
|
||||
**مسیرهای عمومی:**
|
||||
`/`, `/register`, `/about`, `/faq`, `/contact`, `/blog/*`, `/packages`, `/package/*`, `/products`, `/product/*`, `/categories`, `/stores`, `/discount-store`, `/discount-store/product/*`
|
||||
|
||||
---
|
||||
|
||||
### ۲. استخراج کامپوننت PhoneVerifyForm
|
||||
|
||||
**فایلهای جدید:**
|
||||
- `Shared/PhoneVerifyForm.razor`
|
||||
- `Shared/PhoneVerifyForm.razor.cs`
|
||||
|
||||
**فایلهای تغییریافته:**
|
||||
- `Shared/AuthDialog.razor`
|
||||
- `Shared/AuthDialog.razor.cs`
|
||||
|
||||
**شرح:**
|
||||
فرم تلفن + تایید OTP + کپچا که قبلاً بهصورت `RenderFragment` با `__builder` مستقیم در `AuthDialog` نوشته شده بود، به کامپوننت مستقل `PhoneVerifyForm` استخراج شد.
|
||||
|
||||
**ساختار قبل:**
|
||||
```
|
||||
AuthDialog.razor
|
||||
└── @code { PhoneOrVerifyContent() => __builder => { ... } } ← RenderFragment پیچیده
|
||||
```
|
||||
|
||||
**ساختار بعد:**
|
||||
```
|
||||
AuthDialog.razor
|
||||
└── <PhoneVerifyForm @ref="_phoneVerifyForm" ... /> ← کامپوننت مستقل
|
||||
PhoneVerifyForm.razor ← مارکاپ فرم
|
||||
PhoneVerifyForm.razor.cs ← پارامترها و فرم رفها
|
||||
```
|
||||
|
||||
**پارامترهای PhoneVerifyForm:**
|
||||
|
||||
| پارامتر | نوع | توضیح |
|
||||
|---|---|---|
|
||||
| `CurrentStep` | `AuthStep` | مرحله فعلی (Phone / Verify) |
|
||||
| `PhoneRequest` | `CreateNewOtpTokenRequest` | مدل فرم تلفن |
|
||||
| `VerifyRequest` | `VerifyOtpTokenRequest` | مدل فرم تایید |
|
||||
| `CaptchaCode` | `string?` | کد کپچا نمایشدادهشده |
|
||||
| `CaptchaInput` / `CaptchaInputChanged` | `string?` + `EventCallback` | ورودی کپچا (two-way) |
|
||||
| `OnRefreshCaptcha` | `EventCallback` | رفرش کپچا |
|
||||
| `IsBusy` | `bool` | وضعیت بارگذاری |
|
||||
| `ErrorMessage` / `InfoMessage` | `string?` | پیامهای خطا/اطلاع |
|
||||
| `PhoneNumber` | `string?` | شماره تاییدشده |
|
||||
| `ResendRemaining` | `int` | ثانیه تا ارسال مجدد |
|
||||
| `OnChangePhone` / `OnResendOtp` | `EventCallback` | اکشنهای تایید |
|
||||
|
||||
**نکته مهم — Two-way binding کپچا:**
|
||||
فیلد کپچا با `Value` + `ValueChanged` بایند شده (نه `@bind-Value`) تا مقدار تایپشده از فرزند به والد برگردد:
|
||||
```razor
|
||||
<MudTextField Value="CaptchaInput"
|
||||
ValueChanged="@((string v) => CaptchaInputChanged.InvokeAsync(v))" ... />
|
||||
```
|
||||
|
||||
**دسترسی به فرمها از والد:**
|
||||
```csharp
|
||||
// AuthDialog.razor.cs
|
||||
var phoneForm = _phoneVerifyForm?.GetPhoneForm();
|
||||
var verifyForm = _phoneVerifyForm?.GetVerifyForm();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۳. بهبود لایوت مدال ورود (AuthDialog)
|
||||
|
||||
**فایلهای تغییریافته:**
|
||||
- `Shared/AuthDialog.razor`
|
||||
- `Utilities/AuthDialogService.cs`
|
||||
- `wwwroot/css/site.css`
|
||||
|
||||
**مشکلات قبلی:**
|
||||
- دیالوگ روی موبایل `FullScreen` بود → فضای خالی بزرگ بین فرم و دکمهها
|
||||
- `TitleContent` و `DialogActions` جدا → گپ عمودی
|
||||
- ردیف کپچا با `MudStack Row="true"` → آیتمها عمودی رندر میشدند
|
||||
|
||||
**تغییرات:**
|
||||
1. **حذف FullScreen**: `AuthDialogService` حالا همیشه `MaxWidth.ExtraSmall, FullWidth=true, CloseButton=true`
|
||||
2. **ادغام محتوا**: آواتار + عنوان + فرم + دکمهها همه داخل `DialogContent` → بدون `TitleContent` و `DialogActions`
|
||||
3. **کلاس `auth-dialog-wrapper`**: CSS با `.auth-dialog-wrapper .mud-dialog-title { display: none; }` عنوان پیشفرض دیالوگ رو مخفی میکنه
|
||||
4. **ردیف کپچا**: از `MudStack Row` به `div.captcha-row` با CSS flex اختصاصی
|
||||
5. **حالت Inline**: بدون تغییر ساختاری — فقط از `PhoneVerifyForm` استفاده میکنه
|
||||
|
||||
**CSS جدید:**
|
||||
```css
|
||||
.auth-dialog-wrapper .mud-dialog-title { display: none; }
|
||||
.auth-dialog-wrapper .mud-dialog-content { padding-bottom: 24px !important; }
|
||||
.auth-dialog-content { max-width: 400px; margin: 0 auto; }
|
||||
|
||||
.captcha-row { display: flex; align-items: center; gap: 10px; }
|
||||
.captcha-row > .mud-input-control { flex: 1 1 0; min-width: 0; }
|
||||
.captcha-row > .mud-paper { flex: 0 0 auto; }
|
||||
.captcha-row > .mud-button-root { flex: 0 0 auto; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۴. صفحه لندینگ — حذف /pricing و اضافه بنر بلاگ
|
||||
|
||||
**فایلهای تغییریافته:**
|
||||
- `Pages/Index.razor`
|
||||
- `wwwroot/css/site.css`
|
||||
|
||||
**تغییرات:**
|
||||
1. دکمه هیرو «مشاهده پکیجها» → **«آخرین اخبار»** با لینک `/blog`
|
||||
2. **بنر جدیدترین مطلب** بین هیرو و «سه گام تا شروع» اضافه شد
|
||||
- تصویر بندانگشتی + عنوان + خلاصه + آیکون شیشهای
|
||||
- هاور: `translateY(-2px)` + سایه بزرگتر
|
||||
|
||||
**CSS جدید:**
|
||||
```css
|
||||
.landing-blog-banner { border: 1px solid var(--mud-palette-divider); transition: ... }
|
||||
.landing-blog-banner:hover { box-shadow: var(--mud-elevation-4); transform: translateY(-2px); }
|
||||
.landing-blog-thumb { width: 80px; height: 80px; border-radius: 12px; }
|
||||
.landing-blog-title { -webkit-line-clamp: 1; font-weight: 600; }
|
||||
.landing-blog-summary { -webkit-line-clamp: 1; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۵. افزایش ارتفاع تکستباکسها (Global)
|
||||
|
||||
**فایل تغییریافته:**
|
||||
- `wwwroot/css/site.css`
|
||||
|
||||
**قبل:** `padding: 10px 14px`
|
||||
**بعد:** `padding: 14px 14px` + `font-size: 1rem`
|
||||
|
||||
```css
|
||||
.mud-input-outlined .mud-input-slot {
|
||||
padding: 14px 14px !important;
|
||||
font-size: 1rem;
|
||||
}
|
||||
```
|
||||
|
||||
تمام فیلدهای Outlined در کل اپلیکیشن بزرگتر شدند.
|
||||
|
||||
---
|
||||
|
||||
### ۶. اصلاح RTL فیلدهای ورودی
|
||||
|
||||
**فایل تغییریافته:**
|
||||
- `wwwroot/css/site.css`
|
||||
|
||||
**مشکل:** فیلدهای `type="tel"` بهصورت پیشفرض مرورگر `direction: ltr` میگیرن — لیبل سمت راست ولی placeholder/cursor سمت چپ.
|
||||
|
||||
**اصلاح:**
|
||||
```css
|
||||
.mud-input-slot input,
|
||||
.mud-input-slot textarea {
|
||||
direction: rtl !important;
|
||||
text-align: right !important;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۷. اصلاح captcha-box CSS
|
||||
|
||||
**فایل تغییریافته:**
|
||||
- `wwwroot/css/site.css`
|
||||
|
||||
**قبل:**
|
||||
```css
|
||||
.captcha-box {
|
||||
min-width: 120px; min-height: 56px;
|
||||
background: linear-gradient(135deg, rgba(123,97,255,.12), rgba(255,140,189,.12));
|
||||
}
|
||||
```
|
||||
|
||||
**بعد:**
|
||||
```css
|
||||
.captcha-box {
|
||||
min-width: 96px; min-height: 48px;
|
||||
border-radius: var(--mud-default-borderradius);
|
||||
background: rgba(99,102,241,.08);
|
||||
border: 1px solid var(--mud-palette-divider);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 نقشه فایلها
|
||||
|
||||
```
|
||||
Shared/
|
||||
├── AuthDialog.razor ← بازنویسی (کامپوننت PhoneVerifyForm جایگزین RenderFragment)
|
||||
├── AuthDialog.razor.cs ← بروزرسانی (استفاده از _phoneVerifyForm)
|
||||
├── PhoneVerifyForm.razor ← ✨ جدید (فرم تلفن + تایید + کپچا)
|
||||
├── PhoneVerifyForm.razor.cs ← ✨ جدید (پارامترها و فرم رفها)
|
||||
├── MainLayout.razor.cs ← Auth Guard اضافه شد
|
||||
Pages/
|
||||
├── Index.razor ← /pricing → /blog + بنر بلاگ
|
||||
Utilities/
|
||||
├── AuthDialogService.cs ← حذف FullScreen، ثابتسازی سایز
|
||||
wwwroot/css/
|
||||
├── site.css ← captcha-row, auth-dialog, RTL fix, input height
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 وضعیت بیلد
|
||||
|
||||
| تاریخ | خطا | هشدار | توضیح |
|
||||
|---|---|---|---|
|
||||
| بهمن ۱۴۰۴ | **۰** | ۱۰۵ | MUD0002 warnings (بیخطر — مربوط به MudBlazor analyzer) |
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
# سرویسهای FrontOffice (سمت مشتری)
|
||||
|
||||
این سند تمام سرویسهایی را که **FrontOffice** (فرانت مشتری) به **CMS** از طریق gRPC Gateway (`GW_URL`) فراخوانی میکند، به ترتیب **Use Case** فهرست میکند.
|
||||
|
||||
- **مسیر کد سرویسها:** `FrontOffice/src/FrontOffice.Main/Utilities/`
|
||||
- **ثبت gRPC:** `FrontOffice/src/FrontOffice.Main/ConfigureServices.cs`
|
||||
- **تاریخ:** ۱۴۰۵/۰۳/۱۷
|
||||
|
||||
---
|
||||
|
||||
## ۱. احراز هویت و ثبتنام
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 1.1 | `AuthService` — `RefreshTokenAsync` | `UserContract.RefreshToken` | `/profile` (`Profile/Index`) | تمدید JWT پس از بازگشت از درگاه پرداخت |
|
||||
| 1.2 | `AuthService` — `InitUserAuthInfo` / claims | — (local JWT parse) | سراسری | خواندن UserId، نام، کدملی، وضعیت قرارداد/پکیج/باشگاه از توکن |
|
||||
| 1.3 | `UserContract.CreateNewOtpToken` | `UserContract` | `Shared/AuthDialog` | ارسال OTP ورود/ثبتنام با موبایل |
|
||||
| 1.4 | `UserContract.VerifyOtpToken` | `UserContract` | `Shared/AuthDialog` | تأیید OTP و دریافت JWT |
|
||||
| 1.5 | `UserContract.CreateNewOtpToken` (Purpose=`signContract`) | `UserContract` | `/register` (`RegisterWizard`) | ارسال OTP برای امضای قرارداد اصلی |
|
||||
| 1.6 | `UserContract.AcceptContract` | `UserContract` | `/register` (`RegisterWizard`) | ثبت امضای قرارداد اصلی + دریافت توکن بهروز |
|
||||
| 1.7 | `UserContract.GetUserForCustomer` | `UserContract` | `/register`, `/profile`, `/profile/personal`, `/profile/settings` | بارگذاری پروفایل موجود کاربر |
|
||||
| 1.8 | `UserContract.UpdateCustomerProfile` | `UserContract` | `/register`, `/profile`, `/profile/personal` | ذخیره نام، نامخانوادگی، کدملی و… |
|
||||
| 1.9 | `AuthDialogService.ShowAuthDialogAsync` | — (UI) | فروشگاه، سبد، checkout | نمایش دیالوگ ورود برای مهمان |
|
||||
| 1.10 | `GuestActionGate` | — (wrapper روی Auth) | `/`, `/products`, `/product/{id}`, Discount Store | جلوگیری از افزودن به سبد بدون لاگین |
|
||||
|
||||
---
|
||||
|
||||
## ۲. پروفایل و آدرس
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 2.1 | `UserContract.GetUserForCustomer` | `UserContract` | `/profile`, `/profile/personal`, `/profile/settings` | نمایش و ویرایش اطلاعات شخصی |
|
||||
| 2.2 | `UserContract.GetUser` | `UserContract` | `/profile/hub`, `Club/ActivationSection` | اطلاعات کامل کاربر (هاب پروفایل / پس از فعالسازی باشگاه) |
|
||||
| 2.3 | `UserContract.UpdateUser` | `UserContract` | `/profile/settings` | بهروزرسانی تنظیمات حساب |
|
||||
| 2.4 | `UserAddressContract.GetCustomerAddresses` | `UserAddressContract` | `/profile`, `/profile/addresses`, `/checkout`, Store/Discount checkout | لیست آدرسهای تحویل |
|
||||
| 2.5 | `UserAddressContract.CreateCustomerAddress` | `UserAddressContract` | `Profile/Components/AddAddressDialog` | ثبت آدرس جدید |
|
||||
| 2.6 | `UserAddressContract.UpdateCustomerAddress` | `UserAddressContract` | `Profile/Components/EditAddressDialog` | ویرایش آدرس |
|
||||
| 2.7 | `UserAddressContract.DeleteCustomerAddress` / `DeleteUserAddress` | `UserAddressContract` | `/profile/addresses`, `/profile` | حذف آدرس |
|
||||
| 2.8 | `UserAddressContract.SetCustomerDefaultAddress` / `SetAddressAsDefault` | `UserAddressContract` | `/profile/addresses`, `/profile`, `/checkout` | تنظیم آدرس پیشفرض |
|
||||
| 2.9 | `CityContract.GetAllCitiesByFilter` | `CityContract` | `AddAddressDialog`, `EditAddressDialog` | جستجو و انتخاب شهر/استان |
|
||||
|
||||
---
|
||||
|
||||
## ۳. پکیج و خرید عضویت
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 3.1 | `PackageService.GetAllPackagesAsync` | `PackageContract.GetCustomerPackages` | `/packages`, `/my-packages`, `/`, `/checkout`, `PackagePurchaseDialog`, `ClubMembershipContractDialog`, Commission pages | لیست پکیجهای قابل خرید |
|
||||
| 3.2 | `PackageService.GetPackageByIdAsync` | `PackageContract.GetCustomerPackageDetails` | (از طریق wrapper؛ صفحه جزئیات مستقیم RPC دارد) | جزئیات یک پکیج |
|
||||
| 3.3 | `PackageContract.GetCustomerPackageDetails` | `PackageContract` | `/package/{id}` (`PackageDetail`) | جزئیات، ویژگیها و مشخصات پکیج |
|
||||
| 3.4 | `PackageService.GetUserPackageStatusAsync` | `PackageContract.GetUserPackageStatus` | `/packages`, `/my-packages`, `/profile` | وضعیت خرید پکیج، باشگاه، موجودی کیف پول |
|
||||
| 3.5 | `PackageContract.CustomerPurchasePackage` | `PackageContract` | `/profile` (`ProcessDirectPayment`), `/checkout` | شروع پرداخت درگاهی (Zarinpal) برای خرید پکیج |
|
||||
| 3.6 | `PackageContract.CustomerVerifyPackagePurchase` | `PackageContract` | `/profile/payment-callback?type=package` | تأیید پرداخت پکیج پس از بازگشت از درگاه |
|
||||
| 3.7 | `PackagePurchaseDialog` | — (UI + `PackageService`) | `/profile`, `/my-packages` | انتخاب پکیج و روش پرداخت (درگاه / Daya) |
|
||||
| 3.8 | Daya Loan (external URL) | — | `/profile` | هدایت به `dayadiamond.ir` برای خرید اقساطی (بدون RPC) |
|
||||
|
||||
---
|
||||
|
||||
## ۴. کیف پول، شارژ و برداشت
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 4.1 | `WalletService.GetBalancesAsync` | `UserWalletContract.GetCustomerWallet` | `/profile`, `/profile/wallet`, Store checkout, PaymentCallback | موجودی اصلی، تخفیفی و شبکه |
|
||||
| 4.2 | `WalletService.GetTransactionsAsync` | `UserWalletContract.GetCustomerWalletHistory` | `/profile/wallet` | تاریخچه تراکنشهای کیف پول |
|
||||
| 4.3 | `WalletService.GetMagicWalletStatusAsync` | `UserWalletContract.GetMagicWalletStatus` | `/profile`, `/profile/magic-wallet`, `/my-packages` | وضعیت کیف پول جادویی (Magic) |
|
||||
| 4.4 | `WalletService.InitiateMagicChargeAsync` | `UserWalletContract.InitiateMagicCharge` | `/profile/magic-wallet` | شروع شارژ کیف پول جادویی → درگاه |
|
||||
| 4.5 | `WalletService.VerifyMagicChargeAsync` | `UserWalletContract.VerifyMagicCharge` | `/profile/payment-callback?type=magic-wallet` | تأیید شارژ Magic پس از درگاه |
|
||||
| 4.6 | `WalletService.InitiateDiscountChargeAsync` | `UserWalletContract.InitiateDiscountCharge` | `/profile/charge-discount-wallet` | شروع شارژ کیف پول تخفیفی → درگاه |
|
||||
| 4.7 | `WalletService.VerifyDiscountChargeAsync` | `UserWalletContract.VerifyDiscountCharge` | `/profile/payment-callback?type=discount-wallet` | تأیید شارژ کیف تخفیفی |
|
||||
| 4.8 | `WalletService.InitiateCreditChargeAsync` | `UserWalletContract.InitiateCreditCharge` | `/profile/charge-credit-wallet` | شروع شارژ کیف پول اصلی → درگاه |
|
||||
| 4.9 | `WalletService.VerifyCreditChargeAsync` | `UserWalletContract.VerifyCreditCharge` | `/profile/payment-callback?type=credit-wallet` | تأیید شارژ کیف اصلی |
|
||||
| 4.10 | `WalletService.GetWithdrawalsAsync` | `UserWalletContract.GetCustomerWithdrawals` | `/profile/withdrawal-requests` | لیست درخواستهای برداشت |
|
||||
| 4.11 | `WalletService.GetWithdrawalSettingsAsync` | `UserWalletContract.GetCustomerWithdrawalSettings` | `/profile/withdrawal-requests` | حداقل مبلغ برداشت |
|
||||
| 4.12 | `WalletService.RequestWithdrawalAsync` | `UserWalletContract.CustomerWithdrawBalance` | `/profile/withdrawal-requests` | ثبت درخواست برداشت پاداش (با payoutId کمیسیون) |
|
||||
| 4.13 | `CommissionService.GetWithdrawablePayoutsAsync` | `CommissionContract.GetMyCommissionPayouts` | `/profile/withdrawal-requests` | پاداشهای قابل برداشت برای انتخاب |
|
||||
|
||||
---
|
||||
|
||||
## ۵. Callback پرداخت (مشترک)
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 5.1 | `PackageContract.CustomerVerifyPackagePurchase` | `PackageContract` | `/profile/payment-callback` (type=package) | تأیید خرید پکیج |
|
||||
| 5.2 | `WalletService.VerifyMagicChargeAsync` | `UserWalletContract` | `/profile/payment-callback` (type=magic-wallet) | تأیید شارژ Magic |
|
||||
| 5.3 | `WalletService.VerifyDiscountChargeAsync` | `UserWalletContract` | `/profile/payment-callback` (type=discount-wallet) | تأیید شارژ تخفیفی |
|
||||
| 5.4 | `WalletService.VerifyCreditChargeAsync` | `UserWalletContract` | `/profile/payment-callback` (type=credit-wallet) | تأیید شارژ کیف اصلی |
|
||||
| 5.5 | `DiscountOrderService.VerifyDiscountOrderPaymentAsync` | `DiscountOrderContract.CustomerVerifyDiscountOrderPayment` | `/profile/payment-callback` (type=discount-order) | تأیید پرداخت سفارش فروشگاه تخفیفی |
|
||||
| 5.6 | `UserContract.GetUserForCustomer` + `AuthService.InitUserAuthInfo` | `UserContract` | `/profile/payment-callback` | بهروزرسانی claims کاربر پس از پرداخت موفق |
|
||||
|
||||
---
|
||||
|
||||
## ۶. فروشگاه اصلی (Store)
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 6.1 | `ProductService.GetProductsPagedAsync` | `ProductsContract.GetProductsForCustomer` | `/products` | لیست محصولات با فیلتر/صفحهبندی |
|
||||
| 6.2 | `ProductService.GetTopSellingAsync` | `ProductsContract` | `/` (صفحه اصلی) | پرفروشترین محصولات |
|
||||
| 6.3 | `ProductService.GetByIdAsync` | `ProductsContract.GetProductByIdForCustomer` | `/product/{id}` | جزئیات محصول + گالری |
|
||||
| 6.4 | `CategoryService.GetAllAsync` / `GetByIdAsync` | `CategoryContract.GetAllCategoriesForCustomer` | `/categories`, `/products` | درخت دستهبندی محصولات |
|
||||
| 6.5 | `CartService` — Add/Update/Remove/Get | `UserCartsContract.*ForCustomer` | `/`, `/products`, `/product/{id}`, `/cart` | مدیریت سبد خرید (AddNew, Update, Remove, GetCustomerCart) |
|
||||
| 6.6 | `UserOrderContract.SubmitShopBuyOrder` | `UserOrderContract` | `/checkout-summary` | ثبت سفارش و پرداخت از کیف پول |
|
||||
| 6.7 | `OrderService.GetOrdersAsync` | `UserOrderContract.GetCustomerOrders` | `/orders` | لیست سفارشهای کاربر |
|
||||
| 6.8 | `OrderService.GetOrderAsync` | `UserOrderContract.GetCustomerOrder` | `/order/{id}`, `/order-tracking/{id}` | جزئیات یک سفارش |
|
||||
| 6.10 | `VATService.GetRateAsync` | `UserOrderContract` (via scope) | Store/Discount صفحات قیمت | نرخ مالیات بر ارزش افزوده (کش روزانه) |
|
||||
| 6.11 | `WalletService.GetBalancesAsync` | `UserWalletContract` | `/checkout-summary` | بررسی موجودی برای پرداخت سفارش |
|
||||
|
||||
---
|
||||
|
||||
## ۷. فروشگاه تخفیفی (Discount Store)
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 7.1 | `DiscountProductService.GetProductsAsync` | `DiscountProductContract.GetDiscountProducts` | `/discount-store` | لیست محصولات تخفیفی |
|
||||
| 7.2 | `DiscountProductService.GetTopSellingAsync` | `DiscountProductContract` | `/` | پرفروشهای فروشگاه تخفیفی |
|
||||
| 7.3 | `DiscountProductService.GetByIdAsync` | `DiscountProductContract.GetDiscountProductById` + Images | `/discount-store/product/{id}` | جزئیات محصول تخفیفی |
|
||||
| 7.4 | `DiscountProductService.GetCategoriesAsync` | `DiscountCategoryContract.GetDiscountCategories` | `/discount-store` | دستهبندیهای فروشگاه تخفیفی |
|
||||
| 7.5 | `DiscountCartService` — Add/Update/Remove/Get | `DiscountShoppingCartContract.*` | `/discount-store`, `/discount-store/cart` | سبد خرید تخفیفی |
|
||||
| 7.6 | `DiscountOrderService.PlaceOrderAsync` | `DiscountOrderContract.PlaceOrder` | `/discount-store/checkout` | ثبت سفارش (کیف تخفیفی + درگاه) |
|
||||
| 7.7 | `DiscountOrderService.GetUserOrdersAsync` | `DiscountOrderContract.GetUserOrders` | `/discount-store/orders` | لیست سفارشهای تخفیفی |
|
||||
| 7.8 | `DiscountOrderService.GetOrderByIdAsync` | `DiscountOrderContract.GetOrderById` | `/discount-store/order/{id}` | جزئیات سفارش تخفیفی |
|
||||
| 7.9 | `UserAddressContract.GetCustomerAddresses` | `UserAddressContract` | `/discount-store/checkout` | انتخاب آدرس تحویل |
|
||||
|
||||
---
|
||||
|
||||
## ۸. باشگاه مشتریان (Club)
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 8.1 | `ClubMembershipService.GetMyMembershipAsync` | `ClubMembershipContract.GetClubMembership` | `/club/membership` | وضعیت عضویت باشگاه |
|
||||
| 8.2 | `ClubConfigurationService.GetClubConfigurationAsync` | `ConfigurationContract.GetClubConfiguration` | `/club/membership` | هزینه فعالسازی و هدیه عضویت |
|
||||
| 8.3 | `ClubConfigurationService.GetClubFeaturesAsync` | `ConfigurationContract.GetClubFeatures` | `/club/features` | لیست امکانات باشگاه |
|
||||
| 8.4 | `ClubMembershipService.ActivateMembershipAsync` | `ClubMembershipContract.ActivateClubMembership` | `Club/ActivationSection` | فعالسازی/تمدید عضویت باشگاه |
|
||||
| 8.5 | `OtpTokenContract.CreateNewOtpToken` | `OtpTokenContract` | `ClubMembershipContractDialog` | OTP برای امضای قرارداد باشگاه |
|
||||
| 8.6 | `ClubMembershipContract.AcceptClubMembershipContract` | `ClubMembershipContract` | `ClubMembershipContractDialog` | ثبت امضای قرارداد باشگاه |
|
||||
|
||||
---
|
||||
|
||||
## ۹. شبکه فروش (Network / MLM)
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 9.1 | `NetworkMembershipService.GetMyNetworkTreeAsync` | `NetworkMembershipContract.GetMyNetworkTree` | `/profile/tree` (`OrganizationChart`) | درخت سازمان فروش کاربر |
|
||||
| 9.2 | `NetworkMembershipService.GetSubordinateTreeAsync` | `NetworkMembershipContract.GetSubordinateTree` | `/profile/tree` | drill-down به زیرمجموعه |
|
||||
| 9.3 | `NetworkMembershipService.GetMyNetworkStatisticsAsync` | `NetworkMembershipContract.GetMyNetworkStatistics` | `/profile/tree`, `/network/statistics` | آمار پا چپ/راست، تعداد اعضا |
|
||||
|
||||
---
|
||||
|
||||
## ۱۰. کمیسیون و پاداش
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 10.1 | `CommissionService.GetWeekDefinitionsAsync` | `CommissionContract.GetWeekDefinitions` | `Shared/WeekSelector`, Commission pages | لیست هفتههای محاسبه پاداش |
|
||||
| 10.2 | `CommissionService.GetMyCommissionPayoutsAsync` | `CommissionContract.GetMyCommissionPayouts` | `/commission/dashboard` | تاریخچه پرداخت پاداشها |
|
||||
| 10.3 | `CommissionService.GetMyWeeklyBalanceAsync` | `CommissionContract.GetMyWeeklyBalances` | `/commission/weekly-balance` | جزئیات بالانس هفتگی (چپ/راست) |
|
||||
| 10.4 | `PackageService.GetAllPackagesAsync` | `PackageContract` | Commission pages | فیلتر پاداش بر اساس پکیج |
|
||||
|
||||
---
|
||||
|
||||
## ۱۱. محتوای سایت، بلاگ و صفحات ثابت
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 11.1 | `SitePageSettingsService.GetPageAsync` | `SitePageSettingsContract.GetPageSettings` | `/` (landing), `/about`, `/contact`, `/licenses` | محتوای داینامیک صفحات (hero، تصاویر، JSON تنظیمات) |
|
||||
| 11.2 | `BlogPostService.GetFeaturedPostsAsync` | `BlogPostContract.GetFeaturedBlogPosts` | `/`, `/profile` | پستهای ویژه |
|
||||
| 11.3 | `BlogPostService.GetPublishedPostsAsync` | `BlogPostContract.GetPublishedBlogPosts` | `/`, `/blog` | لیست پستهای منتشرشده |
|
||||
| 11.4 | `BlogPostService.GetBySlugAsync` | `BlogPostContract.GetBlogPostBySlug` | `/blog/{slug}` | محتوای کامل یک پست |
|
||||
| 11.5 | `BlogPostService.IncrementViewCountAsync` | `BlogPostContract.IncrementViewCount` | `/blog/{slug}` | افزایش شمارنده بازدید |
|
||||
| 11.6 | `BlogCategoryService.GetActiveCategoriesAsync` | `BlogCategoryContract` | `/blog` | فیلتر دستهبندی بلاگ |
|
||||
| 11.7 | FAQ | — (hardcoded) | `/faq` | سوالات متداول — **بدون فراخوانی API** |
|
||||
|
||||
---
|
||||
|
||||
## ۱۲. زیرساخت مشترک (Infrastructure)
|
||||
|
||||
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||
|---|-------------|---------------|-----------------|--------|
|
||||
| 12.1 | `ImageCacheService.ResolveAsync` | `ImageResolverContract` | `Shared/AppImage` (سراسری) | resolve مسیر تصویر CMS به data-URI |
|
||||
| 12.2 | `AppVersionService.CheckVersionAsync` | `AppVersionContract` | `App.razor`, `MainLayout` | بررسی نسخه جدید اپ |
|
||||
| 12.3 | `AppVersionService.ApplyUpdateAsync` / `SkipVersionAsync` | — (localStorage + reload) | `App.razor`, `MainLayout` | اعمال یا رد آپدیت |
|
||||
| 12.4 | `MainService.OnChangeHandler` | — (in-memory) | `/`, `AuthDialog` | اطلاعرسانی تغییر state سراسری (مثلاً پس از login) |
|
||||
| 12.5 | `IChromiumPdfService` | — (local endpoint `/contract/generate`) | `/register` | تولید PDF قرارداد (Chromium headless) |
|
||||
|
||||
---
|
||||
|
||||
## ۱۳. Gateway / انتخابگر
|
||||
|
||||
| صفحه | سرویس | کاربرد |
|
||||
|------|--------|--------|
|
||||
| `/stores` | — | انتخاب بین فروشگاه اصلی و تخفیفی |
|
||||
| `/my-orders` | — | انتخاب لیست سفارشها |
|
||||
| `/my-cart` | — | انتخاب سبد خرید |
|
||||
|
||||
این صفحات فقط مسیریابی UI هستند و مستقیماً gRPC صدا نمیزنند.
|
||||
|
||||
---
|
||||
|
||||
## ۱۴. سرویسهای ثبتشده ولی بدون استفاده در UI
|
||||
|
||||
| سرویس / Contract | وضعیت |
|
||||
|------------------|--------|
|
||||
| `SitePageService` (`SitePageContract`) | ثبت در DI؛ **هیچ صفحهای inject نمیکند** (جایگزین: `SitePageSettingsService`) |
|
||||
| `TransactionsContract` | ثبت در DI؛ **استفاده نشده** |
|
||||
| `UserWalletHistoryContract` | ثبت در DI؛ **استفاده نشده** (تاریخچه از `UserWalletContract.GetCustomerWalletHistory` میآید) |
|
||||
| `TokenNotificationService` | ثبت در DI؛ **هنوز به Layout وصل نشده** (SignalR برای invalidation توکن) |
|
||||
|
||||
---
|
||||
|
||||
## نمودار جریان پرداخت
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph initiate [شروع پرداخت]
|
||||
A1[Profile / Checkout]
|
||||
A2[MagicWallet / ChargeDiscount]
|
||||
A3[Discount Checkout]
|
||||
end
|
||||
|
||||
subgraph gateway [Zarinpal]
|
||||
G[درگاه پرداخت]
|
||||
end
|
||||
|
||||
subgraph callback [PaymentCallback]
|
||||
C1[Verify Package]
|
||||
C2[Verify Magic]
|
||||
C3[Verify Discount Wallet]
|
||||
C4[Verify Discount Order]
|
||||
end
|
||||
|
||||
A1 --> G
|
||||
A2 --> G
|
||||
A3 --> G
|
||||
G --> callback
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## فهرست gRPC Contractهای فعال
|
||||
|
||||
| Contract | Wrapper اصلی |
|
||||
|----------|----------------|
|
||||
| `UserContract` | `AuthService`, صفحات Profile/Register |
|
||||
| `UserAddressContract` | Profile, Checkout pages |
|
||||
| `CityContract` | Address dialogs |
|
||||
| `PackageContract` | `PackageService`, PaymentCallback, Checkout |
|
||||
| `UserWalletContract` | `WalletService` |
|
||||
| `UserOrderContract` | `OrderService`, CheckoutSummary |
|
||||
| `UserCartsContract` | `CartService` |
|
||||
| `ProductsContract` | `ProductService` |
|
||||
| `CategoryContract` | `CategoryService` |
|
||||
| `DiscountProductContract` / `DiscountCategoryContract` | `DiscountProductService` |
|
||||
| `DiscountShoppingCartContract` | `DiscountCartService` |
|
||||
| `DiscountOrderContract` | `DiscountOrderService` |
|
||||
| `ClubMembershipContract` | `ClubMembershipService`, Dialog |
|
||||
| `OtpTokenContract` | Club contract dialog |
|
||||
| `ConfigurationContract` | `ClubConfigurationService` |
|
||||
| `NetworkMembershipContract` | `NetworkMembershipService` |
|
||||
| `CommissionContract` | `CommissionService` |
|
||||
| `BlogPostContract` | `BlogPostService` |
|
||||
| `BlogCategoryContract` | `BlogCategoryService` |
|
||||
| `SitePageSettingsContract` | `SitePageSettingsService` |
|
||||
| `AppVersionContract` | `AppVersionService` |
|
||||
| `ImageResolverContract` | `ImageCacheService` |
|
||||
+541
-2
@@ -1,3 +1,542 @@
|
||||
# Docs moved to totalDoc
|
||||
# 🎨 پلن جامع یکپارچهسازی UI/UX — کارا بازار سلامت
|
||||
|
||||
See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation.
|
||||
> **تاریخ**: بهمن ۱۴۰۴
|
||||
> **وضعیت**: ✅ **تمام ۶ فاز + فاز ۷ (ناوبری و امنیت) تکمیل شد**
|
||||
> **هدف**: یکپارچهسازی طراحی FrontOffice با ۲۰٪ تغییر UX و ۵۰٪ تغییر UI
|
||||
> **فریمورک**: Blazor Server + MudBlazor 8.14
|
||||
> **📋 تاریخچه تغییرات جزئی**: [CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
---
|
||||
|
||||
## 📊 خلاصه اجرایی
|
||||
|
||||
| شاخص | وضعیت قبل | وضعیت بعد |
|
||||
|---|---|---|
|
||||
| **صفحات کل** | ۴۱ صفحه + ۶ کامپوننت مشترک | ۴۱ صفحه + ۸ کامپوننت مشترک |
|
||||
| **الگوی PageHeader** | ۱۹ صفحه از ۴۱ | ۳۲+ صفحه ✅ |
|
||||
| **Inline Style سنگین** | ۱۱ صفحه 🔴 | ≤۲ صفحه (فقط gradientهای تزئینی) ✅ |
|
||||
| **Loading State** | ۲ الگوی متفاوت (Circular vs Linear) | `<LoadingState>` واحد ✅ |
|
||||
| **Empty State** | ۴+ الگوی متناقض | `<EmptyState>` واحد ✅ |
|
||||
| **Container Spacing** | ۶ الگوی متناقض | `py-6` استاندارد ✅ |
|
||||
| **رنگبندی** | رنگهای hardcoded در ۸+ صفحه | CSS Variable ✅ |
|
||||
| **تم پالت** | Primary `#0380C0` (آبی ساده) | Indigo `#6366f1` + Full PaletteDark ✅ |
|
||||
| **Elevation** | مخلوط ۲/۳/۴ | استاندارد ۰–۲ ✅ |
|
||||
| **Build** | ۰ خطا | ۰ خطا ✅ |
|
||||
|
||||
### فازها
|
||||
| فاز | عنوان | وضعیت |
|
||||
|---|---|---|
|
||||
| ۱ | زیرساخت دیزاین سیستم | ✅ تکمیل |
|
||||
| ۲ | صفحات پروفایل | ✅ تکمیل |
|
||||
| ۳ | صفحات تخصصی | ✅ تکمیل |
|
||||
| ۴ | بهبود بصری فروشگاه | ✅ تکمیل |
|
||||
| ۵ | صفحات عمومی | ✅ تکمیل |
|
||||
| ۶ | پالیش و تست | ✅ تکمیل |
|
||||
| ۷ | ناوبری، امنیت و بازسازی کامپوننتها | ✅ تکمیل |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 بخش ۱: تحلیل ضعفهای جاری
|
||||
|
||||
### ۱.۱ ناسازگاریهای ساختاری (Structural)
|
||||
|
||||
#### ❌ ۱.۱.۱ — PageHeader دوگانه
|
||||
**مشکل**: نیمی از صفحات از `<PageHeader>` استفاده میکنند، نیم دیگر header دستی دارند.
|
||||
|
||||
| از `<PageHeader>` استفاده میکنند ✅ | Header دستی دارند ❌ |
|
||||
|---|---|
|
||||
| Store/* (۸ صفحه) | Profile/* (۷ صفحه) |
|
||||
| DiscountStore/* (۶ صفحه) | Club/* (۲ صفحه) |
|
||||
| Gateway/* (۳ صفحه) | Commission/* (۲ صفحه) |
|
||||
| Package/* (۲ صفحه) | Network/* (۱ صفحه) |
|
||||
| PackageDetail, Checkout | Blog/*, Index, About, Contact, FAQ |
|
||||
|
||||
**تأثیر**: ظاهر متفاوت دکمه بازگشت، فاصلهبندی ناهماهنگ
|
||||
|
||||
#### ❌ ۱.۱.۲ — Container MaxWidth متناقض
|
||||
```
|
||||
MaxWidth.Large → اکثر صفحات
|
||||
MaxWidth.Medium → Personal, Settings, OrderTracking, Blog/Post, Gateway/*
|
||||
MaxWidth.Small → ChangePassword
|
||||
ترکیبی (loading≠content) → OrderDetail, ProductDetail, PackageDetail
|
||||
```
|
||||
|
||||
**تأثیر**: برخی صفحات پهنتر از حد نیاز هستند (مثلاً فرمهای ساده با Large)
|
||||
|
||||
#### ❌ ۱.۱.۳ — Container Padding متناقض
|
||||
```
|
||||
py-6 → اکثریت (استاندارد)
|
||||
py-8 → Package/Packages, Package/MyPackages
|
||||
pa-2 pa-md-6 → Store/Products, DiscountStore/Products
|
||||
py-6 py-md-10 → Gateway/*
|
||||
py-4 py-md-6 → DiscountStore/ProductDetail
|
||||
py-16 سکشنی → Index, About, Contact, FAQ
|
||||
```
|
||||
|
||||
**قاعده پیشنهادی**: `py-6` برای صفحات داخلی، section-based برای صفحات عمومی
|
||||
|
||||
---
|
||||
|
||||
### ۱.۲ ناسازگاریهای بصری (Visual)
|
||||
|
||||
#### ❌ ۱.۲.۱ — Inline Style سنگین (۱۱ صفحه)
|
||||
|
||||
| صفحه | نمونه مشکلدار |
|
||||
|---|---|
|
||||
| **Index.razor** | `Style="color:#fff; font-size:clamp(1.6rem,4.5vw,2.4rem);"` |
|
||||
| **Profile/Index** | `Style="background:rgba(99,102,241,.12);"` |
|
||||
| **Store/ProductDetail** | `style="width:100%;height:360px;background-image:url(...);"` |
|
||||
| **PackageDetail** | `Style="background: radial-gradient(600px 280px..."` |
|
||||
| **Checkout** | `Style="background: radial-gradient(..."` + `Elevation="4"` |
|
||||
| **Blog/Index** | `Style="color:#fff; font-weight:700;"`, `Style="font-size:3.5rem;"` |
|
||||
| **Blog/Post** | `Style="width:100%; height:100%;"`, `Style="font-size:4rem;"` |
|
||||
| **About** | `Style="background: radial-gradient(...);"` |
|
||||
| **WeeklyBalance** | `Style="background: linear-gradient(135deg, #e8f5e9..."` |
|
||||
| **Gateway/**** | `Style="background:rgba(16,185,129,.12)..."`, `Style="color:#10b981;"` |
|
||||
| **DiscountStore/ProductDetail** | `Style="background:rgba(16,185,129,.06)..."` |
|
||||
|
||||
#### ❌ ۱.۲.۲ — رنگهای Hardcoded
|
||||
|
||||
| رنگ | استفاده | باید باشد |
|
||||
|---|---|---|
|
||||
| `#10b981` | Gateway (سبز فروشگاه) | `var(--ds-color-store)` |
|
||||
| `#ef4444` | Gateway (قرمز تخفیفی) | `var(--ds-color-discount)` |
|
||||
| `#6366f1`, `#818cf8`, `#a78bfa` | Hero/Banner gradients | `var(--ds-gradient-primary)` |
|
||||
| `rgba(99,102,241,.12)` | Dashboard backgrounds | `var(--ds-primary-soft)` |
|
||||
| `rgba(16,185,129,.06)` | Discount product highlights | `var(--ds-success-soft)` |
|
||||
| `#e8f5e9`, `#c8e6c9` | WeeklyBalance stat cards | `var(--ds-success-gradient)` |
|
||||
|
||||
#### ❌ ۱.۲.۳ — Elevation ناهماهنگ
|
||||
```
|
||||
Elevation="0" → Blog cards (با border)
|
||||
Elevation="1" → برخی صفحات
|
||||
Elevation="2" → اکثر صفحات (استاندارد)
|
||||
Elevation="3" → WeeklyBalance stat cards
|
||||
Elevation="4" → Checkout.razor
|
||||
```
|
||||
|
||||
**قاعده پیشنهادی**: `Elevation="0"` با `border` = کارتهای اطلاعاتی، `Elevation="2"` = default
|
||||
|
||||
#### ❌ ۱.۲.۴ — Paper Rounding ناهماهنگ
|
||||
```
|
||||
rounded-lg (16px) → اکثریت
|
||||
rounded-xl (20px) → Index, Profile/Index, Gateway, DiscountStore/ProductDetail
|
||||
بدون class → design system !important → 12px
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ۱.۳ ناسازگاریهای UX (تجربه کاربری)
|
||||
|
||||
#### ❌ ۱.۳.۱ — Loading State دوگانه
|
||||
```
|
||||
MudProgressCircular → Store/*, DiscountStore/*, Package/*, About, Blog, Addresses
|
||||
MudProgressLinear → Club/*, Commission/*, Network/*, RegisterWizard
|
||||
```
|
||||
|
||||
**مشکل**: کاربر دو تجربه مختلف «در حال بارگذاری» میبیند
|
||||
|
||||
#### ❌ ۱.۳.۲ — Empty State ناهماهنگ
|
||||
```
|
||||
MudAlert Severity.Info → Store/Orders, Categories, DiscountStore/Orders
|
||||
Icon + Text + Button → Addresses, Package/MyPackages, PackageDetail
|
||||
MudAlert Severity.Warning → WithdrawalRequests
|
||||
Custom dashed-border paper → Blog/Index
|
||||
```
|
||||
|
||||
#### ❌ ۱.۳.۳ — Routing Directive ناهماهنگ
|
||||
```
|
||||
@attribute [Route(RouteConstants...)] → ۳۹ صفحه ✅
|
||||
@page "/categories" → Categories.razor ❌
|
||||
@page "/blog/{Slug}" → Blog/Post.razor ❌
|
||||
```
|
||||
|
||||
#### ❌ ۱.۳.۴ — PackageDetail Loading/Error بدون Container
|
||||
Loading و Error state در `PackageDetail.razor` بدون `MudContainer` رندر میشوند → محتوا تمامعرض نمایش مییابد.
|
||||
|
||||
#### ❌ ۱.۳.۵ — تم فعلی کمرنگ
|
||||
```csharp
|
||||
// CustomMudTheme.cs فعلی
|
||||
Primary = "#0380C0" // آبی ساده
|
||||
// بدون Secondary، Tertiary، Info، Warning تعریفشده
|
||||
// بدون PaletteDark
|
||||
// بدون LayoutProperties
|
||||
```
|
||||
|
||||
**مشکلات**:
|
||||
- فقط Primary تعریف شده، بقیه رنگها default MudBlazor
|
||||
- Dark mode بدون palette اختصاصی
|
||||
- بدون `DefaultBorderRadius`، `AppbarHeight` و غیره
|
||||
- تناقض بین Primary `#0380C0` و gradientهای CSS با `#6366f1`
|
||||
|
||||
---
|
||||
|
||||
## 🎯 بخش ۲: معماری دیزاین سیستم هدف
|
||||
|
||||
### ۲.۱ سلسلهمراتب صفحات
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ MainLayout │
|
||||
│ ├─ AppBar (fixed, transparent) │
|
||||
│ ├─ MudMainContent │
|
||||
│ │ ├─ [Public Pages] → Section-based │
|
||||
│ │ │ (Index, About, Contact, FAQ, Blog) │
|
||||
│ │ └─ [Internal Pages] → Container-based │
|
||||
│ │ ├─ <PageHeader/> │
|
||||
│ │ ├─ Content (MudStack/MudGrid) │
|
||||
│ │ └─ </MudContainer> │
|
||||
│ ├─ Footer (hidden on mobile) │
|
||||
│ └─ BottomNav (mobile only) │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### ۲.۲ قواعد واحد (Single Source of Truth)
|
||||
|
||||
| قاعده | مقدار |
|
||||
|---|---|
|
||||
| **Container MaxWidth** | `Large` = لیست/گرید, `Medium` = فرم/جزئیات, `Small` = تکفرم ساده |
|
||||
| **Container Spacing** | `py-6` صفحات داخلی, section-based صفحات عمومی |
|
||||
| **Paper Elevation** | `0` با border = کارت اطلاعاتی, `2` = default |
|
||||
| **Paper Rounding** | `rounded-lg` = default, `rounded-xl` = hero/banner |
|
||||
| **Loading State** | `<LoadingState/>` component واحد |
|
||||
| **Empty State** | `<EmptyState/>` component واحد |
|
||||
| **Page Header** | `<PageHeader/>` در تمام صفحات داخلی |
|
||||
| **Content Wrapper** | `MudStack Spacing="3"` بعد از PageHeader |
|
||||
|
||||
### ۲.۳ CSS Variables هدف
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* ── Brand Colors ── */
|
||||
--ds-brand-primary: #6366f1; /* Indigo — هویت اصلی */
|
||||
--ds-brand-secondary: #8b5cf6; /* Purple */
|
||||
--ds-brand-accent: #a78bfa; /* Light purple */
|
||||
|
||||
/* ── Semantic Colors ── */
|
||||
--ds-color-store: #10b981; /* فروشگاه عادی */
|
||||
--ds-color-discount: #ef4444; /* فروشگاه تخفیفی */
|
||||
--ds-color-success: #10b981;
|
||||
--ds-color-warning: #f59e0b;
|
||||
--ds-color-error: #ef4444;
|
||||
--ds-color-info: #3b82f6;
|
||||
|
||||
/* ── Soft Backgrounds ── */
|
||||
--ds-primary-soft: rgba(99,102,241,.08);
|
||||
--ds-success-soft: rgba(16,185,129,.08);
|
||||
--ds-error-soft: rgba(239,68,68,.08);
|
||||
--ds-warning-soft: rgba(245,158,11,.08);
|
||||
|
||||
/* ── Gradients ── */
|
||||
--ds-gradient-primary: linear-gradient(135deg, #6366f1 0%, #818cf8 50%, #a78bfa 100%);
|
||||
--ds-gradient-store: linear-gradient(135deg, #10b981 0%, #34d399 100%);
|
||||
--ds-gradient-discount: linear-gradient(135deg, #ef4444 0%, #f97316 50%, #f59e0b 100%);
|
||||
--ds-gradient-success: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
|
||||
|
||||
/* ── Spacing (existing) ── */
|
||||
--ds-radius-sm: 8px;
|
||||
--ds-radius-md: 12px;
|
||||
--ds-radius-lg: 16px;
|
||||
--ds-radius-xl: 20px;
|
||||
--ds-transition: 0.2s ease;
|
||||
--ds-shadow-sm: 0 1px 3px rgba(0,0,0,.06);
|
||||
--ds-shadow-md: 0 4px 12px rgba(0,0,0,.08);
|
||||
--ds-shadow-lg: 0 8px 24px rgba(0,0,0,.10);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 بخش ۳: فازبندی اجرا
|
||||
|
||||
### 🔷 فاز ۱ — زیرساخت دیزاین سیستم (UI ~15%)
|
||||
> **اولویت**: بالا | **ریسک**: پایین | **حجم**: ۶ فایل
|
||||
|
||||
| # | تسک | فایل | نوع تغییر |
|
||||
|---|---|---|---|
|
||||
| 1.1 | ارتقاء `CustomMudTheme.cs` — اضافه کردن PaletteDark، LayoutProperties، رنگهای Secondary/Tertiary/Info، تغییر Primary به `#6366f1` | `CustomMudTheme.cs` | UI |
|
||||
| 1.2 | توسعه CSS Variables — اضافه کردن brand colors، semantic colors، soft backgrounds، gradients | `site.css` | UI |
|
||||
| 1.3 | ساخت `<LoadingState>` component واحد | `Shared/LoadingState.razor` (جدید) | UX |
|
||||
| 1.4 | ساخت `<EmptyState>` component واحد | `Shared/EmptyState.razor` (جدید) | UX |
|
||||
| 1.5 | بهبود `<PageHeader>` — اضافه کردن آیکون، subtitle اختیاری | `Shared/PageHeader.razor` | UI |
|
||||
| 1.6 | اضافه کردن `.page-container` CSS pattern | `site.css` | UI |
|
||||
|
||||
---
|
||||
|
||||
### 🔷 فاز ۲ — یکپارچهسازی صفحات Profile (UI ~10%, UX ~5%)
|
||||
> **اولویت**: بالا | **ریسک**: پایین | **حجم**: ۹ فایل
|
||||
|
||||
| # | تسک | تغییرات |
|
||||
|---|---|---|
|
||||
| 2.1 | `Profile/Personal` → جایگزینی header دستی با `<PageHeader>` | UX |
|
||||
| 2.2 | `Profile/Addresses` → `<PageHeader>` + `<LoadingState>` + `<EmptyState>` | UX |
|
||||
| 2.3 | `Profile/Wallet` → `<PageHeader>` | UX |
|
||||
| 2.4 | `Profile/Settings` → `<PageHeader>` | UX |
|
||||
| 2.5 | `Profile/Tree` → `<PageHeader>` | UX |
|
||||
| 2.6 | `Profile/WithdrawalRequests` → `<PageHeader>` + `<EmptyState>` | UX |
|
||||
| 2.7 | `Profile/ChangePassword` → `<PageHeader>` | UX |
|
||||
| 2.8 | `Profile/Index` (Dashboard) → حذف inline styles، استفاده از CSS Variables | UI |
|
||||
| 2.9 | `Club/MembershipPage` + `Club/FeaturesPage` → `<PageHeader>` + `<LoadingState>` | UX |
|
||||
|
||||
---
|
||||
|
||||
### 🔷 فاز ۳ — یکپارچهسازی صفحات تخصصی (UI ~5%, UX ~5%)
|
||||
> **اولویت**: متوسط | **ریسک**: پایین | **حجم**: ۵ فایل
|
||||
|
||||
| # | تسک | تغییرات |
|
||||
|---|---|---|
|
||||
| 3.1 | `Commission/Dashboard` → `<PageHeader>` + `<LoadingState>` | UX |
|
||||
| 3.2 | `Commission/WeeklyBalance` → `<PageHeader>` + حذف inline gradient styles | UI + UX |
|
||||
| 3.3 | `Network/NetworkStatistics` → `<PageHeader>` + `<LoadingState>` | UX |
|
||||
| 3.4 | `PackageDetail` → wrap loading/error در `MudContainer` | UX bug fix |
|
||||
| 3.5 | `Checkout` → Elevation=4→2، حذف inline radial-gradient | UI |
|
||||
|
||||
---
|
||||
|
||||
### 🔷 فاز ۴ — بهبود بصری فروشگاهها (UI ~10%)
|
||||
> **اولویت**: متوسط | **ریسک**: پایین | **حجم**: ۶ فایل
|
||||
|
||||
| # | تسک | تغییرات |
|
||||
|---|---|---|
|
||||
| 4.1 | `Store/Products` → حذف inline hero styles، استفاده از CSS class | UI |
|
||||
| 4.2 | `Store/ProductDetail` → حذف inline image styles، ساخت `.product-image-main` CSS | UI |
|
||||
| 4.3 | `DiscountStore/ProductDetail` → حذف hardcoded rgba، استفاده از `--ds-success-soft` | UI |
|
||||
| 4.4 | `Gateway/*` (۳ صفحه) → حذف hardcoded `#10b981`/`#ef4444`، استفاده از `--ds-color-store`/`--ds-color-discount` | UI |
|
||||
| 4.5 | یکسانسازی Elevation → `0` با border یا `2` | UI |
|
||||
| 4.6 | یکسانسازی Container spacing → `py-6` | UI |
|
||||
|
||||
---
|
||||
|
||||
### 🔷 فاز ۵ — بهبود صفحات عمومی و بلاگ (UI ~10%)
|
||||
> **اولویت**: پایین | **ریسک**: پایین | **حجم**: ۷ فایل
|
||||
|
||||
| # | تسک | تغییرات |
|
||||
|---|---|---|
|
||||
| 5.1 | `Index.razor` → حذف inline styles از hero، استفاده از CSS class | UI |
|
||||
| 5.2 | `About.razor` → حذف inline radial-gradient | UI |
|
||||
| 5.3 | `Contact.razor` → cleanup minor inline styles | UI |
|
||||
| 5.4 | `FAQ.razor` → cleanup minor inline styles | UI |
|
||||
| 5.5 | `Blog/Index` → حذف inline hero styles، استفاده از CSS class | UI |
|
||||
| 5.6 | `Blog/Post` → حذف inline styles از hero image و typography | UI |
|
||||
| 5.7 | `RegisterWizard` → بهینهسازی wizard-section dark mode | UI |
|
||||
|
||||
---
|
||||
|
||||
### 🔷 فاز ۶ — Polish نهایی و فرآیندی (UX ~10%)
|
||||
> **اولویت**: پایین | **ریسک**: بسیار پایین | **حجم**: ۴ فایل + تست
|
||||
|
||||
| # | تسک | تغییرات |
|
||||
|---|---|---|
|
||||
| 6.1 | Fix routing inconsistency — Categories + Blog/Post | UX |
|
||||
| 6.2 | MudSnackbar notifications styling | UI |
|
||||
| 6.3 | Dialog styling consistency (AuthDialog, AddressDialogs) | UI |
|
||||
| 6.4 | Micro-interactions — button press, card hover, page transition | UI |
|
||||
| 6.5 | تست کامل Dark Mode در تمام صفحات | UI + QA |
|
||||
| 6.6 | تست Mobile Responsive در تمام صفحات | UX + QA |
|
||||
|
||||
---
|
||||
|
||||
## 📈 بخش ۴: جدول تأثیرگذاری
|
||||
|
||||
### تأثیر UI (هدف ~۵۰٪ تغییر)
|
||||
|
||||
| حوزه | تعداد فایل | درصد تأثیر |
|
||||
|---|---|---|
|
||||
| Theme + CSS Variables | ۲ | ۱۵% (تأثیر سراسری) |
|
||||
| حذف Inline Styles | ۱۱ | ۱۵% |
|
||||
| یکسانسازی Elevation/Rounding | ۲۰+ | ۱۰% |
|
||||
| بهبود رنگبندی (CSS Variables) | ۸ | ۵% |
|
||||
| Micro-interactions | سراسری | ۵% |
|
||||
| **جمع** | | **~۵۰%** |
|
||||
|
||||
### تأثیر UX (هدف ~۲۰٪ تغییر)
|
||||
|
||||
| حوزه | تعداد فایل | درصد تأثیر |
|
||||
|---|---|---|
|
||||
| PageHeader یکپارچه | ۱۲ صفحه | ۸% |
|
||||
| LoadingState واحد | ۱۵+ صفحه | ۴% |
|
||||
| EmptyState واحد | ۸+ صفحه | ۳% |
|
||||
| Container fixes (PackageDetail) | ۲ | ۲% |
|
||||
| Routing consistency | ۲ | ۱% |
|
||||
| Flow improvements | ۲ | ۲% |
|
||||
| **جمع** | | **~۲۰%** |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 بخش ۵: مشخصات فنی کامپوننتهای جدید
|
||||
|
||||
### ۵.۱ LoadingState Component
|
||||
|
||||
```razor
|
||||
@* Shared/LoadingState.razor *@
|
||||
<MudStack AlignItems="AlignItems.Center" Class="py-16">
|
||||
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large" />
|
||||
@if (!string.IsNullOrWhiteSpace(Message))
|
||||
{
|
||||
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">@Message</MudText>
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
@code {
|
||||
[Parameter] public string Message { get; set; } = "در حال بارگذاری...";
|
||||
}
|
||||
```
|
||||
|
||||
### ۵.۲ EmptyState Component
|
||||
|
||||
```razor
|
||||
@* Shared/EmptyState.razor *@
|
||||
<MudStack AlignItems="AlignItems.Center" Class="py-12" Spacing="3">
|
||||
<MudIcon Icon="@Icon" Size="Size.Large" Color="Color.Default" Class="mud-text-disabled" />
|
||||
<MudText Typo="Typo.h6" Class="mud-text-secondary">@Title</MudText>
|
||||
@if (!string.IsNullOrWhiteSpace(Description))
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary" Style="max-width:400px; text-align:center;">
|
||||
@Description
|
||||
</MudText>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(ActionText))
|
||||
{
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
||||
Href="@ActionHref" OnClick="@OnAction" Class="mt-2">
|
||||
@ActionText
|
||||
</MudButton>
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
@code {
|
||||
[Parameter] public string Icon { get; set; } = Icons.Material.Filled.Inbox;
|
||||
[Parameter] public string Title { get; set; } = "موردی یافت نشد";
|
||||
[Parameter] public string? Description { get; set; }
|
||||
[Parameter] public string? ActionText { get; set; }
|
||||
[Parameter] public string? ActionHref { get; set; }
|
||||
[Parameter] public EventCallback OnAction { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### ۵.۳ PageHeader ارتقاءیافته
|
||||
|
||||
```razor
|
||||
@* Shared/PageHeader.razor — ارتقاءیافته *@
|
||||
<div class="page-header">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.h5">@Title</MudText>
|
||||
@if (!string.IsNullOrWhiteSpace(Subtitle))
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">@Subtitle</MudText>
|
||||
}
|
||||
</MudStack>
|
||||
@if (!string.IsNullOrWhiteSpace(BackHref))
|
||||
{
|
||||
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack"
|
||||
Href="@BackHref">بازگشت</MudButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack"
|
||||
OnClick="GoBack">بازگشت</MudButton>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string Title { get; set; } = "";
|
||||
[Parameter] public string? Subtitle { get; set; }
|
||||
[Parameter] public string? BackHref { get; set; }
|
||||
|
||||
[Inject] private IJSRuntime JS { get; set; } = default!;
|
||||
private async Task GoBack() => await JS.InvokeVoidAsync("history.back");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ بخش ۶: چکلیست تکمیل هر فاز
|
||||
|
||||
### فاز ۱ چکلیست:
|
||||
- [ ] `CustomMudTheme.cs` — Primary→`#6366f1`, PaletteDark اضافه شد
|
||||
- [ ] `site.css` — CSS Variables جدید (brand, semantic, soft, gradient)
|
||||
- [ ] `Shared/LoadingState.razor` — ساخته و تست شد
|
||||
- [ ] `Shared/EmptyState.razor` — ساخته و تست شد
|
||||
- [ ] `Shared/PageHeader.razor` — Subtitle parameter اضافه شد
|
||||
- [ ] `.page-container` CSS pattern اضافه شد
|
||||
- [ ] Build: 0 errors ✅
|
||||
- [ ] Dark Mode: صحیح ✅
|
||||
- [ ] Mobile: صحیح ✅
|
||||
|
||||
### فاز ۲ چکلیست:
|
||||
- [ ] `Profile/Personal` → `<PageHeader>`
|
||||
- [ ] `Profile/Addresses` → `<PageHeader>` + `<LoadingState>` + `<EmptyState>`
|
||||
- [ ] `Profile/Wallet` → `<PageHeader>`
|
||||
- [ ] `Profile/Settings` → `<PageHeader>`
|
||||
- [ ] `Profile/Tree` → `<PageHeader>`
|
||||
- [ ] `Profile/WithdrawalRequests` → `<PageHeader>` + `<EmptyState>`
|
||||
- [ ] `Profile/ChangePassword` → `<PageHeader>`
|
||||
- [ ] `Profile/Index` → inline styles → CSS
|
||||
- [ ] `Club/*` → `<PageHeader>` + `<LoadingState>`
|
||||
- [ ] Build: 0 errors ✅
|
||||
|
||||
### فاز ۳ چکلیست:
|
||||
- [ ] `Commission/*` → `<PageHeader>` + `<LoadingState>`
|
||||
- [ ] `Network/*` → `<PageHeader>` + `<LoadingState>`
|
||||
- [ ] `PackageDetail` → MudContainer wrapper برای loading/error
|
||||
- [ ] `Checkout` → Elevation fix + inline cleanup
|
||||
- [ ] `WeeklyBalance` → inline gradient → CSS class
|
||||
- [ ] Build: 0 errors ✅
|
||||
|
||||
### فاز ۴ چکلیست:
|
||||
- [ ] `Store/Products` → hero inline → CSS
|
||||
- [ ] `Store/ProductDetail` → image inline → CSS class
|
||||
- [ ] `DiscountStore/ProductDetail` → rgba → variable
|
||||
- [ ] `Gateway/*` → hardcoded → variable
|
||||
- [ ] Elevation یکسانسازی
|
||||
- [ ] Container spacing یکسانسازی
|
||||
- [ ] Build: 0 errors ✅
|
||||
|
||||
### فاز ۵ چکلیست:
|
||||
- [ ] `Index.razor` → hero inline cleanup
|
||||
- [ ] `About.razor` → radial-gradient cleanup
|
||||
- [ ] `Blog/Index` + `Blog/Post` → inline cleanup
|
||||
- [ ] `Contact.razor` + `FAQ.razor` → minor cleanup
|
||||
- [ ] Build: 0 errors ✅
|
||||
|
||||
### فاز ۶ چکلیست:
|
||||
- [ ] Routing fix (Categories, Blog/Post)
|
||||
- [ ] MudSnackbar styling
|
||||
- [ ] Dialog consistency
|
||||
- [ ] Dark mode full test
|
||||
- [ ] Mobile responsive full test
|
||||
- [ ] Build: 0 errors ✅
|
||||
|
||||
---
|
||||
|
||||
## 📋 بخش ۷: خلاصه تغییرات در یک نگاه
|
||||
|
||||
```
|
||||
فایلهای تغییریافته:
|
||||
├── CustomMudTheme.cs [فاز ۱] — ارتقاء کامل تم
|
||||
├── site.css [فاز ۱-۵] — CSS Variables + classهای جدید
|
||||
├── Shared/LoadingState.razor [فاز ۱] — جدید
|
||||
├── Shared/EmptyState.razor [فاز ۱] — جدید
|
||||
├── Shared/PageHeader.razor [فاز ۱] — بهبود
|
||||
├── Profile/* (۷ فایل) [فاز ۲] — PageHeader + LoadingState
|
||||
├── Profile/Index.razor [فاز ۲] — حذف inline styles
|
||||
├── Club/* (۲ فایل) [فاز ۲] — PageHeader + LoadingState
|
||||
├── Commission/* (۲ فایل) [فاز ۳] — PageHeader + LoadingState + cleanup
|
||||
├── Network/* (۱ فایل) [فاز ۳] — PageHeader + LoadingState
|
||||
├── PackageDetail.razor [فاز ۳] — Container fix
|
||||
├── Checkout.razor [فاز ۳] — Elevation + cleanup
|
||||
├── Store/* (۲ فایل) [فاز ۴] — inline → CSS
|
||||
├── DiscountStore/ProductDetail [فاز ۴] — rgba → variable
|
||||
├── Gateway/* (۳ فایل) [فاز ۴] — hardcoded → variable
|
||||
├── Index.razor [فاز ۵] — hero cleanup
|
||||
├── About.razor [فاز ۵] — gradient cleanup
|
||||
├── Blog/* (۲ فایل) [فاز ۵] — inline cleanup
|
||||
├── Contact.razor + FAQ.razor [فاز ۵] — minor cleanup
|
||||
└── Categories + Blog/Post [فاز ۶] — routing fix
|
||||
```
|
||||
|
||||
**مجموع فایلهای تأثیرپذیر**: ~۳۵ فایل
|
||||
**فایلهای جدید**: ۲ (LoadingState, EmptyState)
|
||||
**میزان تغییر UI**: ~۵۰٪
|
||||
**میزان تغییر UX**: ~۲۰٪
|
||||
**ریسک شکست**: پایین (تغییرات تدریجی، build verification در هر فاز)
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
# FrontOffice (Blazor Server) production.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: frontoffice
|
||||
namespace: default
|
||||
labels:
|
||||
app: frontoffice
|
||||
environment: production
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: frontoffice
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: frontoffice
|
||||
spec:
|
||||
containers:
|
||||
- name: frontoffice
|
||||
image: 194.5.195.53:30080/admin/frontoffice:prod
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
env:
|
||||
- name: ASPNETCORE_ENVIRONMENT
|
||||
value: "Production"
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
imagePullSecrets:
|
||||
- name: gitea-registry-secret
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: frontoffice-svc
|
||||
namespace: default
|
||||
labels:
|
||||
app: frontoffice
|
||||
spec:
|
||||
selector:
|
||||
app: frontoffice
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
name: http
|
||||
type: ClusterIP
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
# FrontOffice (Blazor Server) staging.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: frontoffice
|
||||
namespace: default
|
||||
labels:
|
||||
app: frontoffice
|
||||
environment: staging
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: frontoffice
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: frontoffice
|
||||
spec:
|
||||
containers:
|
||||
- name: frontoffice
|
||||
image: 194.5.195.53:30080/admin/frontoffice:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
env:
|
||||
- name: ASPNETCORE_ENVIRONMENT
|
||||
value: "Staging"
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
imagePullSecrets:
|
||||
- name: gitea-registry-secret
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: frontoffice-svc
|
||||
namespace: default
|
||||
labels:
|
||||
app: frontoffice
|
||||
spec:
|
||||
selector:
|
||||
app: frontoffice
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
name: http
|
||||
type: ClusterIP
|
||||
@@ -15,14 +15,13 @@ using CMSMicroservice.Protobuf.Protos.User;
|
||||
using CMSMicroservice.Protobuf.Protos.UserCarts;
|
||||
using CMSMicroservice.Protobuf.Protos.UserOrder;
|
||||
using CMSMicroservice.Protobuf.Protos.UserWallet;
|
||||
using CMSMicroservice.Protobuf.Protos.UserWalletHistory;
|
||||
using CMSMicroservice.Protobuf.Protos.UserWalletChangeLog;
|
||||
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
||||
using CMSMicroservice.Protobuf.Protos.Configuration;
|
||||
using CMSMicroservice.Protobuf.Protos.NetworkMembership;
|
||||
using CMSMicroservice.Protobuf.Protos.Commission;
|
||||
using CMSMicroservice.Protobuf.Protos.AppVersion;
|
||||
using CMSMicroservice.Protobuf.Protos.SitePage;
|
||||
using CMSMicroservice.Protobuf.Protos.SitePageSettings;
|
||||
using CMSMicroservice.Protobuf.Protos.BlogPost;
|
||||
using CMSMicroservice.Protobuf.Protos.BlogCategory;
|
||||
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
|
||||
@@ -30,7 +29,6 @@ using CMSMicroservice.Protobuf.Protos.DiscountCategory;
|
||||
using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
|
||||
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
|
||||
using FrontOffice.Main.Utilities;
|
||||
using FrontOffice.Main.Utilities.Seo;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@@ -57,7 +55,6 @@ public static class ConfigureServices
|
||||
services.AddSingleton<UserAuthInfo>();
|
||||
services.AddScoped<AuthService>();
|
||||
services.AddScoped<AuthDialogService>();
|
||||
services.AddScoped<GuestActionGate>();
|
||||
// Storefront services
|
||||
services.AddScoped<CartService>();
|
||||
services.AddScoped<ProductService>();
|
||||
@@ -86,8 +83,6 @@ public static class ConfigureServices
|
||||
services.AddScoped<DiscountProductService>();
|
||||
services.AddScoped<DiscountCartService>();
|
||||
services.AddScoped<DiscountOrderService>();
|
||||
// Site Page Settings Service (simplified page management)
|
||||
services.AddScoped<SitePageSettingsService>();
|
||||
// Device detection: very light, dependency-free
|
||||
services.AddTransient<IDeviceDetector, DeviceDetector>();
|
||||
// PDF generation (Chromium only)
|
||||
@@ -96,45 +91,26 @@ public static class ConfigureServices
|
||||
// SignalR Token Notification Service
|
||||
services.AddScoped<TokenNotificationService>();
|
||||
|
||||
// SEO
|
||||
services.AddScoped<SeoMetadataProvider>();
|
||||
services.AddScoped<SitemapGenerator>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddGrpcServices(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var baseUrl = ResolveGatewayUrl(configuration)
|
||||
?? throw new InvalidOperationException("Gateway URL is missing. Set GW_URL or GwUrl.");
|
||||
var baseUrl = configuration["GwUrl"];
|
||||
|
||||
var isHttp = baseUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// When the base URL is plain HTTP (e.g. in-cluster http://cms-svc:8080), we must force HTTP/1.1
|
||||
// so that GrpcChannel does not upgrade to h2c and trigger HTTP_1_1_REQUIRED from Kestrel.
|
||||
// For HTTPS the default negotiation is fine.
|
||||
// Register optimized HttpClient for gRPC
|
||||
services.AddScoped(sp =>
|
||||
{
|
||||
HttpMessageHandler inner = isHttp
|
||||
? new SocketsHttpHandler
|
||||
{
|
||||
MaxConnectionsPerServer = 10,
|
||||
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate
|
||||
}
|
||||
: new HttpClientHandler
|
||||
{
|
||||
MaxConnectionsPerServer = 10,
|
||||
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate
|
||||
};
|
||||
var handler = new HttpClientHandler
|
||||
{
|
||||
MaxConnectionsPerServer = 10,
|
||||
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate
|
||||
};
|
||||
|
||||
return new HttpClient(new GrpcWebHandler(GrpcWebMode.GrpcWeb, inner))
|
||||
return new HttpClient(new GrpcWebHandler(GrpcWebMode.GrpcWeb, handler))
|
||||
{
|
||||
Timeout = TimeSpan.FromMinutes(10),
|
||||
BaseAddress = new Uri(baseUrl),
|
||||
DefaultRequestVersion = isHttp ? System.Net.HttpVersion.Version11 : System.Net.HttpVersion.Version20,
|
||||
DefaultVersionPolicy = isHttp
|
||||
? System.Net.Http.HttpVersionPolicy.RequestVersionExact
|
||||
: System.Net.Http.HttpVersionPolicy.RequestVersionOrHigher
|
||||
BaseAddress = new Uri(baseUrl)
|
||||
};
|
||||
});
|
||||
|
||||
@@ -149,7 +125,7 @@ public static class ConfigureServices
|
||||
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.UserCarts.UserCartsContract.UserCartsContractClient>);
|
||||
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.City.CityContract.CityContractClient>);
|
||||
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.UserAddress.UserAddressContract.UserAddressContractClient>);
|
||||
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.UserWalletHistory.UserWalletHistoryContract.UserWalletHistoryContractClient>);
|
||||
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.UserWalletChangeLog.UserWalletChangeLogContract.UserWalletChangeLogContractClient>);
|
||||
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.ClubMembership.ClubMembershipContract.ClubMembershipContractClient>);
|
||||
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.OtpToken.OtpTokenContract.OtpTokenContractClient>);
|
||||
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.Configuration.ConfigurationContract.ConfigurationContractClient>);
|
||||
@@ -157,7 +133,6 @@ public static class ConfigureServices
|
||||
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.Commission.CommissionContract.CommissionContractClient>);
|
||||
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.AppVersion.AppVersionContract.AppVersionContractClient>);
|
||||
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.SitePage.SitePageContract.SitePageContractClient>);
|
||||
services.AddScoped(CreateAuthenticatedClient<SitePageSettingsContract.SitePageSettingsContractClient>);
|
||||
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.BlogPost.BlogPostContract.BlogPostContractClient>);
|
||||
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.BlogCategory.BlogCategoryContract.BlogCategoryContractClient>);
|
||||
// Image Resolver Service
|
||||
@@ -172,22 +147,12 @@ public static class ConfigureServices
|
||||
return services;
|
||||
}
|
||||
|
||||
private static string? ResolveGatewayUrl(IConfiguration configuration)
|
||||
{
|
||||
var envUrl = configuration["GW_URL"];
|
||||
if (!string.IsNullOrWhiteSpace(envUrl))
|
||||
return envUrl.TrimEnd('/');
|
||||
|
||||
return configuration["GwUrl"]?.TrimEnd('/');
|
||||
}
|
||||
|
||||
private static TClient CreateAuthenticatedClient<TClient>(IServiceProvider sp)
|
||||
where TClient : class
|
||||
{
|
||||
var httpClient = sp.GetRequiredService<HttpClient>();
|
||||
var localStorage = sp.GetRequiredService<ILocalStorageService>();
|
||||
var baseUrl = httpClient.BaseAddress?.ToString() ?? throw new InvalidOperationException("Base URL not configured");
|
||||
var isHttps = baseUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var credentials = CallCredentials.FromInterceptor(async (context, metadata) =>
|
||||
{
|
||||
@@ -207,14 +172,10 @@ public static class ConfigureServices
|
||||
}
|
||||
});
|
||||
|
||||
var channelCredentials = isHttps
|
||||
? ChannelCredentials.Create(new SslCredentials(), credentials)
|
||||
: ChannelCredentials.Create(ChannelCredentials.Insecure, credentials);
|
||||
|
||||
var channel = GrpcChannel.ForAddress(baseUrl, new GrpcChannelOptions
|
||||
{
|
||||
UnsafeUseInsecureChannelCallCredentials = !isHttps,
|
||||
Credentials = channelCredentials,
|
||||
UnsafeUseInsecureChannelCallCredentials = true,
|
||||
Credentials = ChannelCredentials.Create(new SslCredentials(), credentials),
|
||||
HttpClient = httpClient,
|
||||
MaxReceiveMessageSize = 1000 * 1024 * 1024, // 1 GB
|
||||
MaxSendMessageSize = 1000 * 1024 * 1024 // 1 GB
|
||||
|
||||
@@ -20,10 +20,6 @@ FROM 194.5.195.53:32082/dotnet/aspnet:9.0 AS runtime
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
|
||||
# Trust the staging-ca so server-side gRPC calls to https://cms.se.kbs1.ir succeed without PartialChain
|
||||
COPY ["FrontOffice.Main/staging-ca.crt", "/usr/local/share/ca-certificates/staging-ca.crt"]
|
||||
RUN update-ca-certificates
|
||||
|
||||
ENV ASPNETCORE_URLS=http://+:80
|
||||
EXPOSE 80
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
@@ -11,8 +11,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
|
||||
<!-- Replace all FrontOffice.BFF protobuf packages with CMS protobuf -->
|
||||
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.210" />
|
||||
<!-- <ProjectReference Include="../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj" />-->
|
||||
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.179" />
|
||||
<!--<ProjectReference Include="../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj" />-->
|
||||
<PackageReference Include="MudBlazor" Version="8.14.0" />
|
||||
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
|
||||
<PackageReference Include="Mapster" Version="7.4.0" />
|
||||
@@ -56,6 +56,12 @@
|
||||
<Content Include="..\.dockerignore">
|
||||
<Link>.dockerignore</Link>
|
||||
</Content>
|
||||
<Content Remove="Pages\Package\Packages.razor" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Pages\Package\Packages.razor.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@attribute [Route(RouteConstants.About.Index)]
|
||||
@inject NavigationManager Navigation
|
||||
@inject SitePageSettingsService PageSettingsService
|
||||
@inject SitePageService SitePageService
|
||||
|
||||
<PageTitle>درباره ما | کارا بازار سلامت</PageTitle>
|
||||
|
||||
@@ -66,22 +66,40 @@ else
|
||||
<MudPaper Elevation="2" Class="pa-6 text-center h-100">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Visibility" Size="Size.Large" Color="Color.Primary" Class="mb-4" />
|
||||
<MudText Typo="Typo.h5" Class="mb-3">
|
||||
@(_settings?.VisionTitle ?? "چشمانداز")
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mud-text-secondary">
|
||||
@(_settings?.VisionText ?? "تبدیل شدن به پیشروترین پلتفرم مدیریت تیمهای فروش در منطقه، با ارائه راهکارهای هوشمند و کاربرپسند برای کسبوکارهای کوچک و بزرگ.")
|
||||
@(_visionSection?.Title ?? "چشمانداز")
|
||||
</MudText>
|
||||
@if (!string.IsNullOrWhiteSpace(_visionSection?.HtmlContent))
|
||||
{
|
||||
<MudText Typo="Typo.body1" Class="mud-text-secondary">
|
||||
@((MarkupString)_visionSection.HtmlContent)
|
||||
</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.body1" Class="mud-text-secondary">
|
||||
تبدیل شدن به پیشروترین پلتفرم مدیریت تیمهای فروش در منطقه، با ارائه راهکارهای هوشمند و کاربرپسند برای کسبوکارهای کوچک و بزرگ.
|
||||
</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
<MudPaper Elevation="2" Class="pa-6 text-center h-100">
|
||||
<MudIcon Icon="@Icons.Material.Filled.TrackChanges" Size="Size.Large" Color="Color.Success" Class="mb-4" />
|
||||
<MudText Typo="Typo.h5" Class="mb-3">
|
||||
@(_settings?.MissionTitle ?? "مأموریت")
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mud-text-secondary">
|
||||
@(_settings?.MissionText ?? "توانمندسازی کسبوکارها از طریق فناوریهای نوین، سادهسازی فرآیندهای پیچیده و ایجاد فرصتهای جدید برای رشد و توسعه پایدار.")
|
||||
@(_missionSection?.Title ?? "مأموریت")
|
||||
</MudText>
|
||||
@if (!string.IsNullOrWhiteSpace(_missionSection?.HtmlContent))
|
||||
{
|
||||
<MudText Typo="Typo.body1" Class="mud-text-secondary">
|
||||
@((MarkupString)_missionSection.HtmlContent)
|
||||
</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.body1" Class="mud-text-secondary">
|
||||
توانمندسازی کسبوکارها از طریق فناوریهای نوین، سادهسازی فرآیندهای پیچیده و ایجاد کارا بازار سلامتهای جدید برای رشد و توسعه پایدار.
|
||||
</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
@@ -93,21 +111,21 @@ else
|
||||
<MudContainer MaxWidth="MaxWidth.Large">
|
||||
<MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">ارزشهای ما</MudText>
|
||||
<MudGrid Spacing="3" Justify="Justify.Center">
|
||||
@if (_valueImages.Any())
|
||||
@if (_valueSections.Any())
|
||||
{
|
||||
@foreach (var value in _valueImages)
|
||||
@foreach (var value in _valueSections)
|
||||
{
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
|
||||
@if (!string.IsNullOrWhiteSpace(value.IconName))
|
||||
{
|
||||
<MudIcon Icon="@($"Icons.Material.Filled.{value.IconName}")" Size="Size.Large" Color="Color.Primary" Class="mb-3" />
|
||||
<MudIcon Icon="@value.IconName" Size="Size.Large" Color="Color.Primary" Class="mb-3" />
|
||||
}
|
||||
<MudText Typo="Typo.h6" Class="mb-2">@value.Title</MudText>
|
||||
@if (!string.IsNullOrWhiteSpace(value.Description))
|
||||
@if (!string.IsNullOrWhiteSpace(value.HtmlContent))
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">
|
||||
@value.Description
|
||||
@((MarkupString)value.HtmlContent)
|
||||
</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
@@ -181,9 +199,9 @@ else
|
||||
<MudContainer MaxWidth="MaxWidth.Large">
|
||||
<MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">تیم ما</MudText>
|
||||
<MudGrid Spacing="4" Justify="Justify.Center">
|
||||
@if (_teamImages.Any())
|
||||
@if (_teamSections.Any())
|
||||
{
|
||||
@foreach (var member in _teamImages)
|
||||
@foreach (var member in _teamSections)
|
||||
{
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudPaper Elevation="2" Class="pa-4 text-center">
|
||||
@@ -204,10 +222,10 @@ else
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2">@member.Subtitle</MudText>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(member.Description))
|
||||
@if (!string.IsNullOrWhiteSpace(member.HtmlContent))
|
||||
{
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
||||
@member.Description
|
||||
@((MarkupString)member.HtmlContent)
|
||||
</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
|
||||
@@ -5,22 +5,24 @@ namespace FrontOffice.Main.Pages;
|
||||
public partial class About
|
||||
{
|
||||
private bool _loading = true;
|
||||
private PageSettingsDto? _page;
|
||||
private AboutSettings? _settings;
|
||||
private List<PageSettingsImageDto> _valueImages = new();
|
||||
private List<PageSettingsImageDto> _teamImages = new();
|
||||
private SitePageDto? _page;
|
||||
private SitePageSectionDto? _visionSection;
|
||||
private SitePageSectionDto? _missionSection;
|
||||
private List<SitePageSectionDto> _valueSections = new();
|
||||
private List<SitePageSectionDto> _teamSections = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_page = await PageSettingsService.GetPageAsync("about");
|
||||
_page = await SitePageService.GetByKeyAsync("about");
|
||||
|
||||
if (_page != null)
|
||||
{
|
||||
_settings = _page.GetSettings<AboutSettings>();
|
||||
_valueImages = _page.GetImages("values");
|
||||
_teamImages = _page.GetImages("team");
|
||||
_visionSection = _page.GetSection("vision");
|
||||
_missionSection = _page.GetSection("mission");
|
||||
_valueSections = _page.GetSections("value-").ToList();
|
||||
_teamSections = _page.GetSections("team-").ToList();
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -32,16 +34,4 @@ public partial class About
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private class AboutSettings
|
||||
{
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -27,11 +27,6 @@
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
||||
<MudText Typo="Typo.h5" Class="mb-4">جزئیات پکیج</MudText>
|
||||
|
||||
@* ── G2: توضیح خرید پکیج (Q28) ── *@
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3">
|
||||
با خرید این پکیج، سقف پاداش هفتگی، ضریب کیفپول جادویی و دسترسی به فیچرهای اختصاصی برای شما فعال میشود.
|
||||
</MudAlert>
|
||||
|
||||
@if (_selectedPackage != null)
|
||||
{
|
||||
<MudCard>
|
||||
@@ -71,6 +66,80 @@
|
||||
</MudStack>
|
||||
}
|
||||
</MudPaper>
|
||||
|
||||
<!-- Address Selection -->
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
||||
<MudText Typo="Typo.h5" Class="mb-4">انتخاب آدرس</MudText>
|
||||
|
||||
@if (_isLoadingAddresses)
|
||||
{
|
||||
<MudStack AlignItems="AlignItems.Center" Class="py-4">
|
||||
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Medium" />
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-2">بارگذاری آدرسها...</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
else if (_addresses.Any())
|
||||
{
|
||||
<MudStack Spacing="2">
|
||||
@foreach (var address in _addresses)
|
||||
{
|
||||
<MudPaper Outlined="@(_selectedAddress?.Id != address.Id)"
|
||||
Elevation="@(_selectedAddress?.Id == address.Id ? 4 : 0)"
|
||||
Class="pa-3 rounded-xl cursor-pointer"
|
||||
Style="@(_selectedAddress?.Id == address.Id ? "border: 2px solid var(--mud-palette-primary);" : "")"
|
||||
@onclick="() => SetAddressAsDefault(address.Id)">
|
||||
<MudStack Spacing="1">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudCheckBox @bind-Value="address.IsDefault"
|
||||
Disabled="true"
|
||||
Color="Color.Primary" />
|
||||
<MudText Typo="Typo.subtitle2">@(address.Title)</MudText>
|
||||
@if (address.IsDefault)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Success" Variant="Variant.Filled" Size="Size.Small">پیشفرض</MudChip>
|
||||
}
|
||||
@if (!address.IsDefault)
|
||||
{
|
||||
@if (_isSettingDefaultAddress && _settingDefaultAddressId == address.Id)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Color="Color.Primary" Indeterminate="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton Variant="Variant.Text"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
OnClick="() => SetAddressAsDefault(address.Id)">
|
||||
تنظیم به عنوان پیشفرض
|
||||
</MudButton>
|
||||
}
|
||||
}
|
||||
</MudStack>
|
||||
<MudStack Row="true">
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">@(address.Address)</MudText>
|
||||
<MudSpacer />
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">کد پستی: @(address.PostalCode)</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
</MudStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudStack AlignItems="AlignItems.Center" Class="py-4">
|
||||
<MudIcon Icon="@Icons.Material.Filled.LocationOff" Size="Size.Large" Color="Color.Default" />
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-2">آدرسی ثبت نشده است.</MudText>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="() => Navigation.NavigateTo(RouteConstants.Profile.Index)"
|
||||
Class="mt-2">
|
||||
افزودن آدرس
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
}
|
||||
</MudPaper>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
|
||||
@@ -138,36 +207,15 @@
|
||||
FullWidth="true"
|
||||
StartIcon="@Icons.Material.Filled.Payment"
|
||||
OnClick="ProcessPayment"
|
||||
Disabled="@(!CanProceedToPayment || _isProcessingPayment || !_selectedPackage.SupportsDirectPurchase)"
|
||||
Disabled="@(!CanProceedToPayment || _isProcessingPayment)"
|
||||
Class="mt-2">
|
||||
@(_isProcessingPayment ? "در حال پردازش..." : "پرداخت آنلاین")
|
||||
</MudButton>
|
||||
|
||||
@if (_selectedPackage.SupportsDayaPurchase)
|
||||
{
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Tertiary"
|
||||
Size="Size.Large"
|
||||
FullWidth="true"
|
||||
StartIcon="@Icons.Material.Filled.Diamond"
|
||||
OnClick="DayaLoanPayment"
|
||||
Disabled="@(!CanProceedToPayment)"
|
||||
Class="mt-2">
|
||||
تأمین اعتبار الماسی دایا
|
||||
</MudButton>
|
||||
}
|
||||
|
||||
@if (!_selectedPackage.SupportsDirectPurchase && !_selectedPackage.SupportsDayaPurchase)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Dense="true" Class="mt-2">
|
||||
این پکیج در حال حاضر قابل خرید نیست.
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
@if (!CanProceedToPayment)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Error" Align="Align.Center">
|
||||
لطفاً پکیج را انتخاب کنید.
|
||||
لطفاً پکیج و آدرس را انتخاب کنید.
|
||||
</MudText>
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
|
||||
using CMSMicroservice.Protobuf.Protos.Package;
|
||||
using CMSMicroservice.Protobuf.Protos.Transactions;
|
||||
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
||||
using CMSMicroservice.Protobuf.Protos.UserOrder;
|
||||
using FrontOffice.Main.Utilities;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using MudBlazor;
|
||||
using Severity = MudBlazor.Severity;
|
||||
|
||||
@@ -9,12 +13,17 @@ namespace FrontOffice.Main.Pages;
|
||||
|
||||
public partial class Checkout
|
||||
{
|
||||
[Inject] private PackageService PackageService { get; set; } = default!;
|
||||
[Inject] private PackageContract.PackageContractClient PackageClient { get; set; } = default!;
|
||||
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
||||
[Inject] private UserOrderContract.UserOrderContractClient UserOrderContract { get; set; } = default!;
|
||||
[Inject] private TransactionsContract.TransactionsContractClient TransactionContract { get; set; } = default!;
|
||||
|
||||
[Parameter] public long? PackageId { get; set; }
|
||||
|
||||
private Pack? _selectedPackage;
|
||||
private List<CustomerAddressModel> _addresses = new();
|
||||
private CustomerAddressModel? _selectedAddress;
|
||||
private bool _isLoadingAddresses;
|
||||
private bool _isProcessingPayment;
|
||||
|
||||
// Discount code
|
||||
@@ -25,11 +34,16 @@ public partial class Checkout
|
||||
private long _discountAmount;
|
||||
private long _finalPrice;
|
||||
|
||||
private bool CanProceedToPayment => _selectedPackage != null;
|
||||
// Address management
|
||||
private bool _isSettingDefaultAddress;
|
||||
private long? _settingDefaultAddressId;
|
||||
|
||||
private bool CanProceedToPayment => _selectedPackage != null && _selectedAddress != null;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadPackageDetails();
|
||||
await LoadAddresses();
|
||||
}
|
||||
|
||||
private async Task LoadPackageDetails()
|
||||
@@ -38,18 +52,15 @@ public partial class Checkout
|
||||
{
|
||||
try
|
||||
{
|
||||
var packages = await PackageService.GetAllPackagesAsync();
|
||||
var pkg = packages.FirstOrDefault(p => p.Id == PackageId.Value);
|
||||
if (pkg != null)
|
||||
var response = await PackageClient.GetPackageAsync(new() { Id = PackageId.Value });
|
||||
if (response != null)
|
||||
{
|
||||
_selectedPackage = new Pack(
|
||||
Id: pkg.Id,
|
||||
Title: pkg.Title,
|
||||
Body: pkg.Description,
|
||||
Image: pkg.ImageUrl,
|
||||
Price: pkg.Price,
|
||||
SupportsDirectPurchase: pkg.SupportsDirectPurchase,
|
||||
SupportsDayaPurchase: pkg.SupportsDayaPurchase
|
||||
Id: response.Id,
|
||||
Title: response.Title,
|
||||
Body: response.Description,
|
||||
Image: response.ImagePath ?? string.Empty,
|
||||
Price: response.Price
|
||||
);
|
||||
_finalPrice = _selectedPackage.Price;
|
||||
}
|
||||
@@ -61,6 +72,60 @@ public partial class Checkout
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadAddresses()
|
||||
{
|
||||
_isLoadingAddresses = true;
|
||||
try
|
||||
{
|
||||
var response = await UserAddressContract.GetCustomerAddressesAsync(request: new());
|
||||
if (response?.Models?.Any() == true)
|
||||
{
|
||||
_addresses = response.Models.ToList();
|
||||
// Select default address if available
|
||||
_selectedAddress = _addresses.FirstOrDefault(a => a.IsDefault) ?? _addresses.First();
|
||||
}
|
||||
else
|
||||
{
|
||||
_addresses = new List<CustomerAddressModel>();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری آدرسها: {ex.Message}", Severity.Error);
|
||||
_addresses = new List<CustomerAddressModel>();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoadingAddresses = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetAddressAsDefault(long addressId)
|
||||
{
|
||||
if (_isSettingDefaultAddress) return;
|
||||
|
||||
_isSettingDefaultAddress = true;
|
||||
_settingDefaultAddressId = addressId;
|
||||
|
||||
try
|
||||
{
|
||||
await UserAddressContract.SetAddressAsDefaultAsync(new() { Id = addressId });
|
||||
await LoadAddresses(); // Reload addresses to reflect the change
|
||||
Snackbar.Add("آدرس پیشفرض با موفقیت تغییر یافت.", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در تغییر آدرس پیشفرض: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSettingDefaultAddress = false;
|
||||
_settingDefaultAddressId = null;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplyDiscountCode()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_discountCode))
|
||||
@@ -92,7 +157,7 @@ public partial class Checkout
|
||||
_finalPrice = _selectedPackage!.Price;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
_discountMessage = "خطا در اعمال کد تخفیف.";
|
||||
_discountApplied = false;
|
||||
@@ -108,11 +173,12 @@ public partial class Checkout
|
||||
|
||||
private async Task ProcessPayment()
|
||||
{
|
||||
if (_isProcessingPayment) return;
|
||||
Snackbar.Add("درگاه پرداخت متصل نیست! لطفا در زمان دیگری مجددا تلاش فرمایید!", Severity.Warning);
|
||||
return;
|
||||
|
||||
if (!CanProceedToPayment || _selectedPackage == null)
|
||||
if (!CanProceedToPayment || _selectedPackage == null || _selectedAddress == null)
|
||||
{
|
||||
Snackbar.Add("لطفاً پکیج را انتخاب کنید.", Severity.Warning);
|
||||
Snackbar.Add("لطفاً پکیج و آدرس را انتخاب کنید.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -120,26 +186,33 @@ public partial class Checkout
|
||||
|
||||
try
|
||||
{
|
||||
var response = await PackageClient.CustomerPurchasePackageAsync(new CustomerPurchasePackageRequest
|
||||
// Step 1: Create payment request
|
||||
var paymentRequest = new CustomerPaymentRequestRequest
|
||||
{
|
||||
Amount = _finalPrice,
|
||||
CallbackUrl = $"{Navigation.BaseUri}checkout/callback",
|
||||
Description = $"خرید پکیج {_selectedPackage.Title}",
|
||||
Currency = CurrencyEnum.Irt,
|
||||
Type = TransactionTypeEnum.Real
|
||||
};
|
||||
|
||||
var paymentResponse = await TransactionContract.CustomerPaymentRequestAsync(paymentRequest);
|
||||
|
||||
if (string.IsNullOrEmpty(paymentResponse.PaymentGWUrl))
|
||||
Snackbar.Add("آدرس درگاه پرداخت دریافت نشد.", Severity.Error);
|
||||
|
||||
// Step 2: Create user order
|
||||
var orderRequest = new CreateNewUserOrderRequest
|
||||
{
|
||||
Amount = _finalPrice,
|
||||
PackageId = _selectedPackage.Id,
|
||||
PurchaseMethod = PurchaseMethodEnum.PurchaseMethodGateway
|
||||
});
|
||||
PaymentStatus = CMSMicroservice.Protobuf.Protos.PaymentStatus.Pending
|
||||
};
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
Snackbar.Add(response.Message ?? "خطا در آغاز فرآیند پرداخت", Severity.Error);
|
||||
return;
|
||||
}
|
||||
var orderResponse = await UserOrderContract.CreateNewUserOrderAsync(orderRequest);
|
||||
|
||||
if (!string.IsNullOrEmpty(response.PaymentGatewayUrl))
|
||||
{
|
||||
Navigation.NavigateTo(response.PaymentGatewayUrl, forceLoad: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add("خطا در دریافت آدرس درگاه پرداخت", Severity.Error);
|
||||
}
|
||||
// Step 3: Redirect to payment gateway
|
||||
Navigation.NavigateTo(paymentResponse.PaymentGWUrl);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -152,17 +225,5 @@ public partial class Checkout
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DayaLoanPayment()
|
||||
{
|
||||
if (_selectedPackage == null)
|
||||
{
|
||||
Snackbar.Add("لطفاً پکیج را انتخاب کنید.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var url = "https://dayadiamond.ir/profile/creditpurchase/?merchantcode=56146364";
|
||||
await JSRuntime.InvokeVoidAsync("open", url, "_blank");
|
||||
}
|
||||
|
||||
private record Pack(long Id, string Title, string Body, string Image, long Price, bool SupportsDirectPurchase = true, bool SupportsDayaPurchase = false);
|
||||
}
|
||||
private record Pack(long Id, string Title, string Body, string Image, long Price);
|
||||
}
|
||||
@@ -3,12 +3,7 @@
|
||||
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
||||
<MudText Typo="Typo.h6" Class="mb-3">فعالسازی یا تمدید عضویت</MudText>
|
||||
|
||||
@* ── G7: راهنمای هزینه فعالسازی (Q28) ── *@
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info">
|
||||
هزینه تخمینی بر اساس پکیج انتخابی محاسبه میشود. هر پکیج قیمت و شرایط فعالسازی متفاوتی دارد.
|
||||
</MudAlert>
|
||||
|
||||
|
||||
<MudForm @ref="_form" @bind-IsValid="_formIsValid">
|
||||
<MudGrid Spacing="2">
|
||||
<MudItem xs="12" sm="6">
|
||||
|
||||
@@ -10,7 +10,6 @@ public partial class ActivationSection : ComponentBase
|
||||
[Inject] private ClubMembershipService ClubService { get; set; } = default!;
|
||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||
[Inject] private UserContract.UserContractClient UserContract { get; set; } = default!;
|
||||
[Inject] private PackageService PackageService { get; set; } = default!;
|
||||
|
||||
[Parameter] public EventCallback OnActivationSuccess { get; set; }
|
||||
|
||||
@@ -22,18 +21,6 @@ public partial class ActivationSection : ComponentBase
|
||||
private long _packageId = 1;
|
||||
private int _durationMonths = 1;
|
||||
private string? _activationCode;
|
||||
private long _basePackagePrice;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var packages = await PackageService.GetAllPackagesAsync();
|
||||
var basePackage = packages.FirstOrDefault(p => p.IsBasePackage) ?? packages.FirstOrDefault();
|
||||
_basePackagePrice = basePackage?.Price ?? 0;
|
||||
}
|
||||
catch { /* fallback to 0 */ }
|
||||
}
|
||||
|
||||
private async Task HandleActivateAsync()
|
||||
{
|
||||
@@ -88,8 +75,9 @@ public partial class ActivationSection : ComponentBase
|
||||
|
||||
private string GetEstimatedCost()
|
||||
{
|
||||
if (_basePackagePrice <= 0) return "در حال بارگذاری...";
|
||||
var total = _basePackagePrice * _durationMonths;
|
||||
// فرمول تقریبی: 56M per month (base amount from BFF implementation)
|
||||
var baseAmount = 56_000_000;
|
||||
var total = baseAmount * _durationMonths;
|
||||
return $"{total:N0} تومان";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,6 @@
|
||||
<MudStack Spacing="3">
|
||||
<PageHeader Title="عضویت باشگاه مشتریان" BackHref="@RouteConstants.Profile.Index" />
|
||||
|
||||
@* ── G6: راهنمای قرارداد باشگاه (Q28) ── *@
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Icon="@Icons.Material.Filled.Gavel">
|
||||
قرارداد باشگاه مشتریان فقط یکبار امضا میشود و با هر بار خرید پکیج جدید نیازی به امضای مجدد نیست.
|
||||
</MudAlert>
|
||||
|
||||
@if (_isLoading)
|
||||
{
|
||||
<LoadingState Message="در حال دریافت اطلاعات عضویت..." />
|
||||
@@ -69,8 +64,8 @@
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="pa-2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Color="Color.Success" Size="Size.Large" />
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.subtitle1">شارژ کیف پول فروشگاه اعتباری</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Default">شارژ برابر ارزش پکیج فعال در کیف پول فروشگاه اعتباری</MudText>
|
||||
<MudText Typo="Typo.subtitle1">شارژ کیف پول فروشگاه تخفیفی</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Default">شارژ ۵۶ میلیون تومان کیف پول فروشگاه تخفیفی</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
<MudDivider />
|
||||
|
||||
@@ -10,15 +10,10 @@
|
||||
<MudStack Spacing="3">
|
||||
<PageHeader Title="پاداشهای من" BackHref="@RouteConstants.Profile.Index" />
|
||||
|
||||
@* ── G5: توضیح محاسبه پاداش (Q28) ── *@
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Icon="@Icons.Material.Filled.Info">
|
||||
پاداش هفتگی بر اساس هر پکیج جداگانه محاسبه میشود. هر پکیج Pool پورسانت مستقل دارد و Carryover بر اساس پکیج زیرمجموعههاست.
|
||||
</MudAlert>
|
||||
|
||||
<!-- فیلترها -->
|
||||
<MudPaper Elevation="2" Class="pa-3 rounded-lg">
|
||||
<MudGrid Spacing="2">
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<WeekSelector @ref="_weekSelector"
|
||||
@bind-Value="_selectedWeekDefinition"
|
||||
Label="انتخاب هفته"
|
||||
@@ -28,7 +23,7 @@
|
||||
Clearable="true"
|
||||
OnlyActive="false" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudSelect T="string" @bind-Value="_filterStatus" Label="وضعیت" Variant="Variant.Outlined">
|
||||
<MudSelectItem T="string" Value="@string.Empty">همه</MudSelectItem>
|
||||
<MudSelectItem T="string" Value="@("Pending")">در انتظار</MudSelectItem>
|
||||
@@ -39,16 +34,7 @@
|
||||
<MudSelectItem T="string" Value="@("Cancelled")">لغو شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect T="long?" @bind-Value="_filterPackageId" Label="پکیج" Variant="Variant.Outlined" Clearable="true">
|
||||
<MudSelectItem T="long?" Value="@((long?)null)">همه پکیجها</MudSelectItem>
|
||||
@foreach (var pkg in _packages)
|
||||
{
|
||||
<MudSelectItem T="long?" Value="@((long?)pkg.Id)">@pkg.Title</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudItem xs="12" sm="12" md="4">
|
||||
<MudStack Row="true" Spacing="2" Style="height: 100%;" AlignItems="AlignItems.End">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ApplyFiltersAsync" StartIcon="@Icons.Material.Filled.FilterList" FullWidth="true">
|
||||
اعمال فیلتر
|
||||
@@ -101,7 +87,6 @@
|
||||
<MudTable Items="_payouts" Hover="true" Striped="true" Dense="true" FixedHeader="true" Height="400px">
|
||||
<HeaderContent>
|
||||
<MudTh>هفته</MudTh>
|
||||
<MudTh>پکیج</MudTh>
|
||||
<MudTh>امتیاز</MudTh>
|
||||
<MudTh>مبلغ (تومان)</MudTh>
|
||||
<MudTh>وضعیت</MudTh>
|
||||
@@ -112,11 +97,6 @@
|
||||
<MudTd DataLabel="هفته">
|
||||
<MudChip T="string" Color="Color.Primary" Size="Size.Small" Variant="Variant.Outlined">@context.WeekDisplayName</MudChip>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="پکیج">
|
||||
<MudChip T="string" Color="Color.Tertiary" Size="Size.Small">
|
||||
@(string.IsNullOrEmpty(context.PackageTitle) ? "-" : context.PackageTitle)
|
||||
</MudChip>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="امتیاز">@context.BalancesEarned</MudTd>
|
||||
<MudTd DataLabel="مبلغ">
|
||||
<MudText Color="Color.Success"><strong>@context.AmountFormatted</strong></MudText>
|
||||
@@ -147,10 +127,6 @@
|
||||
<MudChip T="string" Color="Color.Primary" Size="Size.Small" Variant="Variant.Outlined">@payout.WeekDisplayName</MudChip>
|
||||
<MudChip T="string" Color="@GetStatusColor(payout.StatusColor)" Size="Size.Small">@payout.StatusText</MudChip>
|
||||
</MudStack>
|
||||
@if (!string.IsNullOrEmpty(payout.PackageTitle))
|
||||
{
|
||||
<MudChip T="string" Color="Color.Tertiary" Size="Size.Small">@payout.PackageTitle</MudChip>
|
||||
}
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.h6" Color="Color.Success">@payout.AmountFormatted</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">امتیاز: @payout.BalancesEarned</MudText>
|
||||
|
||||
@@ -8,10 +8,8 @@ namespace FrontOffice.Main.Pages.Commission;
|
||||
public partial class CommissionDashboardPage : ComponentBase
|
||||
{
|
||||
[Inject] private CommissionService CommissionService { get; set; } = default!;
|
||||
[Inject] private PackageService PackageService { get; set; } = default!;
|
||||
|
||||
private List<CommissionPayoutDto> _payouts = new();
|
||||
private List<PackageDto> _packages = new();
|
||||
private int _totalCount;
|
||||
private long _totalAmount;
|
||||
private int _pageNumber = 1;
|
||||
@@ -21,26 +19,12 @@ public partial class CommissionDashboardPage : ComponentBase
|
||||
private WeekSelector? _weekSelector;
|
||||
private WeekDefinitionDto? _selectedWeekDefinition;
|
||||
private string _filterStatus = string.Empty;
|
||||
private long? _filterPackageId;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadPackagesAsync();
|
||||
await LoadPayoutsAsync();
|
||||
}
|
||||
|
||||
private async Task LoadPackagesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_packages = await PackageService.GetAllPackagesAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_packages = new List<PackageDto>();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadPayoutsAsync()
|
||||
{
|
||||
try
|
||||
@@ -51,8 +35,7 @@ public partial class CommissionDashboardPage : ComponentBase
|
||||
weekDefinitionId,
|
||||
_filterStatus,
|
||||
_pageNumber,
|
||||
_pageSize,
|
||||
_filterPackageId
|
||||
_pageSize
|
||||
);
|
||||
_payouts = result.Payouts;
|
||||
_totalCount = result.TotalCount;
|
||||
@@ -78,7 +61,6 @@ public partial class CommissionDashboardPage : ComponentBase
|
||||
{
|
||||
_selectedWeekDefinition = null;
|
||||
_filterStatus = string.Empty;
|
||||
_filterPackageId = null;
|
||||
_pageNumber = 1;
|
||||
await LoadPayoutsAsync();
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<!-- انتخاب هفته -->
|
||||
<MudPaper Elevation="2" Class="pa-3 rounded-lg">
|
||||
<MudGrid Spacing="2">
|
||||
<MudItem xs="8" sm="4" md="4">
|
||||
<MudItem xs="8" sm="6" md="6">
|
||||
<WeekSelector @ref="_weekSelector"
|
||||
@bind-Value="_selectedWeekDefinition"
|
||||
Label="انتخاب هفته"
|
||||
@@ -21,15 +21,15 @@
|
||||
Dense="false"
|
||||
OnlyActive="true" />
|
||||
</MudItem>
|
||||
<MudItem xs="4" sm="4" md="4">
|
||||
<MudSelect T="long?" @bind-Value="_filterPackageId" Label="پکیج" Variant="Variant.Outlined" Clearable="true">
|
||||
<MudSelectItem T="long?" Value="@((long?)null)">همه پکیجها</MudSelectItem>
|
||||
@foreach (var pkg in _packages)
|
||||
{
|
||||
<MudSelectItem T="long?" Value="@((long?)pkg.Id)">@pkg.Title</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
@* <MudItem xs="4" sm="3" md="3"> *@
|
||||
@* <MudButton Variant="Variant.Outlined" *@
|
||||
@* OnClick="LoadCurrentWeekAsync" *@
|
||||
@* StartIcon="@Icons.Material.Filled.Today" *@
|
||||
@* FullWidth="true" *@
|
||||
@* Style="height: 56px;"> *@
|
||||
@* هفته جاری *@
|
||||
@* </MudButton> *@
|
||||
@* </MudItem> *@
|
||||
<MudItem xs="4" sm="3" md="3" Class="pt-6">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
@@ -66,12 +66,6 @@
|
||||
<MudItem xs="6" sm="4">
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">پایان: <strong>@_weeklyBalance.EndDatePersian</strong></MudText>
|
||||
</MudItem>
|
||||
@if (!string.IsNullOrEmpty(_weeklyBalance.PackageTitle))
|
||||
{
|
||||
<MudItem xs="12" sm="12">
|
||||
<MudChip T="string" Color="Color.Info" Size="Size.Small" Icon="@Icons.Material.Filled.Inventory2">@_weeklyBalance.PackageTitle</MudChip>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ namespace FrontOffice.Main.Pages.Commission;
|
||||
public partial class WeeklyBalancePage : ComponentBase
|
||||
{
|
||||
[Inject] private CommissionService CommissionService { get; set; } = default!;
|
||||
[Inject] private PackageService PackageService { get; set; } = default!;
|
||||
[Inject] private ISnackbar SnackbarService { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
@@ -18,8 +17,6 @@ public partial class WeeklyBalancePage : ComponentBase
|
||||
private WeekSelector? _weekSelector;
|
||||
private WeekDefinitionDto? _selectedWeekDefinition;
|
||||
private WeeklyBalanceDto? _weeklyBalance;
|
||||
private List<PackageDto> _packages = new();
|
||||
private long? _filterPackageId;
|
||||
private bool _isLoading = true;
|
||||
private bool _hasError = false;
|
||||
|
||||
@@ -37,8 +34,6 @@ public partial class WeeklyBalancePage : ComponentBase
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await LoadPackagesAsync();
|
||||
|
||||
// Wait for WeekSelector to initialize and load its cached weeks
|
||||
if (_weekSelector != null)
|
||||
{
|
||||
@@ -93,7 +88,7 @@ public partial class WeeklyBalancePage : ComponentBase
|
||||
|
||||
long? weekDefinitionId = _selectedWeekDefinition?.Id;
|
||||
|
||||
_weeklyBalance = await CommissionService.GetMyWeeklyBalanceAsync(weekDefinitionId, _filterPackageId);
|
||||
_weeklyBalance = await CommissionService.GetMyWeeklyBalanceAsync(weekDefinitionId);
|
||||
|
||||
// Update week display name from selected definition if available
|
||||
if (_selectedWeekDefinition != null && _weeklyBalance != null)
|
||||
@@ -118,18 +113,6 @@ public partial class WeeklyBalancePage : ComponentBase
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadPackagesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_packages = await PackageService.GetAllPackagesAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_packages = new();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadCurrentWeekAsync()
|
||||
{
|
||||
if (_weekSelector != null)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@attribute [Route(RouteConstants.Contact.Index)]
|
||||
@inject SitePageSettingsService PageSettingsService
|
||||
@inject SitePageService SitePageService
|
||||
|
||||
<PageTitle>ارتباط با ما | کارا بازار سلامت</PageTitle>
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
<div>
|
||||
<MudText Typo="Typo.body2" >آدرس</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
||||
@(_settings?.Address ?? "کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶")
|
||||
@(_contactAddress ?? "کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶")
|
||||
</MudText>
|
||||
</div>
|
||||
</MudStack>
|
||||
@@ -142,7 +142,7 @@
|
||||
<MudIcon Icon="@Icons.Material.Filled.Phone" Color="Color.Success" Size="Size.Large" />
|
||||
<div>
|
||||
<MudText Typo="Typo.body2" >تلفن</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">@(_settings?.Phone ?? "026-34233563")</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">@(_contactPhone ?? "026-34233563")</MudText>
|
||||
</div>
|
||||
</MudStack>
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
<MudIcon Icon="@Icons.Material.Filled.Email" Color="Color.Info" Size="Size.Large" />
|
||||
<div>
|
||||
<MudText Typo="Typo.body2" >ایمیل</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">@(_settings?.Email ?? "info@kbs1.co")</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">@(_contactEmail ?? "info@kbs1.co")</MudText>
|
||||
</div>
|
||||
</MudStack>
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
<div>
|
||||
<MudText Typo="Typo.body2" >ساعات کاری</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
||||
@((MarkupString)(_settings?.WorkingHours ?? "شنبه تا پنجشنبه<br />۹ صبح تا ۶ عصر"))
|
||||
@((MarkupString)(_contactHours ?? "شنبه تا پنجشنبه<br />۹ صبح تا ۶ عصر"))
|
||||
</MudText>
|
||||
</div>
|
||||
</MudStack>
|
||||
@@ -174,7 +174,7 @@
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Inherit"
|
||||
StartIcon="@Icons.Custom.Brands.Telegram"
|
||||
Href="@(_settings?.TelegramUrl ?? "https://t.me/kbs1")"
|
||||
Href="@(_socialTelegram ?? "https://t.me/kbs1")"
|
||||
Target="_blank"
|
||||
FullWidth="true">
|
||||
تلگرام
|
||||
@@ -183,7 +183,7 @@
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Inherit"
|
||||
StartIcon="@Icons.Custom.Brands.Instagram"
|
||||
Href="@(_settings?.InstagramUrl ?? "https://instagram.com/kbs1")"
|
||||
Href="@(_socialInstagram ?? "https://instagram.com/kbs1")"
|
||||
Target="_blank"
|
||||
FullWidth="true">
|
||||
اینستاگرام
|
||||
@@ -192,7 +192,7 @@
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Inherit"
|
||||
StartIcon="@Icons.Custom.Brands.LinkedIn"
|
||||
Href="@(_settings?.LinkedinUrl ?? "https://linkedin.com/company/kbs1")"
|
||||
Href="@(_socialLinkedin ?? "https://linkedin.com/company/kbs1")"
|
||||
Target="_blank"
|
||||
FullWidth="true">
|
||||
لینکدین
|
||||
@@ -201,7 +201,7 @@
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Inherit"
|
||||
StartIcon="@Icons.Custom.Brands.WhatsApp"
|
||||
Href="@(_settings?.WhatsappUrl ?? "https://wa.me/989123456789")"
|
||||
Href="@(_socialWhatsapp ?? "https://wa.me/989123456789")"
|
||||
Target="_blank"
|
||||
FullWidth="true">
|
||||
واتساپ
|
||||
@@ -226,7 +226,7 @@
|
||||
<MudIcon Icon="@Icons.Material.Filled.Map" Size="Size.Large" Style="color: white;" />
|
||||
<MudText Typo="Typo.h6" Style="color: white;">نقشه موقعیت مکانی</MudText>
|
||||
<MudText Typo="Typo.body2" Style="color: rgba(255,255,255,0.8);" Align="Align.Center">
|
||||
@(_settings?.Address ?? "کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶")
|
||||
@(_contactAddress ?? "کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶")
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</div>
|
||||
|
||||
@@ -12,19 +12,52 @@ public partial class Contact
|
||||
private bool _isSubmitting;
|
||||
private readonly ContactFormValidator _contactFormValidator = new();
|
||||
|
||||
// Dynamic CMS data (new simplified system)
|
||||
private PageSettingsDto? _page;
|
||||
private ContactSettings? _settings;
|
||||
// Dynamic CMS data
|
||||
private SitePageDto? _page;
|
||||
private string? _contactAddress;
|
||||
private string? _contactPhone;
|
||||
private string? _contactEmail;
|
||||
private string? _contactHours;
|
||||
private string? _socialTelegram;
|
||||
private string? _socialInstagram;
|
||||
private string? _socialLinkedin;
|
||||
private string? _socialWhatsapp;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_page = await PageSettingsService.GetPageAsync("contact");
|
||||
_page = await SitePageService.GetByKeyAsync("contact");
|
||||
|
||||
if (_page != null)
|
||||
{
|
||||
_settings = _page.GetSettings<ContactSettings>();
|
||||
// Load contact info from CMS section
|
||||
var contactInfoSection = _page.GetSection("contact-info");
|
||||
if (contactInfoSection != null)
|
||||
{
|
||||
var contactData = contactInfoSection.GetExtraData<ContactInfoData>();
|
||||
if (contactData != null)
|
||||
{
|
||||
_contactAddress = contactData.Address;
|
||||
_contactPhone = contactData.Phone;
|
||||
_contactEmail = contactData.Email;
|
||||
_contactHours = contactData.Hours;
|
||||
}
|
||||
}
|
||||
|
||||
// Load social media links from CMS section
|
||||
var socialSection = _page.GetSection("social-media");
|
||||
if (socialSection != null)
|
||||
{
|
||||
var socialData = socialSection.GetExtraData<SocialMediaData>();
|
||||
if (socialData != null)
|
||||
{
|
||||
_socialTelegram = socialData.Telegram;
|
||||
_socialInstagram = socialData.Instagram;
|
||||
_socialLinkedin = socialData.Linkedin;
|
||||
_socialWhatsapp = socialData.Whatsapp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -78,12 +111,12 @@ public partial class Contact
|
||||
|
||||
private void CallSupport()
|
||||
{
|
||||
Snackbar.Add($"شماره تماس: {_settings?.Phone ?? "026-34233563"}", Severity.Info);
|
||||
Snackbar.Add($"شماره تماس: {_contactPhone ?? "026-34233563"}", Severity.Info);
|
||||
}
|
||||
|
||||
private void SendEmail()
|
||||
{
|
||||
Snackbar.Add($"ایمیل: {_settings?.Email ?? "info@kbs1.co"}", Severity.Info);
|
||||
Snackbar.Add($"ایمیل: {_contactEmail ?? "info@kbs1.co"}", Severity.Info);
|
||||
}
|
||||
|
||||
public class ContactForm
|
||||
@@ -109,16 +142,20 @@ public partial class Contact
|
||||
}
|
||||
}
|
||||
|
||||
// DTO class for SettingsJson deserialization
|
||||
private class ContactSettings
|
||||
// DTO classes for ExtraData JSON deserialization
|
||||
private class ContactInfoData
|
||||
{
|
||||
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; }
|
||||
public string? Hours { get; set; }
|
||||
}
|
||||
|
||||
private class SocialMediaData
|
||||
{
|
||||
public string? Telegram { get; set; }
|
||||
public string? Instagram { get; set; }
|
||||
public string? Linkedin { get; set; }
|
||||
public string? Whatsapp { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
@attribute [Route(RouteConstants.DiscountStore.Cart)]
|
||||
|
||||
<PageTitle>سبد خرید اعتباری</PageTitle>
|
||||
<PageTitle>سبد خرید تخفیفی</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
|
||||
<MudStack Spacing="3">
|
||||
<PageHeader Title="سبد خرید اعتباری" BackHref="@RouteConstants.DiscountStore.Products" />
|
||||
<PageHeader Title="سبد خرید تخفیفی" BackHref="@RouteConstants.DiscountStore.Products" />
|
||||
|
||||
@if (DiscountCart.Items.Count == 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info">سبد خرید اعتباری شما خالی است.</MudAlert>
|
||||
<MudAlert Severity="Severity.Info">سبد خرید تخفیفی شما خالی است.</MudAlert>
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.ArrowBack"
|
||||
OnClick="() => Navigation.NavigateTo(RouteConstants.DiscountStore.Products)">
|
||||
بازگشت به فروشگاه اعتباری
|
||||
بازگشت به فروشگاه تخفیفی
|
||||
</MudButton>
|
||||
}
|
||||
else
|
||||
@@ -23,9 +23,9 @@
|
||||
<HeaderContent>
|
||||
<MudTh>محصول</MudTh>
|
||||
<MudTh>قیمت واحد</MudTh>
|
||||
<MudTh>سقف اعتبار</MudTh>
|
||||
<MudTh>سقف تخفیف</MudTh>
|
||||
<MudTh>تعداد</MudTh>
|
||||
<MudTh>اعتبار</MudTh>
|
||||
<MudTh>تخفیف</MudTh>
|
||||
<MudTh>قیمت نهایی</MudTh>
|
||||
<MudTh></MudTh>
|
||||
</HeaderContent>
|
||||
@@ -33,14 +33,14 @@
|
||||
<MudTd>
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
||||
<AppImage Path="@GetImageUrl(context.ImageUrl)" Alt="@context.Title"
|
||||
ImgWidth="64" ImgHeight="64" Class="rounded-lg" ObjectFit="ObjectFit.Cover" />
|
||||
ImgWidth="64" ImgHeight="64" Class="product-thumb" />
|
||||
<MudText>@context.Title</MudText>
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
<MudTd>@FormatPrice(context.UnitPrice)</MudTd>
|
||||
<MudTd>
|
||||
<MudChip T="string" Color="Color.Error" Variant="Variant.Outlined" Size="Size.Small">
|
||||
@context.MaxDiscountPercent% اعتبار
|
||||
@context.MaxDiscountPercent% تخفیف
|
||||
</MudChip>
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
@@ -82,11 +82,11 @@
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
||||
<AppImage Path="@GetImageUrl(item.ImageUrl)" Alt="@item.Title"
|
||||
ImgWidth="50" ImgHeight="50" Class="rounded-lg" ObjectFit="ObjectFit.Cover" />
|
||||
ImgWidth="50" ImgHeight="50" Class="rounded-circle" />
|
||||
<MudText Typo="Typo.subtitle2">@item.Title</MudText>
|
||||
</MudStack>
|
||||
<MudChip T="string" Color="Color.Error" Variant="Variant.Outlined" Size="Size.Small">
|
||||
@item.MaxDiscountPercent% اعتبار
|
||||
@item.MaxDiscountPercent% تخفیف
|
||||
</MudChip>
|
||||
</MudStack>
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
@if (item.DiscountAmount > 0)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Success">اعتبار: @FormatPrice(item.DiscountAmount)-</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Success">تخفیف: @FormatPrice(item.DiscountAmount)-</MudText>
|
||||
}
|
||||
<MudText>جمع: @FormatPrice(item.FinalPrice)</MudText>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error"
|
||||
@@ -131,20 +131,13 @@
|
||||
@if (DiscountCart.TotalDiscount > 0)
|
||||
{
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.caption" Color="Color.Success">سقف اعتبار قابل اعمال:</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Success">سقف تخفیف قابل اعمال:</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Success">@FormatPrice(DiscountCart.TotalDiscount)- تومان</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
@if (VAT.IsEnabled)
|
||||
{
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">مالیات ارزش افزوده (@VAT.VatPercentage%):</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">@FormatPrice(VatAmount)+ تومان</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.h6" Class="fw-bold">حداقل پرداخت:</MudText>
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary" Class="fw-bold">@FormatPrice(TotalWithVat) تومان</MudText>
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary" Class="fw-bold">@FormatPrice(DiscountCart.FinalPrice) تومان</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
|
||||
@@ -168,21 +161,14 @@
|
||||
@if (DiscountCart.TotalDiscount > 0)
|
||||
{
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2" Color="Color.Success">سقف اعتبار قابل اعمال:</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Success">سقف تخفیف قابل اعمال:</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Success">@FormatPrice(DiscountCart.TotalDiscount)- تومان</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
@if (VAT.IsEnabled)
|
||||
{
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">مالیات بر ارزش افزوده (@VAT.VatPercentage%):</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">@FormatPrice(VatAmount)+ تومان</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
<MudDivider Class="my-1" />
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.subtitle1" Class="fw-bold">حداقل مبلغ پرداخت درگاه:</MudText>
|
||||
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="fw-bold">@FormatPrice(TotalWithVat) تومان</MudText>
|
||||
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="fw-bold">@FormatPrice(DiscountCart.FinalPrice) تومان</MudText>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
||||
مبلغ دقیق پرداخت در مرحله تسویه مشخص خواهد شد.
|
||||
|
||||
@@ -6,16 +6,9 @@ namespace FrontOffice.Main.Pages.DiscountStore;
|
||||
public partial class Cart : IDisposable
|
||||
{
|
||||
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
|
||||
[Inject] private VATService VAT { get; set; } = default!;
|
||||
[Inject] private AuthDialogService AuthDialogService { get; set; } = default!;
|
||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (!await AuthService.IsAuthenticatedAsync())
|
||||
{
|
||||
await AuthDialogService.ShowAuthDialogAsync();
|
||||
}
|
||||
await DiscountCart.EnsureInitializedAsync();
|
||||
DiscountCart.OnChange += StateHasChanged;
|
||||
}
|
||||
@@ -51,18 +44,6 @@ public partial class Cart : IDisposable
|
||||
Navigation.NavigateTo(RouteConstants.DiscountStore.Checkout);
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await VAT.LoadAsync();
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private long VatAmount => VAT.CalculateVAT(DiscountCart.FinalPrice);
|
||||
private long TotalWithVat => DiscountCart.FinalPrice + VatAmount;
|
||||
|
||||
private static string FormatPrice(long price) => $"{price:N0} ";
|
||||
|
||||
private static string GetImageUrl(string? imageUrl)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.UserAddress
|
||||
@attribute [Route(RouteConstants.DiscountStore.Checkout)]
|
||||
|
||||
<PageTitle>تسویه حساب فروشگاه اعتباری</PageTitle>
|
||||
<PageTitle>تسویه حساب فروشگاه تخفیفی</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
|
||||
<PageHeader Title="تسویه حساب اعتباری" BackHref="@RouteConstants.DiscountStore.Cart" />
|
||||
<PageHeader Title="تسویه حساب تخفیفی" BackHref="@RouteConstants.DiscountStore.Cart" />
|
||||
<MudGrid Spacing="3">
|
||||
<!-- Left Column: Address + Payment Settings -->
|
||||
<MudItem xs="12" md="8">
|
||||
@@ -22,11 +22,9 @@
|
||||
else if (_addresses.Count == 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning">
|
||||
هیچ آدرسی ثبت نشده است. میتوانید همینجا آدرس جدید اضافه کنید.
|
||||
هیچ آدرسی ثبت نشده است. لطفاً از بخش پروفایل آدرس خود را اضافه کنید.
|
||||
</MudAlert>
|
||||
<MudButton Class="mt-2" Variant="Variant.Outlined" Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="OpenAddAddressDialog">افزودن آدرس</MudButton>
|
||||
<MudButton Class="mt-2" Variant="Variant.Outlined" Href="/profile/addresses">افزودن آدرس</MudButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -43,25 +41,13 @@
|
||||
<MudText Typo="Typo.subtitle2">@address.Title</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">@address.Address</MudText>
|
||||
</MudStack>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
@if (address.IsDefault)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Success" Variant="Variant.Outlined" Size="Size.Small">پیشفرض</MudChip>
|
||||
}
|
||||
<span @onclick:stopPropagation="true">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Size="Size.Small"
|
||||
Color="Color.Primary"
|
||||
aria-label="ویرایش آدرس"
|
||||
OnClick="@(() => OpenEditAddressDialog(address))" />
|
||||
</span>
|
||||
</MudStack>
|
||||
@if (address.IsDefault)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Success" Variant="Variant.Outlined">پیشفرض</MudChip>
|
||||
}
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
<MudButton Class="mt-2" Variant="Variant.Text" Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="OpenAddAddressDialog">افزودن آدرس جدید</MudButton>
|
||||
</MudStack>
|
||||
}
|
||||
</MudPaper>
|
||||
@@ -94,7 +80,7 @@
|
||||
<MudStack Spacing="1">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<AppImage Path="@GetImageUrl(item.ImageUrl)" Alt="@item.Title"
|
||||
ImgWidth="40" ImgHeight="40" Class="rounded-lg" ObjectFit="ObjectFit.Cover" />
|
||||
ImgWidth="40" ImgHeight="40" Class="rounded-circle" />
|
||||
<MudText Typo="Typo.subtitle2" Style="flex:1; margin:0 8px;">@item.Title</MudText>
|
||||
<MudText Typo="Typo.subtitle2">@FormatPrice(item.FinalPrice)</MudText>
|
||||
</MudStack>
|
||||
@@ -104,7 +90,7 @@
|
||||
</MudText>
|
||||
@if (item.MaxDiscountPercent > 0)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Error">@item.MaxDiscountPercent% اعتبار</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Error">@item.MaxDiscountPercent% تخفیف</MudText>
|
||||
}
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
@@ -122,17 +108,17 @@
|
||||
</MudStack>
|
||||
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2" Color="Color.Success">اعتبار از کیف اعتباری:</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Success">تخفیف از کیف تخفیفی:</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Success">@FormatPrice(DiscountCart.TotalDiscount)- تومان</MudText>
|
||||
</MudStack>
|
||||
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2">مبلغ پس از اعتبار:</MudText>
|
||||
<MudText Typo="Typo.body2">مبلغ پس از تخفیف:</MudText>
|
||||
<MudText Typo="Typo.body2">@FormatPrice(NetGatewayAmount) تومان</MudText>
|
||||
</MudStack>
|
||||
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">مالیات بر ارزش افزوده (@VAT.VatPercentage٪):</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">مالیات بر ارزش افزوده (۹٪):</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">@FormatPrice(VatAmount)+ تومان</MudText>
|
||||
</MudStack>
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
||||
using FrontOffice.Main.Pages.Profile.Components;
|
||||
using FrontOffice.Main.Utilities;
|
||||
using MudBlazor;
|
||||
|
||||
@@ -11,9 +10,6 @@ public partial class Checkout
|
||||
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
|
||||
[Inject] private DiscountOrderService DiscountOrderService { get; set; } = default!;
|
||||
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
||||
[Inject] private VATService VAT { get; set; } = default!;
|
||||
[Inject] private AuthDialogService AuthDialogService { get; set; } = default!;
|
||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||
|
||||
private List<CustomerAddressModel> _addresses = new();
|
||||
private CustomerAddressModel? _selectedAddress;
|
||||
@@ -22,11 +18,13 @@ public partial class Checkout
|
||||
private string? _notes;
|
||||
private bool _placing;
|
||||
|
||||
/// <summary>مبلغ درگاه قبل از مالیات (جمع کل - اعتبار)</summary>
|
||||
private const decimal VAT_RATE = 0.09m;
|
||||
|
||||
/// <summary>مبلغ درگاه قبل از مالیات (جمع کل - تخفیف)</summary>
|
||||
private long NetGatewayAmount => DiscountCart.TotalPrice - DiscountCart.TotalDiscount;
|
||||
|
||||
/// <summary>مالیات بر ارزش افزوده</summary>
|
||||
private long VatAmount => VAT.CalculateVAT(NetGatewayAmount);
|
||||
/// <summary>مالیات بر ارزش افزوده ۹٪</summary>
|
||||
private long VatAmount => (long)(NetGatewayAmount * VAT_RATE);
|
||||
|
||||
/// <summary>مبلغ نهایی قابل پرداخت (شامل VAT)</summary>
|
||||
private long FinalGatewayAmount => NetGatewayAmount + VatAmount;
|
||||
@@ -35,17 +33,8 @@ public partial class Checkout
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (!await AuthService.IsAuthenticatedAsync())
|
||||
{
|
||||
await AuthDialogService.ShowAuthDialogAsync();
|
||||
}
|
||||
await VAT.LoadAsync();
|
||||
await DiscountCart.EnsureInitializedAsync();
|
||||
var userInfo = await AuthService.GetUserAuthInfo();
|
||||
if (userInfo.HasAddress)
|
||||
await LoadAddresses();
|
||||
else
|
||||
_addresses = new();
|
||||
await LoadAddresses();
|
||||
}
|
||||
|
||||
private async Task LoadAddresses()
|
||||
@@ -62,14 +51,12 @@ public partial class Checkout
|
||||
else
|
||||
{
|
||||
_addresses = new();
|
||||
_selectedAddress = null;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری آدرسها: {ex.Message}", Severity.Error);
|
||||
_addresses = new();
|
||||
_selectedAddress = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -78,30 +65,6 @@ public partial class Checkout
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenAddAddressDialog()
|
||||
{
|
||||
var dialog = await DialogService.ShowAsync<AddAddressDialog>("افزودن آدرس جدید");
|
||||
var result = await dialog.Result;
|
||||
if (result is { Canceled: false })
|
||||
{
|
||||
await AuthService.RefreshTokenAsync();
|
||||
await LoadAddresses();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenEditAddressDialog(CustomerAddressModel address)
|
||||
{
|
||||
var dialog = await DialogService.ShowAsync<EditAddressDialog>("ویرایش آدرس", new DialogParameters<EditAddressDialog>
|
||||
{
|
||||
{ x => x.Model, address }
|
||||
});
|
||||
var result = await dialog.Result;
|
||||
if (result is { Canceled: false })
|
||||
{
|
||||
await LoadAddresses();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PlaceOrder()
|
||||
{
|
||||
if (!CanPlaceOrder || _selectedAddress is null)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@attribute [Route("/discount-store/order/{Id:long}")]
|
||||
|
||||
<PageTitle>جزئیات سفارش اعتباری</PageTitle>
|
||||
<PageTitle>جزئیات سفارش تخفیفی</PageTitle>
|
||||
|
||||
@if (!string.IsNullOrEmpty(_paymentMessage))
|
||||
{
|
||||
@@ -82,8 +82,8 @@ else
|
||||
<MudTh>محصول</MudTh>
|
||||
<MudTh>قیمت واحد</MudTh>
|
||||
<MudTh>تعداد</MudTh>
|
||||
<MudTh>سقف اعتبار</MudTh>
|
||||
<MudTh>اعتبار</MudTh>
|
||||
<MudTh>سقف تخفیف</MudTh>
|
||||
<MudTh>تخفیف</MudTh>
|
||||
<MudTh>قیمت نهایی</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
@@ -131,7 +131,7 @@ else
|
||||
@if (item.DiscountAmount > 0)
|
||||
{
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2" Color="Color.Success">اعتبار (@item.MaxDiscountPercent%):</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Success">تخفیف (@item.MaxDiscountPercent%):</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Success">@FormatPrice(item.DiscountAmount)-</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
@@ -158,19 +158,11 @@ else
|
||||
@if (_order.DiscountBalanceUsed > 0)
|
||||
{
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2" Color="Color.Success">از کیف اعتباری:</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Success">از کیف تخفیفی:</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Success">@FormatPrice(_order.DiscountBalanceUsed)-</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
|
||||
@if (VAT.IsEnabled)
|
||||
{
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">مالیات بر ارزش افزوده (@VAT.VatPercentage%):</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">@FormatPrice(OrderVatAmount)+</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
|
||||
<MudDivider Class="my-1" />
|
||||
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
|
||||
@@ -12,28 +12,12 @@ public partial class OrderDetail
|
||||
public string? PaymentResult { get; set; }
|
||||
|
||||
[Inject] private DiscountOrderService DiscountOrderService { get; set; } = default!;
|
||||
[Inject] private VATService VAT { get; set; } = default!;
|
||||
|
||||
private DiscountOrderDetail? _order;
|
||||
private bool _loading = true;
|
||||
private string? _paymentMessage;
|
||||
private MudBlazor.Severity _paymentSeverity;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await VAT.LoadAsync();
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>مبلغ قبل از مالیات (جمع کل - اعتبار)</summary>
|
||||
private long PreVatGateway => _order is null ? 0 : (_order.TotalPrice - _order.DiscountBalanceUsed);
|
||||
|
||||
/// <summary>مالیات بر ارزش افزوده</summary>
|
||||
private long OrderVatAmount => VAT.CalculateVAT(PreVatGateway);
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// نمایش پیام نتیجه پرداخت (بعد از بازگشت از درگاه)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
@attribute [Route(RouteConstants.DiscountStore.Orders)]
|
||||
|
||||
<PageTitle>سفارشهای فروشگاه اعتباری</PageTitle>
|
||||
<PageTitle>سفارشهای فروشگاه تخفیفی</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
|
||||
<MudStack Spacing="3">
|
||||
<PageHeader Title="سفارشهای اعتباری" BackHref="@RouteConstants.DiscountStore.Products" />
|
||||
<PageHeader Title="سفارشهای تخفیفی" BackHref="@RouteConstants.DiscountStore.Products" />
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
@@ -15,7 +15,7 @@
|
||||
<MudAlert Severity="Severity.Info">هنوز سفارشی ثبت نشده است.</MudAlert>
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Store"
|
||||
Href="@RouteConstants.DiscountStore.Products">
|
||||
مشاهده فروشگاه اعتباری
|
||||
مشاهده فروشگاه تخفیفی
|
||||
</MudButton>
|
||||
}
|
||||
else
|
||||
@@ -28,7 +28,7 @@
|
||||
<MudTh>شماره سفارش</MudTh>
|
||||
<MudTh>تعداد اقلام</MudTh>
|
||||
<MudTh>مبلغ کل</MudTh>
|
||||
<MudTh>از کیف اعتباری</MudTh>
|
||||
<MudTh>از کیف تخفیفی</MudTh>
|
||||
<MudTh>درگاه</MudTh>
|
||||
<MudTh>وضعیت پرداخت</MudTh>
|
||||
<MudTh>وضعیت ارسال</MudTh>
|
||||
@@ -108,7 +108,7 @@
|
||||
@if (order.DiscountBalanceUsed > 0)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Success">
|
||||
از کیف اعتباری: @FormatPrice(order.DiscountBalanceUsed) تومان
|
||||
از کیف تخفیفی: @FormatPrice(order.DiscountBalanceUsed) تومان
|
||||
</MudText>
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@attribute [Route("/discount-store/product/{Id:long}")]
|
||||
|
||||
<PageTitle>@(_product?.Title ?? "محصول اعتباری")</PageTitle>
|
||||
<PageTitle>@(_product?.Title ?? "محصول تخفیفی")</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="py-4 py-md-6">
|
||||
@if (_loading)
|
||||
@@ -11,21 +11,21 @@
|
||||
{
|
||||
<EmptyState Icon="@Icons.Material.Filled.SearchOff"
|
||||
Title="محصول مورد نظر یافت نشد."
|
||||
ActionText="بازگشت به فروشگاه اعتباری"
|
||||
ActionText="بازگشت به فروشگاه تخفیفی"
|
||||
ActionHref="@RouteConstants.DiscountStore.Products" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Breadcrumb -->
|
||||
<PageHeader Title="جزئیات محصول اعتباری" BackHref="@RouteConstants.DiscountStore.Products" />
|
||||
<PageHeader Title="جزئیات محصول تخفیفی" BackHref="@RouteConstants.DiscountStore.Products" />
|
||||
|
||||
<MudGrid Spacing="4">
|
||||
<!-- Image Gallery -->
|
||||
<MudItem xs="12" md="5">
|
||||
<MudPaper Class="pa-2 rounded-xl" Elevation="1">
|
||||
<AppImage Path="@_selectedImage" Alt="@_product.Title"
|
||||
ObjectFit="ObjectFit.Cover"
|
||||
Style="width:100%; aspect-ratio:1/1; border-radius:12px;" />
|
||||
ObjectFit="ObjectFit.Contain"
|
||||
Style="width:100%; max-height:400px; border-radius:12px;" />
|
||||
|
||||
@if (_product.Images.Count > 1)
|
||||
{
|
||||
@@ -63,58 +63,25 @@
|
||||
}
|
||||
|
||||
<!-- Price & Discount -->
|
||||
@if (_isAuthenticated)
|
||||
{
|
||||
<MudPaper Class="pa-4 rounded-lg discount-price-box" Elevation="0">
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">قیمت محصول:</MudText>
|
||||
<MudText Typo="Typo.body1" Class="fw-bold">
|
||||
@($"{_product.Price:N0}") تومان
|
||||
</MudText>
|
||||
</MudStack>
|
||||
@if (VAT.IsEnabled)
|
||||
{
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">مالیات ارزش افزوده (@VAT.VatPercentage%):</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">
|
||||
@($"{VAT.CalculateVAT(_product.Price):N0}") تومان
|
||||
</MudText>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">قیمت با مالیات:</MudText>
|
||||
<MudText Typo="Typo.h5" Color="Color.Primary" Class="fw-bold">
|
||||
@($"{VAT.AddVAT(_product.Price):N0}") تومان
|
||||
</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">قیمت نهایی:</MudText>
|
||||
<MudText Typo="Typo.h5" Color="Color.Primary" Class="fw-bold">
|
||||
@($"{_product.Price:N0}") تومان
|
||||
</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
@if (_product.MaxDiscountPercent > 0)
|
||||
{
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">اعتبار از کیف اعتباری:</MudText>
|
||||
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">
|
||||
@_product.MaxDiscountPercent% (@($"{_product.Price * _product.MaxDiscountPercent / 100:N0}") تومان)
|
||||
</MudChip>
|
||||
</MudStack>
|
||||
}
|
||||
<MudPaper Class="pa-4 rounded-lg discount-price-box" Elevation="0">
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">قیمت محصول:</MudText>
|
||||
<MudText Typo="Typo.h5" Color="Color.Primary" Class="fw-bold">
|
||||
@($"{_product.Price:N0}") تومان
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined" Icon="@Icons.Material.Filled.Lock">
|
||||
برای مشاهده قیمت ابتدا وارد شوید
|
||||
</MudAlert>
|
||||
}
|
||||
@if (_product.MaxDiscountPercent > 0)
|
||||
{
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">تخفیف از کیف تخفیفی:</MudText>
|
||||
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">
|
||||
@_product.MaxDiscountPercent% (@($"{_product.Price * _product.MaxDiscountPercent / 100:N0}") تومان)
|
||||
</MudChip>
|
||||
</MudStack>
|
||||
}
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<!-- Stock & Stats -->
|
||||
<MudStack Row="true" Spacing="3" Class="flex-wrap">
|
||||
|
||||
@@ -9,11 +9,6 @@ public partial class ProductDetail
|
||||
|
||||
[Inject] private DiscountProductService DiscountProductService { get; set; } = default!;
|
||||
[Inject] private DiscountCartService DiscountCartService { get; set; } = default!;
|
||||
[Inject] private VATService VAT { get; set; } = default!;
|
||||
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||
|
||||
private bool _isAuthenticated;
|
||||
|
||||
private DiscountProductDetail? _product;
|
||||
private string _selectedImage = string.Empty;
|
||||
@@ -23,19 +18,9 @@ public partial class ProductDetail
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
|
||||
await LoadProductAsync();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await VAT.LoadAsync();
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadProductAsync()
|
||||
{
|
||||
_loading = true;
|
||||
@@ -64,14 +49,8 @@ public partial class ProductDetail
|
||||
_addingToCart = true;
|
||||
try
|
||||
{
|
||||
var productId = _product.Id;
|
||||
var qty = _quantity;
|
||||
var title = _product.Title;
|
||||
await GuestGate.RunAsync(async () =>
|
||||
{
|
||||
await DiscountCartService.AddAsync(productId, qty);
|
||||
Snackbar.Add($"{title} به سبد خرید اضافه شد", MudBlazor.Severity.Success);
|
||||
});
|
||||
await DiscountCartService.AddAsync(_product.Id, _quantity);
|
||||
Snackbar.Add($"{_product.Title} به سبد خرید اضافه شد", MudBlazor.Severity.Success);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
@attribute [Route(RouteConstants.DiscountStore.Products)]
|
||||
|
||||
<PageTitle>فروشگاه اعتباری</PageTitle>
|
||||
<PageTitle>فروشگاه تخفیفی</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
|
||||
<!-- Hero -->
|
||||
<MudPaper Class="discount-hero pa-6 pa-md-8 mb-4 rounded-xl" Elevation="0">
|
||||
<MudStack AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Loyalty" Size="Size.Large" Class="dash-hero-name" />
|
||||
<MudText Typo="Typo.h5" Align="Align.Center" Class="dash-hero-name">فروشگاه اعتباری</MudText>
|
||||
<MudText Typo="Typo.h5" Align="Align.Center" Class="dash-hero-name">فروشگاه تخفیفی</MudText>
|
||||
<MudText Typo="Typo.body2" Align="Align.Center" Class="dash-hero-sub">
|
||||
خرید با استفاده از موجودی کیف پول اعتباری
|
||||
خرید با استفاده از موجودی کیف پول تخفیفی
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
@@ -64,7 +64,7 @@
|
||||
<MudText Class="mt-2 mud-text-secondary">در حال بارگذاری...</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
else if (_products.Count == 0)
|
||||
else if (_result.Products.Count == 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Class="my-4">محصولی یافت نشد.</MudAlert>
|
||||
}
|
||||
@@ -72,15 +72,14 @@
|
||||
{
|
||||
<!-- Products Grid -->
|
||||
<MudGrid Spacing="1">
|
||||
@foreach (var p in _products)
|
||||
@foreach (var p in _result.Products)
|
||||
{
|
||||
<MudItem xs="6" sm="6" md="3"
|
||||
onclick="@(() => NavigateToProduct(p.Id))">
|
||||
<div id="@($"shop-product-{p.Id}")" class="h-100">
|
||||
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden"
|
||||
Style="cursor:pointer;">
|
||||
Style="cursor:pointer;height: 300px">
|
||||
<MudCardContent Class="d-flex flex-column pa-1 h-100">
|
||||
<div style="aspect-ratio:1/1;width:100%;background-image: url('@(string.IsNullOrWhiteSpace(p.ThumbnailUrl) ? "/images/product-placeholder.svg" : p.ThumbnailUrl)');background-size: cover; background-position: center;border-radius: 0.5rem; position: relative;">
|
||||
<div style="height: 60%;background-image: url('@(string.IsNullOrWhiteSpace(p.ThumbnailUrl) ? "/images/product-placeholder.svg" : p.ThumbnailUrl)');background-size: cover; background-position: center;border-radius: 0.5rem; position: relative;">
|
||||
@if (p.RemainingCount <= 0)
|
||||
{
|
||||
<div style="position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;border-radius:0.5rem;">
|
||||
@@ -98,25 +97,13 @@
|
||||
{
|
||||
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small"
|
||||
Style="position:absolute;top:8px;left:8px;">
|
||||
@p.MaxDiscountPercent% اعتبار
|
||||
@p.MaxDiscountPercent% تخفیف
|
||||
</MudChip>
|
||||
}
|
||||
</div>
|
||||
<div class="pa-1 flex-grow-1 d-flex flex-column justify-space-between">
|
||||
<MudText Typo="Typo.subtitle1">@p.Title</MudText>
|
||||
@if (_isAuthenticated)
|
||||
{
|
||||
<div>
|
||||
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatPrice(p.Price)</MudText>
|
||||
<MudText Typo="Typo.overline" Class="mud-text-secondary" Style="font-size:0.6rem;line-height:1;">(+ ارزش افزوده)</MudText>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Small" Class="me-1"/>برای مشاهده قیمت وارد شوید
|
||||
</MudText>
|
||||
}
|
||||
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatPrice(p.Price)</MudText>
|
||||
</div>
|
||||
</MudCardContent>
|
||||
<MudCardActions Class="mt-auto d-flex justify-space-between pa-2">
|
||||
@@ -128,34 +115,19 @@
|
||||
</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
</div>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
|
||||
@* Lazy Load — بارگذاری بیشتر *@
|
||||
@if (_loadingMore)
|
||||
<!-- Pagination -->
|
||||
@if (_result.TotalPages > 1)
|
||||
{
|
||||
<MudStack AlignItems="AlignItems.Center" Class="py-4">
|
||||
<MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true"/>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">بارگذاری بیشتر...</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
else if (_hasMore)
|
||||
{
|
||||
<div class="d-flex justify-center py-4">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.ExpandMore"
|
||||
OnClick="LoadMore">
|
||||
نمایش محصولات بیشتر
|
||||
</MudButton>
|
||||
<div class="d-flex justify-center mt-4">
|
||||
<MudPagination Count="@_result.TotalPages" Selected="@_currentPage"
|
||||
SelectedChanged="OnPageChanged"
|
||||
Color="Color.Primary" Variant="Variant.Filled"
|
||||
BoundaryCount="1" MiddleCount="3" />
|
||||
</div>
|
||||
}
|
||||
else if (_products.Count > 0)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Align="Align.Center" Class="py-4 mud-text-secondary">
|
||||
همه @(_products.Count) محصول نمایش داده شد
|
||||
</MudText>
|
||||
}
|
||||
}
|
||||
</MudContainer>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Routing;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.JSInterop;
|
||||
using FrontOffice.Main.Utilities;
|
||||
|
||||
namespace FrontOffice.Main.Pages.DiscountStore;
|
||||
@@ -10,203 +8,80 @@ public partial class Products : ComponentBase, IDisposable
|
||||
{
|
||||
[Inject] private DiscountProductService ProductService { get; set; } = default!;
|
||||
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
|
||||
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||
[Inject] private IJSRuntime Js { get; set; } = default!;
|
||||
|
||||
private bool _isAuthenticated;
|
||||
private string _search = string.Empty;
|
||||
private long? _selectedCategoryId;
|
||||
private int _currentPage = 1;
|
||||
private bool _loading;
|
||||
private bool _loadingMore;
|
||||
private bool _hasMore = true;
|
||||
private int _totalCount;
|
||||
private const int PageSize = 12;
|
||||
private const string DefaultSortBy = "price desc";
|
||||
|
||||
private List<DiscountProductCard> _products = new();
|
||||
private DiscountProductListResult _result = new(new(), 0, 0, 1);
|
||||
private List<DiscountCategoryNode> _categories = new();
|
||||
private bool _ignoreNextLocationChange;
|
||||
private bool _pendingScrollRestore;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
|
||||
_loading = true;
|
||||
await DiscountCart.EnsureInitializedAsync();
|
||||
DiscountCart.OnChange += StateHasChanged;
|
||||
Navigation.LocationChanged += HandleLocationChanged;
|
||||
|
||||
_categories = await ProductService.GetCategoriesAsync();
|
||||
ApplyStateFromUri();
|
||||
_loading = true;
|
||||
await LoadPages(_currentPage);
|
||||
var categoriesTask = ProductService.GetCategoriesAsync();
|
||||
var productsTask = ProductService.GetProductsAsync(page: 1, pageSize: PageSize);
|
||||
await Task.WhenAll(categoriesTask, productsTask);
|
||||
_categories = categoriesTask.Result;
|
||||
_result = productsTask.Result;
|
||||
_loading = false;
|
||||
_pendingScrollRestore = true;
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (_pendingScrollRestore && !_loading && _products.Count > 0)
|
||||
{
|
||||
_pendingScrollRestore = false;
|
||||
var payload = await ShopListScrollRestore.TakeAsync(Js, ShopListScrollRestore.DiscountKey);
|
||||
if (payload is not null)
|
||||
await ShopListScrollRestore.RestoreAsync(Js, payload);
|
||||
}
|
||||
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
private void ApplyStateFromUri()
|
||||
{
|
||||
var state = ShopListQueryState.Parse(Navigation.ToAbsoluteUri(Navigation.Uri));
|
||||
_search = state.Query;
|
||||
_selectedCategoryId = state.CategoryId;
|
||||
_currentPage = state.Pages;
|
||||
}
|
||||
|
||||
private ShopListQueryState CaptureState() => new()
|
||||
{
|
||||
Query = _search,
|
||||
CategoryId = _selectedCategoryId,
|
||||
Pages = Math.Max(1, _currentPage)
|
||||
};
|
||||
|
||||
private void SyncUrl()
|
||||
{
|
||||
var target = CaptureState().ToRelativeUrl(RouteConstants.DiscountStore.Products);
|
||||
var currentPathAndQuery = Navigation.ToAbsoluteUri(Navigation.Uri).PathAndQuery;
|
||||
if (string.Equals(currentPathAndQuery, target, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
_ignoreNextLocationChange = true;
|
||||
Navigation.NavigateTo(target, replace: true);
|
||||
}
|
||||
|
||||
private async Task LoadPages(int pagesToLoad)
|
||||
{
|
||||
_products.Clear();
|
||||
_hasMore = true;
|
||||
pagesToLoad = Math.Max(1, pagesToLoad);
|
||||
var search = string.IsNullOrWhiteSpace(_search) ? null : _search;
|
||||
|
||||
for (var page = 1; page <= pagesToLoad; page++)
|
||||
{
|
||||
var result = await ProductService.GetProductsAsync(
|
||||
page: page,
|
||||
pageSize: PageSize,
|
||||
search: search,
|
||||
categoryId: _selectedCategoryId,
|
||||
sortBy: DefaultSortBy);
|
||||
|
||||
if (page == 1)
|
||||
{
|
||||
_products = result.Products;
|
||||
_totalCount = result.TotalCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
_products.AddRange(result.Products);
|
||||
}
|
||||
|
||||
_currentPage = page;
|
||||
_hasMore = result.CurrentPage < result.TotalPages;
|
||||
if (!_hasMore)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReloadFromFilters()
|
||||
private async Task LoadProductsAsync()
|
||||
{
|
||||
_loading = true;
|
||||
_currentPage = 1;
|
||||
StateHasChanged();
|
||||
await LoadPages(1);
|
||||
SyncUrl();
|
||||
_loading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task LoadMore()
|
||||
{
|
||||
if (_loadingMore || !_hasMore) return;
|
||||
|
||||
_loadingMore = true;
|
||||
StateHasChanged();
|
||||
|
||||
_currentPage++;
|
||||
var result = await ProductService.GetProductsAsync(
|
||||
_result = await ProductService.GetProductsAsync(
|
||||
page: _currentPage,
|
||||
pageSize: PageSize,
|
||||
search: string.IsNullOrWhiteSpace(_search) ? null : _search,
|
||||
categoryId: _selectedCategoryId,
|
||||
sortBy: DefaultSortBy);
|
||||
_products.AddRange(result.Products);
|
||||
_hasMore = result.CurrentPage < result.TotalPages;
|
||||
SyncUrl();
|
||||
|
||||
_loadingMore = false;
|
||||
categoryId: _selectedCategoryId);
|
||||
_loading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task SearchProducts() => await ReloadFromFilters();
|
||||
private async Task SearchProducts() => await LoadProductsAsync();
|
||||
|
||||
private async Task OnSearchKeyUp(KeyboardEventArgs e)
|
||||
{
|
||||
if (e.Key == "Enter")
|
||||
await ReloadFromFilters();
|
||||
{
|
||||
_currentPage = 1;
|
||||
await LoadProductsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnCategoryChanged(long? value)
|
||||
{
|
||||
_selectedCategoryId = value;
|
||||
await ReloadFromFilters();
|
||||
_currentPage = 1;
|
||||
await LoadProductsAsync();
|
||||
}
|
||||
|
||||
private async Task OnPageChanged(int page)
|
||||
{
|
||||
_currentPage = page;
|
||||
await LoadProductsAsync();
|
||||
}
|
||||
|
||||
private async Task AddToCart(DiscountProductCard p)
|
||||
{
|
||||
await GuestGate.RunAsync(() => DiscountCart.AddAsync(p.Id));
|
||||
await DiscountCart.AddAsync(p.Id);
|
||||
}
|
||||
|
||||
private async Task NavigateToProduct(long id)
|
||||
private void NavigateToProduct(long id)
|
||||
{
|
||||
await ShopListScrollRestore.SaveAsync(Js, ShopListScrollRestore.DiscountKey, id);
|
||||
Navigation.NavigateTo($"{RouteConstants.DiscountStore.ProductDetail}{id}");
|
||||
}
|
||||
|
||||
private void HandleLocationChanged(object? sender, LocationChangedEventArgs args)
|
||||
{
|
||||
if (_ignoreNextLocationChange)
|
||||
{
|
||||
_ignoreNextLocationChange = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var uri = Navigation.ToAbsoluteUri(args.Location);
|
||||
if (!uri.AbsolutePath.Equals(RouteConstants.DiscountStore.Products, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
var incoming = ShopListQueryState.Parse(uri);
|
||||
if (incoming.Matches(CaptureState()))
|
||||
return;
|
||||
|
||||
_ = InvokeAsync(async () =>
|
||||
{
|
||||
ApplyStateFromUri();
|
||||
_loading = true;
|
||||
await LoadPages(_currentPage);
|
||||
_loading = false;
|
||||
_pendingScrollRestore = true;
|
||||
StateHasChanged();
|
||||
});
|
||||
}
|
||||
|
||||
private static string FormatPrice(long price) => $"{price:N0} تومان";
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DiscountCart.OnChange -= StateHasChanged;
|
||||
Navigation.LocationChanged -= HandleLocationChanged;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ public partial class FAQ
|
||||
Icon = Icons.Material.Filled.Build,
|
||||
Questions = new List<FAQQuestion>
|
||||
{
|
||||
new() { Question = "سازمان فروش چگونه کار میکند؟", Answer = "سازمان فروش بصری نمایش سلسله مراتبی تیم شما را نشان میدهد و امکان ردیابی روابط ارجاعی را فراهم میکند." },
|
||||
new() { Question = "شجرهنامه چگونه کار میکند؟", Answer = "شجرهنامه بصری نمایش سلسله مراتبی تیم شما را نشان میدهد و امکان ردیابی روابط ارجاعی را فراهم میکند." },
|
||||
new() { Question = "گزارشگیری به چه صورت است؟", Answer = "سیستم گزارشهای جامع مالی، عملکردی و آماری ارائه میدهد که قابل فیلتر و دانلود به فرمت Excel است." },
|
||||
new() { Question = "آیا از موبایل قابل استفاده است؟", Answer = "بله، اپلیکیشن کاملاً responsive است و تجربه کاربری عالی در موبایل و تبلت ارائه میدهد." }
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<MudAvatar Size="Size.Large" Class="mx-auto mb-3 gateway-avatar-sm-discount">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Loyalty" Size="Size.Large" Class="text-brand-discount" />
|
||||
</MudAvatar>
|
||||
<MudText Typo="Typo.h6" Class="fw-semibold">سبد خرید اعتباری</MudText>
|
||||
<MudText Typo="Typo.h6" Class="fw-semibold">سبد خرید تخفیفی</MudText>
|
||||
@if (_discountCartCount > 0)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small" Class="mt-2">
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
<MudAvatar Size="Size.Large" Class="mx-auto mb-3 gateway-avatar-sm-discount">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Loyalty" Size="Size.Large" Class="text-brand-discount" />
|
||||
</MudAvatar>
|
||||
<MudText Typo="Typo.h6" Class="fw-semibold">سفارشات فروشگاه اعتباری</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary mt-1">سفارشات خرید اعتباری</MudText>
|
||||
<MudText Typo="Typo.h6" Class="fw-semibold">سفارشات فروشگاه تخفیفی</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary mt-1">سفارشات خرید تخفیفی</MudText>
|
||||
</MudPaper>
|
||||
</MudLink>
|
||||
</MudItem>
|
||||
|
||||
@@ -38,9 +38,9 @@
|
||||
<MudAvatar Size="Size.Large" Class="mx-auto mb-3 gateway-avatar-discount">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Loyalty" Size="Size.Large" Class="text-brand-discount" />
|
||||
</MudAvatar>
|
||||
<MudText Typo="Typo.h5" Class="fw-bold">فروشگاه اعتباری</MudText>
|
||||
<MudText Typo="Typo.h5" Class="fw-bold">فروشگاه تخفیفی</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-2">
|
||||
خرید با استفاده از موجودی کیف اعتباری + درگاه پرداخت
|
||||
خرید با استفاده از موجودی کیف تخفیفی + درگاه پرداخت
|
||||
</MudText>
|
||||
@if (_discountCartCount > 0)
|
||||
{
|
||||
|
||||
@@ -11,15 +11,15 @@
|
||||
<MudStack AlignItems="AlignItems.Center" Spacing="4" Class="text-center">
|
||||
<MudChip T="string" Color="Color.Default" Variant="Variant.Filled"
|
||||
Class="pulse-chip hero-chip-glass" Size="Size.Small">
|
||||
باشگاه مشتریان KBS کارا بازار سلامت
|
||||
🌿 پلتفرم سلامتمحور فروش و تیمسازی
|
||||
</MudChip>
|
||||
|
||||
<MudText Typo="Typo.h1" Class="hero-title" Style="max-width:680px;">
|
||||
@(_pageData?.HeroTitle ?? "رشد تیم، فروش واقعی، پاداش شفاف")
|
||||
رشد تیم، فروش واقعی، پاداش شفاف
|
||||
</MudText>
|
||||
|
||||
<MudText Typo="Typo.body1" Class="hero-subtitle" Style="max-width:520px;">
|
||||
@(_pageData?.HeroSubtitle ?? "با دعوت از دوستان، از خریدهای واقعی محصولات سلامت پاداش بگیرید. ثبتنام سریع، داشبورد لحظهای.")
|
||||
با دعوت از دوستان، از خریدهای واقعی محصولات سلامت پاداش بگیرید. ثبتنام سریع، داشبورد لحظهای.
|
||||
</MudText>
|
||||
|
||||
<MudStack Row="true" Spacing="2" Class="mt-2 flex-wrap" Justify="Justify.Center">
|
||||
@@ -27,13 +27,13 @@
|
||||
Class="rounded-pill hero-cta-primary"
|
||||
Size="Size.Large"
|
||||
OnClick="NavigateToRegistrationWizard">
|
||||
@(_settings?.HeroButtonPrimaryText ?? "شروع رایگان")
|
||||
شروع رایگان
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Class="rounded-pill hero-cta-outline"
|
||||
Size="Size.Large"
|
||||
OnClick="@(() => Navigation.NavigateTo("/blog"))">
|
||||
@(_settings?.HeroButtonSecondaryText ?? "آخرین اخبار")
|
||||
آخرین اخبار
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
<section class="section-landing">
|
||||
<MudContainer MaxWidth="MaxWidth.Large">
|
||||
<div class="text-center mb-8 fade-in-up">
|
||||
<MudText Typo="Typo.h3">@(_settings?.StepsTitle ?? "سه گام تا شروع")</MudText>
|
||||
<MudText Typo="Typo.h3">سه گام تا شروع</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
|
||||
بدون پیچیدگی، سریع و آسان
|
||||
</MudText>
|
||||
@@ -128,7 +128,7 @@
|
||||
<section class="section-landing" style="background:var(--mud-palette-background-gray);">
|
||||
<MudContainer MaxWidth="MaxWidth.Large">
|
||||
<div class="text-center mb-8 fade-in-up">
|
||||
<MudText Typo="Typo.h3">@(_settings?.FeaturesTitle ?? "چرا کارا بازار سلامت؟")</MudText>
|
||||
<MudText Typo="Typo.h3">چرا کارا بازار سلامت؟</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2" Style="max-width:540px;margin:0 auto;">
|
||||
ابزارهایی ساده و قدرتمند برای رشد کسبوکار شما
|
||||
</MudText>
|
||||
@@ -172,179 +172,6 @@
|
||||
</MudContainer>
|
||||
</section>
|
||||
|
||||
@* ═══════════════════════════════════════════════
|
||||
1c. TOP-SELLING REGULAR PRODUCTS
|
||||
═══════════════════════════════════════════════ *@
|
||||
@if (_loadingTopProducts)
|
||||
{
|
||||
<section class="section-landing" style="background:var(--mud-palette-background-gray);">
|
||||
<MudContainer MaxWidth="MaxWidth.Large">
|
||||
<MudStack AlignItems="AlignItems.Center" Class="py-4">
|
||||
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Small" />
|
||||
</MudStack>
|
||||
</MudContainer>
|
||||
</section>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (_topRegularProducts.Any())
|
||||
{
|
||||
<section class="section-landing" style="background:var(--mud-palette-background-gray);">
|
||||
<MudContainer MaxWidth="MaxWidth.Large">
|
||||
<div class="text-center mb-6 fade-in-up">
|
||||
<MudText Typo="Typo.h3">محصولات پرفروش فروشگاه</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
|
||||
محبوبترین محصولات کارا بازار سلامت
|
||||
</MudText>
|
||||
</div>
|
||||
|
||||
<MudGrid Spacing="2" Justify="Justify.FlexStart">
|
||||
@foreach (var p in _topRegularProducts)
|
||||
{
|
||||
<MudItem xs="6" sm="6" md="4">
|
||||
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden landing-product-card"
|
||||
Style="cursor:pointer;"
|
||||
@onclick="() => NavigateToRegularProduct(p.Id)">
|
||||
<MudCardContent Class="d-flex flex-column pa-1 h-100">
|
||||
<div style="aspect-ratio:1/1;width:100%;background-image:url('@p.ImageUrl');background-size:cover;background-position:center;border-radius:0.5rem;position:relative;">
|
||||
@if (p.RemainingCount <= 0)
|
||||
{
|
||||
<div style="position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;border-radius:0.5rem;">
|
||||
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">ناموجود</MudChip>
|
||||
</div>
|
||||
}
|
||||
else if (p.RemainingCount <= 5)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Warning" Variant="Variant.Filled" Size="Size.Small"
|
||||
Style="position:absolute;top:8px;right:8px;">
|
||||
فقط @p.RemainingCount عدد
|
||||
</MudChip>
|
||||
}
|
||||
</div>
|
||||
<div class="pa-1 flex-grow-1 d-flex flex-column justify-space-between">
|
||||
<MudText Typo="Typo.subtitle1" Style="display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">@p.Title</MudText>
|
||||
@if (_isAuthenticated)
|
||||
{
|
||||
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatPrice(p.Price)</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Small" Class="me-1"/>برای مشاهده قیمت وارد شوید
|
||||
</MudText>
|
||||
}
|
||||
</div>
|
||||
</MudCardContent>
|
||||
<MudCardActions Class="mt-auto pa-2" @onclick:stopPropagation="true">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
|
||||
StartIcon="@Icons.Material.Filled.AddShoppingCart"
|
||||
Disabled="@(p.RemainingCount <= 0)"
|
||||
OnClick="@(() => AddRegularToCart(p))">
|
||||
@(p.RemainingCount <= 0 ? "ناموجود" : "افزودن به سبد")
|
||||
</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
|
||||
<div class="text-center mt-6">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Class="rounded-pill"
|
||||
Href="@RouteConstants.Store.Products"
|
||||
EndIcon="@Icons.Material.Filled.ArrowBack">
|
||||
مشاهده همه محصولات
|
||||
</MudButton>
|
||||
</div>
|
||||
</MudContainer>
|
||||
</section>
|
||||
}
|
||||
|
||||
@* ═══════════════════════════════════════════════
|
||||
1d. TOP-SELLING DISCOUNT STORE PRODUCTS
|
||||
═══════════════════════════════════════════════ *@
|
||||
@if (_topDiscountProducts.Any())
|
||||
{
|
||||
<section class="section-landing">
|
||||
<MudContainer MaxWidth="MaxWidth.Large">
|
||||
<div class="text-center mb-6 fade-in-up">
|
||||
<MudText Typo="Typo.h3">محصولات پرفروش فروشگاه اعتباری</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
|
||||
محصولات ویژه اعضای باشگاه با پرداخت اعتباری
|
||||
</MudText>
|
||||
</div>
|
||||
|
||||
<MudGrid Spacing="2" Justify="Justify.FlexStart">
|
||||
@foreach (var p in _topDiscountProducts)
|
||||
{
|
||||
<MudItem xs="6" sm="6" md="4">
|
||||
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden landing-product-card"
|
||||
Style="cursor:pointer;"
|
||||
@onclick="() => NavigateToDiscountProduct(p.Id)">
|
||||
<MudCardContent Class="d-flex flex-column pa-1 h-100">
|
||||
<div style="aspect-ratio:1/1;width:100%;background-image:url('@(string.IsNullOrWhiteSpace(p.ThumbnailUrl) ? p.ImageUrl : p.ThumbnailUrl)');background-size:cover;background-position:center;border-radius:0.5rem;position:relative;">
|
||||
@if (p.RemainingCount <= 0)
|
||||
{
|
||||
<div style="position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;border-radius:0.5rem;">
|
||||
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">ناموجود</MudChip>
|
||||
</div>
|
||||
}
|
||||
else if (p.RemainingCount <= 5)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Warning" Variant="Variant.Filled" Size="Size.Small"
|
||||
Style="position:absolute;top:8px;right:8px;">
|
||||
فقط @p.RemainingCount عدد
|
||||
</MudChip>
|
||||
}
|
||||
@if (p.MaxDiscountPercent > 0)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled" Size="Size.Small"
|
||||
Style="position:absolute;top:8px;left:8px;">
|
||||
@p.MaxDiscountPercent% اعتبار
|
||||
</MudChip>
|
||||
}
|
||||
</div>
|
||||
<div class="pa-1 flex-grow-1 d-flex flex-column justify-space-between">
|
||||
<MudText Typo="Typo.subtitle1" Style="display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">@p.Title</MudText>
|
||||
@if (_isAuthenticated)
|
||||
{
|
||||
<div>
|
||||
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatDiscountPrice(p.Price)</MudText>
|
||||
<MudText Typo="Typo.overline" Class="mud-text-secondary" Style="font-size:0.6rem;line-height:1;">(+ ارزش افزوده)</MudText>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Small" Class="me-1"/>برای مشاهده قیمت وارد شوید
|
||||
</MudText>
|
||||
}
|
||||
</div>
|
||||
</MudCardContent>
|
||||
<MudCardActions Class="mt-auto pa-2" @onclick:stopPropagation="true">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Secondary" FullWidth="true"
|
||||
StartIcon="@Icons.Material.Filled.AddShoppingCart"
|
||||
Disabled="@(p.RemainingCount <= 0)"
|
||||
OnClick="@(() => AddDiscountToCart(p))">
|
||||
@(p.RemainingCount <= 0 ? "ناموجود" : "افزودن به سبد")
|
||||
</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
|
||||
<div class="text-center mt-6">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" Class="rounded-pill"
|
||||
Href="@RouteConstants.DiscountStore.Products"
|
||||
EndIcon="@Icons.Material.Filled.ArrowBack">
|
||||
مشاهده همه محصولات اعتباری
|
||||
</MudButton>
|
||||
</div>
|
||||
</MudContainer>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
@* ═══════════════════════════════════════════════
|
||||
5. LATEST BLOG POSTS
|
||||
═══════════════════════════════════════════════ *@
|
||||
@@ -429,12 +256,10 @@ else
|
||||
@* ═══════════════════════════════════════════════
|
||||
6. TESTIMONIALS
|
||||
═══════════════════════════════════════════════ *@
|
||||
@if (_testimonials.Count > 0)
|
||||
{
|
||||
<section class="section-landing">
|
||||
<MudContainer MaxWidth="MaxWidth.Large">
|
||||
<div class="text-center mb-8 fade-in-up">
|
||||
<MudText Typo="Typo.h3">@(_settings?.TestimonialsTitle ?? "اعتماد مشتریان")</MudText>
|
||||
<MudText Typo="Typo.h3">اعتماد مشتریان</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
|
||||
بخشی از تجربه استفاده از «کارا بازار سلامت»
|
||||
</MudText>
|
||||
@@ -467,7 +292,6 @@ else
|
||||
</MudGrid>
|
||||
</MudContainer>
|
||||
</section>
|
||||
}
|
||||
|
||||
@* ═══════════════════════════════════════════════
|
||||
7. FAQ
|
||||
@@ -475,7 +299,7 @@ else
|
||||
<section class="section-landing" style="background:var(--mud-palette-background-gray);">
|
||||
<MudContainer MaxWidth="MaxWidth.Medium">
|
||||
<div class="text-center mb-8 fade-in-up">
|
||||
<MudText Typo="Typo.h3">@(_settings?.FaqTitle ?? "سوالات متداول")</MudText>
|
||||
<MudText Typo="Typo.h3">سوالات متداول</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
|
||||
پاسخ به سوالات رایج شما
|
||||
</MudText>
|
||||
@@ -502,16 +326,16 @@ else
|
||||
<div class="cta-banner pa-8 pa-md-12 fade-in-up">
|
||||
<MudStack AlignItems="AlignItems.Center" Spacing="3">
|
||||
<MudText Typo="Typo.h3" Align="Align.Center" Class="dash-hero-name">
|
||||
@(_settings?.CtaTitle ?? "آماده شروع هستید؟")
|
||||
آماده شروع هستید؟
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body1" Align="Align.Center" Class="hero-subtitle">
|
||||
@(_settings?.CtaDescription ?? "همین الان ثبتنام کنید و از مزایای کارا بازار سلامت بهرهمند شوید.")
|
||||
همین الان ثبتنام کنید و از مزایای کارا بازار سلامت بهرهمند شوید.
|
||||
</MudText>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Size="Size.Large"
|
||||
Class="rounded-pill mt-2 hero-cta-primary"
|
||||
OnClick="NavigateToRegistrationWizard">
|
||||
@(_settings?.CtaButtonText ?? "شروع رایگان")
|
||||
شروع رایگان
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</div>
|
||||
|
||||
@@ -9,88 +9,21 @@ public partial class Index : IDisposable
|
||||
{
|
||||
[Inject] private BlogPostService BlogPostService { get; set; } = default!;
|
||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||
[Inject] private SitePageSettingsService PageSettingsService { get; set; } = default!;
|
||||
[Inject] private ProductService ProductService { get; set; } = default!;
|
||||
[Inject] private DiscountProductService DiscountProductService { get; set; } = default!;
|
||||
[Inject] private CartService Cart { get; set; } = default!;
|
||||
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
|
||||
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
||||
[Inject] private VATService VAT { get; set; } = default!;
|
||||
|
||||
private bool _isAuthenticated;
|
||||
|
||||
// ── CMS page data ──
|
||||
private PageSettingsDto? _pageData;
|
||||
private LandingSettings? _settings;
|
||||
|
||||
// ── Top-selling product sections ──
|
||||
private const int LandingTopProductCount = 6;
|
||||
|
||||
private List<Product> _topRegularProducts = new();
|
||||
private List<DiscountProductCard> _topDiscountProducts = new();
|
||||
private bool _loadingTopProducts = true;
|
||||
|
||||
// ── Latest blog posts (loaded from CMS) ──
|
||||
private List<BlogPostCardDto> _latestPosts = new();
|
||||
|
||||
// ── Data lists (populated from DB or fallback) ──
|
||||
private List<(string Icon, string Text)> _trustBadges = new();
|
||||
private List<(string Title, string Desc)> _steps = new();
|
||||
private List<(string Icon, string Title, string Desc, int Delay)> _features = new();
|
||||
private List<StatItem> _stats = new();
|
||||
private List<TestimonialItem> _testimonials = new();
|
||||
private List<QA> _faqs = new();
|
||||
|
||||
// Track whether animations need re-initialization after data load
|
||||
private bool _dataLoaded;
|
||||
private bool _animationsInitialized;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
MainService.OnChangeHandler += OnStateChanged;
|
||||
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
|
||||
|
||||
// Load landing page settings from CMS
|
||||
try
|
||||
{
|
||||
_pageData = await PageSettingsService.GetPageAsync("landing");
|
||||
if (_pageData != null)
|
||||
{
|
||||
_settings = _pageData.GetSettings<LandingSettings>();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback: settings remain null → defaults below
|
||||
}
|
||||
|
||||
PopulateFromSettings();
|
||||
_dataLoaded = true;
|
||||
|
||||
// Load top-selling in-stock products and blog posts in parallel
|
||||
var topRegTask = ProductService.GetTopSellingAsync(LandingTopProductCount, inStock: true);
|
||||
var topDiscTask = DiscountProductService.GetTopSellingAsync(LandingTopProductCount, inStock: true);
|
||||
var featuredPostsTask = BlogPostService.GetFeaturedPostsAsync(2);
|
||||
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(topRegTask, topDiscTask, featuredPostsTask);
|
||||
_topRegularProducts = topRegTask.Result.Products;
|
||||
_topDiscountProducts = topDiscTask.Result.Products;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback: sections remain empty
|
||||
}
|
||||
_loadingTopProducts = false;
|
||||
|
||||
// Load latest published blog posts
|
||||
// اول پینشدهها، اگر کافی نبود آخرین پستها اضافه شود
|
||||
try
|
||||
{
|
||||
_latestPosts = featuredPostsTask.IsCompletedSuccessfully
|
||||
? featuredPostsTask.Result
|
||||
: await BlogPostService.GetFeaturedPostsAsync(2);
|
||||
|
||||
_latestPosts = await BlogPostService.GetFeaturedPostsAsync(2);
|
||||
|
||||
// اگر فقط ۱ پینشده داریم، ۱ پست آخر هم اضافه کن
|
||||
if (_latestPosts.Count < 2)
|
||||
{
|
||||
var result = await BlogPostService.GetPublishedPostsAsync(page: 1, pageSize: 2);
|
||||
@@ -111,163 +44,10 @@ public partial class Index : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateFromSettings()
|
||||
{
|
||||
// ── Trust badges ──
|
||||
if (_settings?.TrustBadges?.Any() == true)
|
||||
{
|
||||
_trustBadges = _settings.TrustBadges
|
||||
.Select(b => (ResolveIcon(b.IconName), b.Text ?? ""))
|
||||
.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
_trustBadges = new()
|
||||
{
|
||||
(Icons.Material.Outlined.Timer, "ثبتنام زیر ۲ دقیقه"),
|
||||
(Icons.Material.Outlined.SupportAgent, "پشتیبانی ۷×۲۴"),
|
||||
(Icons.Material.Outlined.Lock, "پرداخت ایمن"),
|
||||
(Icons.Material.Outlined.Verified, "تضمین کیفیت"),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Steps ──
|
||||
if (_settings?.Steps?.Any() == true)
|
||||
{
|
||||
_steps = _settings.Steps
|
||||
.Select(s => (s.Title ?? "", s.Description ?? ""))
|
||||
.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
_steps = new()
|
||||
{
|
||||
("ثبتنام و احراز هویت", "یک حساب بسازید، شماره موبایل را تأیید و اطلاعات هویتی را تکمیل کنید."),
|
||||
("دعوت دوستان", "لینک دعوت اختصاصی خود را با دوستان و آشنایان به اشتراک بگذارید."),
|
||||
("دریافت پاداش", "از خریدهای واقعی اعضای تیمتان پاداش شفاف و لحظهای دریافت کنید."),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Features ──
|
||||
if (_settings?.Features?.Any() == true)
|
||||
{
|
||||
_features = _settings.Features
|
||||
.Select((f, i) => (ResolveIcon(f.IconName), f.Title ?? "", f.Description ?? "", i * 100))
|
||||
.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
_features = new()
|
||||
{
|
||||
(Icons.Material.Outlined.VerifiedUser, "ثبتنام سریع و ساده", "در چند گام کوتاه حساب بسازید و شروع کنید.", 0),
|
||||
(Icons.Material.Outlined.StarBorder, "پاداشهای شفاف", "قوانین روشن، دسترسی آسان به سوابق و گزارشها.", 100),
|
||||
(Icons.Material.Outlined.Devices, "طراحی واکنشگرا", "تجربهای روان در موبایل و دسکتاپ.", 200),
|
||||
(Icons.Material.Outlined.Groups, "تیمسازی هوشمند", "ساختار درختی شبکه و مدیریت تیمهای فروش.", 300),
|
||||
(Icons.Material.Outlined.Insights, "گزارشهای لحظهای", "داشبورد پویا برای مشاهده عملکرد و کمیسیون.", 400),
|
||||
(Icons.Material.Outlined.Lock, "امنیت بالا", "رمزنگاری اطلاعات و احراز هویت چندمرحلهای.", 500),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Stats ──
|
||||
if (_settings?.Stats?.Any() == true)
|
||||
{
|
||||
_stats = _settings.Stats
|
||||
.Select((s, i) => new StatItem(
|
||||
$"stat-{i}",
|
||||
$"{s.Value}{s.Suffix}",
|
||||
s.Value,
|
||||
s.Suffix ?? "",
|
||||
ResolveColor(s.Color),
|
||||
s.Label ?? ""))
|
||||
.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
_stats = new()
|
||||
{
|
||||
new("stat-growth", "+۵۰٪", 50, "%+", Color.Success, "رشد میانگین تیم"),
|
||||
new("stat-uptime", "۹۹.۹٪", 99.9, "%", Color.Primary, "آپتایم سرویس"),
|
||||
new("stat-deploy", "۳ روز", 3, " روز", Color.Warning, "میانگین زمان استقرار"),
|
||||
new("stat-coverage", "+۲۰ کشور", 20, "+", Color.Secondary, "پوشش ارسال کد"),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Testimonials ──
|
||||
// اگر تنظیمات از CMS لود شده، لیست خالی یعنی ادمین عمداً حذف کرده — بدون fallback hardcode
|
||||
if (_settings is not null)
|
||||
{
|
||||
_testimonials = (_settings.Testimonials ?? [])
|
||||
.Select((t, i) => new TestimonialItem(t.Quote ?? "", t.Name ?? "", t.Role ?? "", i * 150))
|
||||
.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
_testimonials = new()
|
||||
{
|
||||
new("با کارا بازار سلامت، محاسبه کارمزدها و پایش تیمها بدون اکسل و دردسر انجام میشود.", "شرکت سینا نت", "مدیر عملیات", 0),
|
||||
new("سازمان فروش بصری و گزارشهای دقیق باعث شد رشد تیم را لحظهای ببینیم.", "هولدینگ آریانا", "مدیر فروش", 150),
|
||||
new("سادگی ثبتنام و شفافیت پاداشها مهمترین مزیت این پلتفرم است.", "گروه بهداشتی نوین", "مدیر توسعه", 300),
|
||||
};
|
||||
}
|
||||
|
||||
// ── FAQs ──
|
||||
if (_settings?.Faqs?.Any() == true)
|
||||
{
|
||||
_faqs = _settings.Faqs
|
||||
.Select(f => new QA(f.Question ?? "", f.Answer ?? ""))
|
||||
.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
_faqs = new()
|
||||
{
|
||||
new("دامنه اختصاصی دارم؛ قابل اتصال است؟", "بله، پشت دامنه و گواهی SSL خودتان مستقر میشود."),
|
||||
new("با دیتابیس خودم کار میکند؟", "کاملاً. SQL Server، PostgreSQL و MySQL پشتیبانی میشود."),
|
||||
new("چه درگاههایی پشتیبانی میشود؟", "Stripe و PayPal یا درگاه اختصاصی از طریق وبهوکها."),
|
||||
new("میتوانم دادهها را خروجی بگیرم؟", "هر زمان از داشبورد ادمین خروجی CSV/Excel بگیرید."),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve icon name from SettingsJson to MudBlazor icon string.
|
||||
/// Falls back to a generic icon.
|
||||
/// </summary>
|
||||
private static string ResolveIcon(string? iconName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(iconName)) return Icons.Material.Outlined.Info;
|
||||
|
||||
// Try to get from MudBlazor Icons via reflection
|
||||
var field = typeof(Icons.Material.Outlined).GetField(iconName,
|
||||
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
if (field != null) return (string)(field.GetValue(null) ?? Icons.Material.Outlined.Info);
|
||||
|
||||
// Also try Filled
|
||||
field = typeof(Icons.Material.Filled).GetField(iconName,
|
||||
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
if (field != null) return (string)(field.GetValue(null) ?? Icons.Material.Outlined.Info);
|
||||
|
||||
return Icons.Material.Outlined.Info;
|
||||
}
|
||||
|
||||
private static Color ResolveColor(string? colorName) => colorName?.ToLowerInvariant() switch
|
||||
{
|
||||
"primary" => Color.Primary,
|
||||
"secondary" => Color.Secondary,
|
||||
"success" => Color.Success,
|
||||
"warning" => Color.Warning,
|
||||
"error" => Color.Error,
|
||||
"info" => Color.Info,
|
||||
_ => Color.Default
|
||||
};
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
// Init/re-init scroll animations after data is loaded and rendered
|
||||
if ((firstRender || (_dataLoaded && !_animationsInitialized)) && _steps.Any())
|
||||
if (firstRender)
|
||||
{
|
||||
_animationsInitialized = true;
|
||||
|
||||
// Init scroll-triggered fade-in animations
|
||||
await JS.InvokeVoidAsync("initScrollAnimations");
|
||||
|
||||
@@ -303,60 +83,70 @@ public partial class Index : IDisposable
|
||||
Navigation.NavigateTo($"/blog/{slug}");
|
||||
}
|
||||
|
||||
private void NavigateToRegularProduct(long id)
|
||||
=> Navigation.NavigateTo(RouteConstants.Store.ProductDetail + id);
|
||||
|
||||
private void NavigateToDiscountProduct(long id)
|
||||
=> Navigation.NavigateTo(RouteConstants.DiscountStore.ProductDetail + id);
|
||||
|
||||
private async Task AddRegularToCart(Product p)
|
||||
=> await GuestGate.RunAsync(() => Cart.Add(p, 1));
|
||||
|
||||
private async Task AddDiscountToCart(DiscountProductCard p)
|
||||
=> await GuestGate.RunAsync(() => DiscountCart.AddAsync(p.Id));
|
||||
|
||||
private string FormatPrice(long price) => $"{VAT.AddVAT(price):N0} تومان";
|
||||
private string FormatDiscountPrice(long price) => $"{price:N0} تومان";
|
||||
|
||||
private async void OnStateChanged()
|
||||
{
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
// ── Trust badges ──
|
||||
private readonly List<(string Icon, string Text)> _trustBadges = new()
|
||||
{
|
||||
(Icons.Material.Outlined.Timer, "ثبتنام زیر ۲ دقیقه"),
|
||||
(Icons.Material.Outlined.SupportAgent, "پشتیبانی ۷×۲۴"),
|
||||
(Icons.Material.Outlined.Lock, "پرداخت ایمن"),
|
||||
(Icons.Material.Outlined.Verified, "تضمین کیفیت"),
|
||||
};
|
||||
|
||||
// ── How it works steps ──
|
||||
private readonly List<(string Title, string Desc)> _steps = new()
|
||||
{
|
||||
("ثبتنام و احراز هویت", "یک حساب بسازید، شماره موبایل را تأیید و اطلاعات هویتی را تکمیل کنید."),
|
||||
("دعوت دوستان", "لینک دعوت اختصاصی خود را با دوستان و آشنایان به اشتراک بگذارید."),
|
||||
("دریافت پاداش", "از خریدهای واقعی اعضای تیمتان پاداش شفاف و لحظهای دریافت کنید."),
|
||||
};
|
||||
|
||||
// ── Feature cards data (6 cards) ──
|
||||
private readonly List<(string Icon, string Title, string Desc, int Delay)> _features = new()
|
||||
{
|
||||
(Icons.Material.Outlined.VerifiedUser, "ثبتنام سریع و ساده", "در چند گام کوتاه حساب بسازید و شروع کنید.", 0),
|
||||
(Icons.Material.Outlined.StarBorder, "پاداشهای شفاف", "قوانین روشن، دسترسی آسان به سوابق و گزارشها.", 100),
|
||||
(Icons.Material.Outlined.Devices, "طراحی واکنشگرا", "تجربهای روان در موبایل و دسکتاپ.", 200),
|
||||
(Icons.Material.Outlined.Groups, "تیمسازی هوشمند", "ساختار درختی شبکه و مدیریت تیمهای فروش.", 300),
|
||||
(Icons.Material.Outlined.Insights, "گزارشهای لحظهای", "داشبورد پویا برای مشاهده عملکرد و کمیسیون.", 400),
|
||||
(Icons.Material.Outlined.Lock, "امنیت بالا", "رمزنگاری اطلاعات و احراز هویت چندمرحلهای.", 500),
|
||||
};
|
||||
|
||||
// ── Stats data (animated counters) ──
|
||||
private readonly List<StatItem> _stats = new()
|
||||
{
|
||||
new("stat-growth", "+۵۰٪", 50, "%+", Color.Success, "رشد میانگین تیم"),
|
||||
new("stat-uptime", "۹۹.۹٪", 99.9, "%", Color.Primary, "آپتایم سرویس"),
|
||||
new("stat-deploy", "۳ روز", 3, " روز", Color.Warning, "میانگین زمان استقرار"),
|
||||
new("stat-coverage", "+۲۰ کشور", 20, "+", Color.Secondary, "پوشش ارسال کد"),
|
||||
};
|
||||
|
||||
// ── Testimonials ──
|
||||
private readonly List<TestimonialItem> _testimonials = new()
|
||||
{
|
||||
new("با کارا بازار سلامت، محاسبه کارمزدها و پایش تیمها بدون اکسل و دردسر انجام میشود.", "شرکت سینا نت", "مدیر عملیات", 0),
|
||||
new("شجرهنامه بصری و گزارشهای دقیق باعث شد رشد تیم را لحظهای ببینیم.", "هولدینگ آریانا", "مدیر فروش", 150),
|
||||
new("سادگی ثبتنام و شفافیت پاداشها مهمترین مزیت این پلتفرم است.", "گروه بهداشتی نوین", "مدیر توسعه", 300),
|
||||
};
|
||||
|
||||
// ── FAQ data ──
|
||||
private readonly List<QA> _faqs = new()
|
||||
{
|
||||
new("دامنه اختصاصی دارم؛ قابل اتصال است؟", "بله، پشت دامنه و گواهی SSL خودتان مستقر میشود."),
|
||||
new("با دیتابیس خودم کار میکند؟", "کاملاً. SQL Server، PostgreSQL و MySQL پشتیبانی میشود."),
|
||||
new("چه درگاههایی پشتیبانی میشود؟", "Stripe و PayPal یا درگاه اختصاصی از طریق وبهوکها."),
|
||||
new("میتوانم دادهها را خروجی بگیرم؟", "هر زمان از داشبورد ادمین خروجی CSV/Excel بگیرید."),
|
||||
};
|
||||
|
||||
// ── Records ──
|
||||
private record QA(string Q, string A);
|
||||
private record StatItem(string ElementId, string Display, double Target, string Suffix, Color Color, string Label);
|
||||
private record TestimonialItem(string Quote, string Name, string Role, int Delay);
|
||||
|
||||
// ── SettingsJson DTO ──
|
||||
private class LandingSettings
|
||||
{
|
||||
public string? HeroButtonPrimaryText { get; set; }
|
||||
public string? HeroButtonSecondaryText { get; set; }
|
||||
public List<TrustBadgeItem>? TrustBadges { get; set; }
|
||||
public string? StepsTitle { get; set; }
|
||||
public List<StepItem>? Steps { get; set; }
|
||||
public string? FeaturesTitle { get; set; }
|
||||
public List<FeatureItem>? Features { get; set; }
|
||||
public string? StatsTitle { get; set; }
|
||||
public List<StatSettingItem>? Stats { get; set; }
|
||||
public string? TestimonialsTitle { get; set; }
|
||||
public List<TestimonialSettingItem>? Testimonials { get; set; }
|
||||
public string? FaqTitle { get; set; }
|
||||
public List<FaqItem>? Faqs { get; set; }
|
||||
public string? CtaTitle { get; set; }
|
||||
public string? CtaDescription { get; set; }
|
||||
public string? CtaButtonText { get; set; }
|
||||
public bool? FeaturedBlogEnabled { get; set; }
|
||||
}
|
||||
|
||||
private class TrustBadgeItem { public string? IconName { get; set; } public string? Text { get; set; } }
|
||||
private class StepItem { public string? Title { get; set; } public string? Description { get; set; } }
|
||||
private class FeatureItem { public string? IconName { get; set; } public string? Title { get; set; } public string? Description { get; set; } }
|
||||
private class StatSettingItem { public string? Label { get; set; } public double Value { get; set; } public string? Suffix { get; set; } public string? Color { get; set; } }
|
||||
private class TestimonialSettingItem { public string? Quote { get; set; } public string? Name { get; set; } public string? Role { get; set; } }
|
||||
private class FaqItem { public string? Question { get; set; } public string? Answer { get; set; } }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
MainService.OnChangeHandler -= OnStateChanged;
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
@attribute [Route(RouteConstants.Licenses.Index)]
|
||||
@inject SitePageSettingsService PageSettingsService
|
||||
|
||||
<PageTitle>مجوزها و گواهینامهها | کارا بازار سلامت</PageTitle>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="py-16">
|
||||
<LoadingState />
|
||||
</MudContainer>
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Hero Section -->
|
||||
<section class="licenses-hero-section py-16">
|
||||
<MudContainer MaxWidth="MaxWidth.Large">
|
||||
<MudStack Spacing="3" AlignItems="AlignItems.Center" Class="text-center">
|
||||
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled">مجوزها و گواهینامهها</MudChip>
|
||||
<MudText Typo="Typo.h2" Class="mb-3">
|
||||
@(_pageData?.HeroTitle ?? "مجوزها و گواهینامههای رسمی")
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mud-text-secondary" Style="max-width: 700px;">
|
||||
@(_pageData?.HeroSubtitle ?? "ما با دریافت مجوزهای رسمی و تأییدیههای معتبر، اطمینان شما را در استفاده از خدمات خود جلب کردهایم.")
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudContainer>
|
||||
</section>
|
||||
|
||||
<MudStack Spacing="8" Class="pb-12">
|
||||
|
||||
<!-- Licenses Gallery -->
|
||||
<section class="py-12">
|
||||
<MudContainer MaxWidth="MaxWidth.Large">
|
||||
<MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">مجوزهای ما</MudText>
|
||||
@if (_licenseImages.Any())
|
||||
{
|
||||
<MudGrid Spacing="4" Justify="Justify.Center">
|
||||
@foreach (var license in _licenseImages)
|
||||
{
|
||||
<MudItem xs="6" sm="6" md="4">
|
||||
<MudCard Elevation="2" Class="h-100">
|
||||
<MudCardMedia Image="@license.ImagePath" Height="250" />
|
||||
<MudCardContent>
|
||||
@if (!string.IsNullOrWhiteSpace(license.Title))
|
||||
{
|
||||
<MudText Typo="Typo.h6" Align="Align.Center" Class="mb-1">@license.Title</MudText>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(license.Description))
|
||||
{
|
||||
<MudText Typo="Typo.body2" Align="Align.Center" Class="mud-text-secondary">@license.Description</MudText>
|
||||
}
|
||||
</MudCardContent>
|
||||
@if (!string.IsNullOrWhiteSpace(license.LinkUrl))
|
||||
{
|
||||
<MudCardActions Class="justify-center">
|
||||
<MudButton Variant="Variant.Text"
|
||||
Color="Color.Primary"
|
||||
Href="@license.LinkUrl"
|
||||
Target="_blank"
|
||||
StartIcon="@Icons.Material.Filled.OpenInNew">
|
||||
مشاهده جزئیات
|
||||
</MudButton>
|
||||
</MudCardActions>
|
||||
}
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Class="my-8" Dense="true">
|
||||
اطلاعات مجوزها بهزودی بارگذاری خواهد شد.
|
||||
</MudAlert>
|
||||
}
|
||||
</MudContainer>
|
||||
</section>
|
||||
|
||||
<!-- CTA Section -->
|
||||
<section class="py-12">
|
||||
<MudContainer MaxWidth="MaxWidth.Medium">
|
||||
<MudPaper Elevation="3" Class="pa-8 rounded-xl text-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.VerifiedUser" Size="Size.Large" Color="Color.Primary" Class="mb-4" />
|
||||
<MudText Typo="Typo.h4" Class="mb-3">نیاز به اطلاعات بیشتر دارید؟</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mud-text-secondary mb-6">
|
||||
برای دریافت اطلاعات بیشتر درباره مجوزها و گواهینامههای ما، با ما تماس بگیرید.
|
||||
</MudText>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Large"
|
||||
OnClick="() => Navigation.NavigateTo(RouteConstants.Contact.Index)">
|
||||
تماس با ما
|
||||
</MudButton>
|
||||
</MudPaper>
|
||||
</MudContainer>
|
||||
</section>
|
||||
|
||||
</MudStack>
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
using FrontOffice.Main.Utilities;
|
||||
|
||||
namespace FrontOffice.Main.Pages;
|
||||
|
||||
public partial class Licenses
|
||||
{
|
||||
private bool _loading = true;
|
||||
private PageSettingsDto? _pageData;
|
||||
private List<PageSettingsImageDto> _licenseImages = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_pageData = await PageSettingsService.GetPageAsync("licenses");
|
||||
|
||||
if (_pageData != null)
|
||||
{
|
||||
_licenseImages = _pageData.GetImages("licenses");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback: page remains null → hardcoded content will render
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
<MudIcon Icon="@Icons.Material.Filled.CardGiftcard" Size="Size.Large" Color="Color.Primary" Style="font-size: 80px;" />
|
||||
<MudText Typo="Typo.h5">شما هنوز پکیجی خریداری نکردهاید</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mud-text-secondary" Style="max-width: 500px;">
|
||||
با خرید پکیج، به باشگاه مشتریان بپیوندید و از مزایای ویژه مانند پاداش هفتگی و تیمسازی بهرهمند شوید.
|
||||
با خرید پکیج طلایی، به باشگاه مشتریان بپیوندید و از مزایای ویژه مانند پاداش هفتگی و تیمسازی بهرهمند شوید.
|
||||
</MudText>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
@@ -34,76 +34,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
@* ── G3: راهنمای پکیجهای من (Q28) ── *@
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-2" Icon="@Icons.Material.Filled.Info">
|
||||
بعد از تکمیل چرخه کیفپول جادویی، میتوانید مجدداً پکیج خریداری کرده و دور جدیدی را آغاز کنید.
|
||||
</MudAlert>
|
||||
|
||||
<MudGrid Spacing="4">
|
||||
|
||||
@* Re-Purchase / Cycle Progress Section *@
|
||||
@if (_canRepurchase)
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<MudAlert Severity="Severity.Success" Icon="@Icons.Material.Filled.Celebration" Class="mb-0">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Style="width:100%">
|
||||
<MudText Typo="Typo.body1">
|
||||
🎉 چرخه جادویی تکمیل شد! میتوانید پکیج جدید بخرید.
|
||||
</MudText>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.ShoppingCart"
|
||||
OnClick="@(() => Navigation.NavigateTo(RouteConstants.Package.List))">
|
||||
خرید پکیج جدید
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudAlert>
|
||||
</MudItem>
|
||||
}
|
||||
else if (_magicStatus != null && _magicStatus.WalletMode == 1)
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<MudPaper Elevation="2" Class="pa-6">
|
||||
<MudStack Spacing="3">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.AutoAwesome" Color="Color.Warning" />
|
||||
<MudText Typo="Typo.h6">پیشرفت چرخه جادویی</MudText>
|
||||
@if (_magicStatus.PurchaseCycleCount > 0)
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Info">دور @(_magicStatus.PurchaseCycleCount + 1)</MudChip>
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
<MudDivider />
|
||||
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">واریز انجامشده:</MudText>
|
||||
<MudText Typo="Typo.body2">@FormatPrice(_magicStatus.MagicTotalDeposited)</MudText>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">سقف واریز:</MudText>
|
||||
<MudText Typo="Typo.body2">@FormatPrice(_magicStatus.MagicMaxDeposit)</MudText>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">باقیمانده:</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Warning">@FormatPrice(_magicStatus.MagicRemainingDeposit)</MudText>
|
||||
</MudStack>
|
||||
|
||||
<MudProgressLinear Color="Color.Primary" Value="@GetDepositProgress()" Class="my-2" Rounded="true" Size="Size.Large">
|
||||
<MudText Typo="Typo.caption"><b>@GetDepositProgress()%</b></MudText>
|
||||
</MudProgressLinear>
|
||||
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">اعتبار دریافتی:</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Success">@FormatPrice(_magicStatus.MagicTotalCredited)</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
}
|
||||
|
||||
<!-- Package Status Card -->
|
||||
<MudItem xs="12" md="6">
|
||||
<MudPaper Elevation="2" Class="pa-6 h-100">
|
||||
@@ -118,7 +49,7 @@
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">نوع پکیج:</MudText>
|
||||
<MudText Typo="Typo.body2">@_userStatus.PackageTitle</MudText>
|
||||
<MudText Typo="Typo.body2">پکیج طلایی</MudText>
|
||||
</MudStack>
|
||||
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
@@ -215,7 +146,7 @@
|
||||
<MudStack Spacing="3">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Stars" Color="Color.Warning" />
|
||||
<MudText Typo="Typo.h6">مزایای پکیج</MudText>
|
||||
<MudText Typo="Typo.h6">مزایای پکیج طلایی</MudText>
|
||||
</MudStack>
|
||||
|
||||
<MudDivider />
|
||||
@@ -240,7 +171,7 @@
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Outlined="true" Class="pa-4 text-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ShoppingBag" Color="Color.Secondary" Size="Size.Large" />
|
||||
<MudText Typo="Typo.subtitle2" Class="mt-2">فروشگاه اعتباری</MudText>
|
||||
<MudText Typo="Typo.subtitle2" Class="mt-2">فروشگاه تخفیفی</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">خرید با تخفیف ویژه</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
@@ -9,9 +9,7 @@ public partial class MyPackages : ComponentBase
|
||||
[Inject] private PackageService PackageService { get; set; } = default!;
|
||||
|
||||
private UserPackageStatusDto? _userStatus;
|
||||
private MagicWalletStatus? _magicStatus;
|
||||
private bool _isLoading = true;
|
||||
private bool _canRepurchase = false;
|
||||
|
||||
private List<BreadcrumbItem> _breadcrumbItems = new()
|
||||
{
|
||||
@@ -32,18 +30,6 @@ public partial class MyPackages : ComponentBase
|
||||
try
|
||||
{
|
||||
_userStatus = await PackageService.GetUserPackageStatusAsync();
|
||||
|
||||
if (_userStatus?.HasPurchasedPackage == true)
|
||||
{
|
||||
_magicStatus = await WalletService.GetMagicWalletStatusAsync();
|
||||
|
||||
// Cycle complete: wallet back to Normal mode, at least one cycle done,
|
||||
// and deposit ceiling fully used (remaining = 0)
|
||||
_canRepurchase = _magicStatus != null
|
||||
&& _magicStatus.WalletMode == 0 // Normal mode
|
||||
&& _magicStatus.PurchaseCycleCount >= 1 // At least one cycle completed
|
||||
&& _magicStatus.MagicRemainingDeposit == 0; // Deposit ceiling used up
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -81,11 +67,4 @@ public partial class MyPackages : ComponentBase
|
||||
{
|
||||
return string.Format("{0:N0} تومان", price);
|
||||
}
|
||||
|
||||
private int GetDepositProgress()
|
||||
{
|
||||
if (_magicStatus == null || _magicStatus.MagicMaxDeposit <= 0) return 0;
|
||||
var pct = (int)((_magicStatus.MagicTotalDeposited * 100) / _magicStatus.MagicMaxDeposit);
|
||||
return Math.Min(pct, 100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,6 @@
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="py-8">
|
||||
<PageHeader Title="پکیجهای سرمایهگذاری" BackHref="@RouteConstants.Profile.Index" />
|
||||
|
||||
@* ── G1: راهنمای سیستم پکیجبیس (Q28) ── *@
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-4" Icon="@Icons.Material.Filled.Info">
|
||||
هر پکیج ویژگیها و مزایای منحصربهفردی دارد. با انتخاب پکیج مناسب، از سقف پاداش و ضریب کیفپول جادویی متفاوتی بهرهمند میشوید.
|
||||
</MudAlert>
|
||||
|
||||
@if (_isLoading)
|
||||
{
|
||||
<MudStack AlignItems="AlignItems.Center" Class="py-16">
|
||||
@@ -32,7 +27,7 @@
|
||||
{
|
||||
<MudAlert Severity="Severity.Success" Class="mb-6" Icon="@Icons.Material.Filled.CheckCircle">
|
||||
<MudText>
|
||||
شما قبلاً پکیج را خریداری کردهاید.
|
||||
شما قبلاً پکیج طلایی را خریداری کردهاید.
|
||||
@if (_userStatus.IsClubMemberActive)
|
||||
{
|
||||
<span>عضویت باشگاه شما فعال است.</span>
|
||||
@@ -69,43 +64,20 @@
|
||||
<MudText Typo="Typo.h6" Color="Color.Success">@package.FormattedPrice</MudText>
|
||||
</MudStack>
|
||||
|
||||
<!-- Package Highlights -->
|
||||
<!-- Features Preview -->
|
||||
<MudStack Spacing="1" Class="mt-2">
|
||||
@if (package.SupportsDirectPurchase)
|
||||
{
|
||||
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.caption">پرداخت مستقیم</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
@if (package.SupportsDayaPurchase)
|
||||
{
|
||||
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.caption">پرداخت با اعتبار دایا</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
@if (package.DiscountMultiplier > 0)
|
||||
{
|
||||
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.caption">ضریب اعتبار: @package.DiscountMultiplier.ToString("F1")x</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
@if (package.MagicWalletMultiplier > 0)
|
||||
{
|
||||
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.caption">کیف پول جادویی: @package.MagicWalletMultiplier.ToString("F1")x</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
@if (package.IsBasePackage)
|
||||
{
|
||||
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Star" Size="Size.Small" Color="Color.Warning" />
|
||||
<MudText Typo="Typo.caption">پکیج پایه</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.caption">عضویت در باشگاه مشتریان</MudText>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.caption">دریافت پاداش هفتگی</MudText>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.caption">تیمسازی نامحدود</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
@@ -128,9 +100,9 @@
|
||||
<MudGrid Spacing="4">
|
||||
<MudItem xs="12" md="8">
|
||||
<MudStack Spacing="2">
|
||||
<MudText Typo="Typo.h5" Style="color: white;">چرا پکیج بخریم؟</MudText>
|
||||
<MudText Typo="Typo.h5" Style="color: white;">چرا پکیج طلایی؟</MudText>
|
||||
<MudText Typo="Typo.body1" Style="color: rgba(255,255,255,0.9);">
|
||||
با خرید پکیج، علاوه بر دسترسی به محصولات ویژه، میتوانید تیم فروش خود را بسازید
|
||||
با خرید پکیج طلایی، علاوه بر دسترسی به محصولات ویژه، میتوانید تیم فروش خود را بسازید
|
||||
و از پاداشهای هفتگی بهرهمند شوید. هر چه تیم شما گستردهتر، درآمد شما بیشتر!
|
||||
</MudText>
|
||||
</MudStack>
|
||||
|
||||
@@ -64,38 +64,36 @@ public partial class PackageDetail : IDisposable
|
||||
|
||||
try
|
||||
{
|
||||
// Load package details via Customer RPC
|
||||
var response = await PackageClient.GetCustomerPackageDetailsAsync(
|
||||
new GetCustomerPackageDetailsRequest { PackageId = Id },
|
||||
cancellationToken: _loadCts.Token);
|
||||
// Load package details
|
||||
var packageResponse = await PackageClient.GetPackageAsync(request: new() { Id = Id}, cancellationToken: _loadCts.Token);
|
||||
|
||||
if (response != null)
|
||||
if (packageResponse != null)
|
||||
{
|
||||
// Build features from API response
|
||||
var features = response.Features?.Select(f => f.Title).ToList() ?? new List<string>();
|
||||
|
||||
// Build specifications from API features (highlighted ones)
|
||||
var specs = response.Features?
|
||||
.Where(f => f.IsHighlighted)
|
||||
.Select(f => new Specification
|
||||
{
|
||||
Name = f.Title,
|
||||
Value = f.Description,
|
||||
Icon = string.IsNullOrEmpty(f.Icon) ? Icons.Material.Filled.Star : f.Icon
|
||||
}).ToList() ?? new List<Specification>();
|
||||
|
||||
_package = new PackageDetailDto
|
||||
{
|
||||
Id = response.Id,
|
||||
Title = response.Title,
|
||||
Body = response.Description,
|
||||
Image = response.ImagePath ?? string.Empty,
|
||||
Specifications = specs,
|
||||
Features = features,
|
||||
Id = packageResponse.Id,
|
||||
Title = packageResponse.Title,
|
||||
Body = packageResponse.Description,
|
||||
Image = packageResponse.ImagePath ?? string.Empty,
|
||||
Specifications = new List<Specification>
|
||||
{
|
||||
new() { Name = "ظرفیت", Value = "تا ۲۰۰ عضو", Icon = Icons.Material.Filled.Group },
|
||||
new() { Name = "شجرهنامه", Value = "پیشرفته", Icon = Icons.Material.Filled.AccountTree },
|
||||
new() { Name = "گزارشگیری", Value = "جامع", Icon = Icons.Material.Filled.Analytics },
|
||||
new() { Name = "پشتیبانی", Value = "۲۴ ساعته", Icon = Icons.Material.Filled.Support }
|
||||
},
|
||||
Features = new List<string>
|
||||
{
|
||||
"مدیریت تیم نامحدود",
|
||||
"شجرهنامه بصری",
|
||||
"محاسبه کارمزد خودکار",
|
||||
"گزارشهای مالی",
|
||||
"پشتیبانی اولویتدار"
|
||||
},
|
||||
Pricing = new PricingInfo
|
||||
{
|
||||
OriginalPrice = response.Price,
|
||||
FinalPrice = response.Price,
|
||||
OriginalPrice = packageResponse.Price,
|
||||
FinalPrice = packageResponse.Price,
|
||||
HasDiscount = false,
|
||||
DiscountPercent = 0
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using FrontOffice.Main.Utilities;
|
||||
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
||||
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
||||
using FrontOffice.Main.Pages.Profile.Components;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
@@ -9,7 +9,6 @@ namespace FrontOffice.Main.Pages.Profile;
|
||||
public partial class Addresses : ComponentBase
|
||||
{
|
||||
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||
|
||||
private List<CustomerAddressModel> _addresses = new();
|
||||
private bool _isLoadingAddresses;
|
||||
@@ -43,10 +42,7 @@ public partial class Addresses : ComponentBase
|
||||
var dialog = await DialogService.ShowAsync<AddAddressDialog>("افزودن آدرس جدید");
|
||||
var result = await dialog.Result;
|
||||
if (result is not null && !result.Canceled)
|
||||
{
|
||||
await LoadAddresses();
|
||||
await AuthService.RefreshTokenAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenEditAddressDialog(CustomerAddressModel address)
|
||||
@@ -84,7 +80,6 @@ public partial class Addresses : ComponentBase
|
||||
await UserAddressContract.DeleteCustomerAddressAsync(new() { Id = id });
|
||||
Snackbar.Add("آدرس حذف شد.", Severity.Success);
|
||||
await LoadAddresses();
|
||||
await AuthService.RefreshTokenAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
@attribute [Route(RouteConstants.Profile.ChargeCreditWallet)]
|
||||
@attribute [Authorize]
|
||||
|
||||
<PageTitle>شارژ کیفپول اصلی</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
|
||||
<MudStack Spacing="3">
|
||||
<PageHeader Title="💳 شارژ کیفپول اصلی" BackHref="@RouteConstants.Profile.Wallet" />
|
||||
|
||||
@if (_paymentResult != null)
|
||||
{
|
||||
<MudAlert Severity="@(_paymentResult == "success" ? MudBlazor.Severity.Success : MudBlazor.Severity.Error)"
|
||||
Class="rounded-lg" Variant="Variant.Filled">
|
||||
@if (_paymentResult == "success")
|
||||
{
|
||||
<MudStack Spacing="2">
|
||||
<span>شارژ کیف پول اصلی با موفقیت انجام شد! ✅</span>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.ShoppingCart"
|
||||
OnClick="ContinueShoppingAsync">
|
||||
ادامه خرید
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
}
|
||||
else if (_paymentResult == "cancelled")
|
||||
{
|
||||
<span>پرداخت توسط شما لغو شد.</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>پرداخت ناموفق بود. لطفاً دوباره تلاش کنید.</span>
|
||||
}
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
@if (_authLoaded && _redirectingToMagic)
|
||||
{
|
||||
<MudAlert Severity="MudBlazor.Severity.Info" Class="rounded-lg" Variant="Variant.Outlined">
|
||||
<MudStack Spacing="2" AlignItems="AlignItems.Center">
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Color="Color.Primary" />
|
||||
<MudText>
|
||||
کیف پول شما در حالت جادویی است. در حال انتقال به صفحه شارژ کیف پول جادویی...
|
||||
</MudText>
|
||||
<MudButton Size="Size.Small" Variant="Variant.Filled" Color="Color.Primary"
|
||||
Href="@RouteConstants.Profile.MagicWallet"
|
||||
StartIcon="@Icons.Material.Filled.AutoAwesome">
|
||||
رفتن به شارژ کیف جادویی
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudAlert>
|
||||
}
|
||||
else if (_authLoaded && !_isClubMemberActive)
|
||||
{
|
||||
<MudAlert Severity="MudBlazor.Severity.Warning" Class="rounded-lg" Variant="Variant.Outlined">
|
||||
<MudStack Spacing="2">
|
||||
<MudText>
|
||||
برای شارژ کیف پول اصلی ابتدا باید پکیج را خریداری کرده و قرارداد باشگاه مشتریان را امضا کنید.
|
||||
</MudText>
|
||||
<MudStack Row="true" Spacing="1" Class="flex-wrap">
|
||||
@if (!_hasPurchasedPackage)
|
||||
{
|
||||
<MudButton Size="Size.Small" Variant="Variant.Filled" Color="Color.Primary"
|
||||
Href="@RouteConstants.Package.List"
|
||||
StartIcon="@Icons.Material.Filled.CardGiftcard">
|
||||
مشاهده پکیجها
|
||||
</MudButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton Size="Size.Small" Variant="Variant.Filled" Color="Color.Primary"
|
||||
Href="@RouteConstants.Club.Membership"
|
||||
StartIcon="@Icons.Material.Filled.Handshake">
|
||||
امضای قرارداد باشگاه
|
||||
</MudButton>
|
||||
}
|
||||
<MudButton Size="Size.Small" Variant="Variant.Text"
|
||||
Href="@RouteConstants.Profile.Wallet">
|
||||
بازگشت به کیف پول
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudAlert>
|
||||
}
|
||||
else if (_authLoaded && _needsMagicCeilingConsent && !_allowNormalDespiteMagic && !_redirectingToMagic)
|
||||
{
|
||||
<MudAlert Severity="MudBlazor.Severity.Warning" Class="rounded-lg" Variant="Variant.Outlined">
|
||||
<MudStack Spacing="3">
|
||||
<MudText Typo="Typo.subtitle1">
|
||||
سقف شارژ کیف پول جادویی در این دور پر شده است.
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body2">
|
||||
شارژ از این صفحه بدون ضریب (۱:۱) به موجودی اضافه میشود و ضریب جادویی اعمال نمیشود.
|
||||
برای ادامه، تأیید کنید.
|
||||
</MudText>
|
||||
<MudStack Row="true" Spacing="1" Class="flex-wrap">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Check"
|
||||
OnClick="AcceptMagicCeilingConsent">
|
||||
ادامه شارژ بدون ضریب
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Secondary"
|
||||
StartIcon="@Icons.Material.Filled.AutoAwesome"
|
||||
OnClick="DeclineMagicCeilingConsent">
|
||||
بازگشت به کیف جادویی
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudAlert>
|
||||
}
|
||||
else if (ShowChargeForm)
|
||||
{
|
||||
<MudPaper Elevation="2" Class="pa-5 rounded-lg">
|
||||
<MudText Typo="Typo.h6" Class="mb-3">
|
||||
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Class="ml-1" />
|
||||
شارژ موجودی اصلی
|
||||
</MudText>
|
||||
|
||||
@if (_allowNormalDespiteMagic)
|
||||
{
|
||||
<MudAlert Severity="MudBlazor.Severity.Warning" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Warning">
|
||||
این شارژ بدون ضریب جادویی است (۱:۱). سقف واریز جادویی شما در این دور پر شده است.
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
<MudAlert Severity="MudBlazor.Severity.Info" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info">
|
||||
مبلغ وارد شده (به تومان) از طریق درگاه پرداخت به موجودی اصلی شما اضافه میشود.
|
||||
از موجودی اصلی برای خرید از فروشگاه عادی و پرداخت سفارش استفاده میشود.
|
||||
</MudAlert>
|
||||
|
||||
<MudStack Spacing="2">
|
||||
<MudNumericField @bind-Value="_chargeAmount"
|
||||
Label="مبلغ (تومان)"
|
||||
Variant="Variant.Outlined"
|
||||
Min="10_000"
|
||||
Max="1_000_000_000"
|
||||
Format="N0"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Payments"
|
||||
HelperText="حداقل ۱۰,۰۰۰ و حداکثر ۱,۰۰۰,۰۰۰,۰۰۰ تومان" />
|
||||
|
||||
<MudStack Row="true" Spacing="1" Class="flex-wrap">
|
||||
@foreach (var preset in _presetAmounts)
|
||||
{
|
||||
<MudButton Variant="Variant.Outlined" Size="Size.Small"
|
||||
Color="@(_chargeAmount == preset ? Color.Primary : Color.Default)"
|
||||
OnClick="() => _chargeAmount = preset"
|
||||
Class="rounded-pill">
|
||||
@FormatToman(preset)
|
||||
</MudButton>
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
@if (_chargeAmount > 0)
|
||||
{
|
||||
<MudPaper Outlined="true" Class="pa-3 rounded-lg" Style="background: #eff6ff;">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2">مبلغ پرداخت:</MudText>
|
||||
<MudText Typo="Typo.body2"><strong>@FormatToman(_chargeAmount)</strong></MudText>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2">موجودی دریافتی:</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Primary"><strong>@FormatToman(_chargeAmount)</strong></MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Large"
|
||||
FullWidth="true"
|
||||
Disabled="@(_isProcessing || _chargeAmount < 10_000)"
|
||||
OnClick="StartCreditCharge"
|
||||
StartIcon="@Icons.Material.Filled.Payment"
|
||||
Class="rounded-lg mt-2">
|
||||
@if (_isProcessing)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="me-2" />
|
||||
<span>در حال انتقال به درگاه...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>پرداخت با کارت بانکی</span>
|
||||
}
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<MudExpansionPanels Elevation="1" Class="rounded-lg">
|
||||
<MudExpansionPanel Text="قوانین شارژ کیف پول اصلی" MaxHeight="500" IsInitiallyExpanded="false">
|
||||
<MudList T="string" Dense="true">
|
||||
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
|
||||
موجودی اصلی برای خرید از فروشگاه عادی و پرداخت سفارش قابل استفاده است
|
||||
</MudListItem>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
|
||||
حداقل مبلغ شارژ: ۱۰,۰۰۰ تومان
|
||||
</MudListItem>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
|
||||
مبلغ واریزی بدون ضریب به موجودی اضافه میشود (۱:۱)
|
||||
</MudListItem>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Filled.Info" IconColor="Color.Info">
|
||||
پرداخت از طریق درگاه بانکی انجام میشود
|
||||
</MudListItem>
|
||||
</MudList>
|
||||
</MudExpansionPanel>
|
||||
</MudExpansionPanels>
|
||||
}
|
||||
</MudStack>
|
||||
</MudContainer>
|
||||
@@ -1,199 +0,0 @@
|
||||
using FrontOffice.Main.Utilities;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using MudBlazor;
|
||||
|
||||
namespace FrontOffice.Main.Pages.Profile;
|
||||
|
||||
public partial class ChargeCreditWallet : ComponentBase
|
||||
{
|
||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||
|
||||
private bool _isProcessing;
|
||||
private long _chargeAmount;
|
||||
private string? _paymentResult;
|
||||
private string? _pendingReturnUrl;
|
||||
private bool _isClubMemberActive;
|
||||
private bool _hasPurchasedPackage;
|
||||
private bool _authLoaded;
|
||||
private bool _redirectingToMagic;
|
||||
private bool _allowNormalDespiteMagic;
|
||||
private bool _needsMagicCeilingConsent;
|
||||
|
||||
private readonly long[] _presetAmounts = { 500_000, 1_000_000, 5_000_000, 10_000_000, 20_000_000, 50_000_000 };
|
||||
|
||||
[SupplyParameterFromQuery(Name = "payment")]
|
||||
public string? PaymentQueryParam { get; set; }
|
||||
|
||||
[SupplyParameterFromQuery(Name = "amount")]
|
||||
public long? AmountQueryParam { get; set; }
|
||||
|
||||
[SupplyParameterFromQuery(Name = "returnUrl")]
|
||||
public string? ReturnUrlQueryParam { get; set; }
|
||||
|
||||
private bool ShowChargeForm =>
|
||||
_authLoaded
|
||||
&& _isClubMemberActive
|
||||
&& !_redirectingToMagic
|
||||
&& (!_needsMagicCeilingConsent || _allowNormalDespiteMagic);
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_paymentResult = PaymentQueryParam;
|
||||
|
||||
if (AmountQueryParam is > 0)
|
||||
_chargeAmount = CreditChargeNavigation.NormalizeChargeAmount(AmountQueryParam.Value);
|
||||
|
||||
if (CreditChargeNavigation.IsValidReturnUrl(ReturnUrlQueryParam))
|
||||
_pendingReturnUrl = ReturnUrlQueryParam;
|
||||
|
||||
try
|
||||
{
|
||||
var userInfo = await AuthService.GetUserAuthInfo();
|
||||
_isClubMemberActive = userInfo.IsClubMemberActive;
|
||||
_hasPurchasedPackage = userInfo.HasPurchasedPackage;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_isClubMemberActive = false;
|
||||
_hasPurchasedPackage = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_authLoaded = true;
|
||||
}
|
||||
|
||||
if (_isClubMemberActive)
|
||||
await EvaluateMagicWalletGateAsync();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender && !string.IsNullOrEmpty(_pendingReturnUrl) && _isClubMemberActive && !_redirectingToMagic)
|
||||
await CreditChargeNavigation.SaveReturnUrlAsync(JSRuntime, _pendingReturnUrl);
|
||||
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
private void AcceptMagicCeilingConsent()
|
||||
{
|
||||
_allowNormalDespiteMagic = true;
|
||||
_redirectingToMagic = false;
|
||||
}
|
||||
|
||||
private void DeclineMagicCeilingConsent()
|
||||
{
|
||||
_redirectingToMagic = true;
|
||||
Navigation.NavigateTo(RouteConstants.Profile.MagicWallet);
|
||||
}
|
||||
|
||||
private async Task StartCreditCharge()
|
||||
{
|
||||
if (!_isClubMemberActive)
|
||||
{
|
||||
Snackbar.Add(
|
||||
"برای شارژ کیف پول اصلی ابتدا باید پکیج را خریداری کرده و قرارداد باشگاه مشتریان را امضا کنید.",
|
||||
Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await EvaluateMagicWalletGateAsync())
|
||||
return;
|
||||
|
||||
if (_needsMagicCeilingConsent && !_allowNormalDespiteMagic)
|
||||
return;
|
||||
|
||||
if (_chargeAmount <= 0 || _isProcessing) return;
|
||||
|
||||
_isProcessing = true;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_pendingReturnUrl))
|
||||
await CreditChargeNavigation.SaveReturnUrlAsync(JSRuntime, _pendingReturnUrl);
|
||||
else
|
||||
{
|
||||
var storedReturnUrl = await CreditChargeNavigation.GetReturnUrlAsync(JSRuntime);
|
||||
if (CreditChargeNavigation.IsValidReturnUrl(storedReturnUrl))
|
||||
_pendingReturnUrl = storedReturnUrl;
|
||||
}
|
||||
|
||||
var (success, gatewayUrl, error) = await WalletService.InitiateCreditChargeAsync(_chargeAmount);
|
||||
|
||||
if (success && !string.IsNullOrEmpty(gatewayUrl))
|
||||
{
|
||||
Navigation.NavigateTo(gatewayUrl, forceLoad: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add(error ?? "خطا در ایجاد درخواست پرداخت", Severity.Error);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isProcessing = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Magic با ظرفیت باقیمانده → ریدایرکت به MagicWallet.
|
||||
/// Magic با سقف پر → نمایش تأیید روی صفحه برای شارژ ۱:۱.
|
||||
/// true یعنی مسیر شارژ فعلی باید متوقف شود.
|
||||
/// </summary>
|
||||
private async Task<bool> EvaluateMagicWalletGateAsync()
|
||||
{
|
||||
if (_allowNormalDespiteMagic)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var status = await WalletService.GetMagicWalletStatusAsync();
|
||||
if (status.WalletMode != 1)
|
||||
{
|
||||
_needsMagicCeilingConsent = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (status.MagicRemainingDeposit > 0)
|
||||
{
|
||||
var multiplier = status.MagicMultiplier > 0 ? status.MagicMultiplier : 2.5;
|
||||
var multiplierText = multiplier.ToString("0.#");
|
||||
|
||||
_redirectingToMagic = true;
|
||||
_needsMagicCeilingConsent = false;
|
||||
Snackbar.Add(
|
||||
$"کیف پول شما در حالت جادویی است. شارژ از این صفحه موجودی را چندبرابر نمیکند. برای دریافت ضریب جادویی (×{multiplierText}) به صفحه شارژ کیف پول جادویی هدایت میشوید.",
|
||||
Severity.Info);
|
||||
|
||||
Navigation.NavigateTo(RouteConstants.Profile.MagicWallet);
|
||||
return true;
|
||||
}
|
||||
|
||||
_needsMagicCeilingConsent = true;
|
||||
return !_allowNormalDespiteMagic;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// در صورت خطا در خواندن وضعیت، مانع شارژ عادی نشو
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ContinueShoppingAsync()
|
||||
{
|
||||
var returnUrl = await CreditChargeNavigation.GetReturnUrlAsync(JSRuntime);
|
||||
await CreditChargeNavigation.ClearReturnUrlAsync(JSRuntime);
|
||||
|
||||
if (CreditChargeNavigation.IsValidReturnUrl(returnUrl))
|
||||
Navigation.NavigateTo(returnUrl!);
|
||||
}
|
||||
|
||||
private static string FormatToman(long toman)
|
||||
=> string.Format("{0:N0} تومان", toman);
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
@attribute [Route(RouteConstants.Profile.ChargeDiscountWallet)]
|
||||
@attribute [Authorize]
|
||||
|
||||
<PageTitle>شارژ کیفپول اعتباری</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
|
||||
<MudStack Spacing="3">
|
||||
<PageHeader Title="💳 شارژ کیفپول اعتباری" BackHref="@RouteConstants.Profile.Wallet" />
|
||||
|
||||
@* ─── نتیجه پرداخت (اگه از callback برگشته) ─── *@
|
||||
@if (_paymentResult != null)
|
||||
{
|
||||
<MudAlert Severity="@(_paymentResult == "success" ? MudBlazor.Severity.Success : MudBlazor.Severity.Error)"
|
||||
Class="rounded-lg" Variant="Variant.Filled">
|
||||
@if (_paymentResult == "success")
|
||||
{
|
||||
<span>شارژ اعتباری با موفقیت انجام شد! ✅</span>
|
||||
}
|
||||
else if (_paymentResult == "cancelled")
|
||||
{
|
||||
<span>پرداخت توسط شما لغو شد.</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>پرداخت ناموفق بود. لطفاً دوباره تلاش کنید.</span>
|
||||
}
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
@* ─── فرم شارژ ─── *@
|
||||
<MudPaper Elevation="2" Class="pa-5 rounded-lg">
|
||||
<MudText Typo="Typo.h6" Class="mb-3">
|
||||
<MudIcon Icon="@Icons.Material.Filled.CreditCard" Class="ml-1" />
|
||||
شارژ موجودی اعتباری
|
||||
</MudText>
|
||||
|
||||
<MudAlert Severity="MudBlazor.Severity.Info" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info">
|
||||
مبلغ وارد شده (به تومان) از طریق درگاه پرداخت به موجودی اعتباری شما اضافه میشود.
|
||||
از موجودی اعتباری فقط برای خرید از فروشگاه اعتباری میتوانید استفاده کنید.
|
||||
</MudAlert>
|
||||
|
||||
<MudStack Spacing="2">
|
||||
<MudNumericField @bind-Value="_chargeAmount"
|
||||
Label="مبلغ (تومان)"
|
||||
Variant="Variant.Outlined"
|
||||
Min="10_000"
|
||||
Max="1_000_000_000"
|
||||
Format="N0"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Payments"
|
||||
HelperText="حداقل ۱۰,۰۰۰ و حداکثر ۱,۰۰۰,۰۰۰,۰۰۰ تومان" />
|
||||
|
||||
@* دکمههای مبلغ سریع *@
|
||||
<MudStack Row="true" Spacing="1" Class="flex-wrap">
|
||||
@foreach (var preset in _presetAmounts)
|
||||
{
|
||||
<MudButton Variant="Variant.Outlined" Size="Size.Small"
|
||||
Color="@(_chargeAmount == preset ? Color.Primary : Color.Default)"
|
||||
OnClick="() => _chargeAmount = preset"
|
||||
Class="rounded-pill">
|
||||
@FormatToman(preset)
|
||||
</MudButton>
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
@if (_chargeAmount > 0)
|
||||
{
|
||||
<MudPaper Outlined="true" Class="pa-3 rounded-lg" Style="background: #f0fdf4;">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2">مبلغ پرداخت:</MudText>
|
||||
<MudText Typo="Typo.body2"><strong>@FormatToman(_chargeAmount)</strong></MudText>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2">اعتبار دریافتی:</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Success"><strong>@FormatToman(_chargeAmount)</strong></MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Large"
|
||||
FullWidth="true"
|
||||
Disabled="@(_isProcessing || _chargeAmount < 10_000)"
|
||||
OnClick="StartDiscountCharge"
|
||||
StartIcon="@Icons.Material.Filled.Payment"
|
||||
Class="rounded-lg mt-2">
|
||||
@if (_isProcessing)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="me-2" />
|
||||
<span>در حال انتقال به درگاه...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>پرداخت با کارت بانکی</span>
|
||||
}
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
@* ─── قوانین ─── *@
|
||||
<MudExpansionPanels Elevation="1" Class="rounded-lg">
|
||||
<MudExpansionPanel Text="قوانین شارژ اعتباری" MaxHeight="500" IsInitiallyExpanded="false">
|
||||
<MudList T="string" Dense="true">
|
||||
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
|
||||
موجودی اعتباری فقط قابل استفاده در فروشگاه اعتباری است
|
||||
</MudListItem>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
|
||||
حداقل مبلغ شارژ: ۱۰,۰۰۰ تومان
|
||||
</MudListItem>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
|
||||
مبلغ واریزی بدون ضریب به موجودی اضافه میشود (۱:۱)
|
||||
</MudListItem>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Filled.Info" IconColor="Color.Info">
|
||||
پرداخت از طریق درگاه بانکی انجام میشود
|
||||
</MudListItem>
|
||||
</MudList>
|
||||
</MudExpansionPanel>
|
||||
</MudExpansionPanels>
|
||||
</MudStack>
|
||||
</MudContainer>
|
||||
@@ -1,57 +0,0 @@
|
||||
using FrontOffice.Main.Utilities;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace FrontOffice.Main.Pages.Profile;
|
||||
|
||||
public partial class ChargeDiscountWallet : ComponentBase
|
||||
{
|
||||
private bool _isProcessing;
|
||||
private long _chargeAmount;
|
||||
private string? _paymentResult;
|
||||
|
||||
private readonly long[] _presetAmounts = { 500_000, 1_000_000, 5_000_000, 10_000_000, 20_000_000, 50_000_000 };
|
||||
|
||||
[SupplyParameterFromQuery(Name = "payment")]
|
||||
public string? PaymentQueryParam { get; set; }
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_paymentResult = PaymentQueryParam;
|
||||
}
|
||||
|
||||
private async Task StartDiscountCharge()
|
||||
{
|
||||
if (_chargeAmount <= 0 || _isProcessing) return;
|
||||
|
||||
_isProcessing = true;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
// مبلغ به تومان — CMS خودش موقع ارسال به درگاه ×۱۰ میکنه
|
||||
var (success, gatewayUrl, error) = await WalletService.InitiateDiscountChargeAsync(_chargeAmount);
|
||||
|
||||
if (success && !string.IsNullOrEmpty(gatewayUrl))
|
||||
{
|
||||
Navigation.NavigateTo(gatewayUrl, forceLoad: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add(error ?? "خطا در ایجاد درخواست پرداخت", Severity.Error);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isProcessing = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatToman(long toman)
|
||||
=> string.Format("{0:N0} تومان", toman);
|
||||
}
|
||||
@@ -61,11 +61,11 @@
|
||||
<span class="stat-value">@_statistics.TotalMembers</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">سازمان چپ:</span>
|
||||
<span class="stat-label">پای چپ:</span>
|
||||
<span class="stat-value left">@_statistics.LeftLegCount</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">سازمان راست:</span>
|
||||
<span class="stat-label">پای راست:</span>
|
||||
<span class="stat-value right">@_statistics.RightLegCount</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using FrontOffice.Main.Utilities;
|
||||
using FrontOffice.Main.Utilities;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
@@ -95,27 +95,7 @@ public partial class OrganizationChart : IAsyncDisposable
|
||||
try
|
||||
{
|
||||
_dotNetHelper = DotNetObjectReference.Create(this);
|
||||
// Explicit camelCase mapping (same pattern as BackOffice admin chart)
|
||||
var flatData = _networkTree.ToFlatArray().Select(n => new
|
||||
{
|
||||
id = n.Id,
|
||||
parentId = n.ParentId,
|
||||
fullName = n.FullName,
|
||||
mobile = n.Mobile,
|
||||
avatar = n.Avatar,
|
||||
position = n.Position,
|
||||
level = n.Level,
|
||||
isActive = n.IsActive,
|
||||
isClubActive = n.IsClubActive,
|
||||
activationWeekNumber = n.ActivationWeekNumber,
|
||||
joinedAt = n.JoinedAt,
|
||||
referralCode = n.ReferralCode,
|
||||
packageName = n.PackageName ?? string.Empty,
|
||||
goldLeftLegTotal = n.GoldLeftLegTotal,
|
||||
goldRightLegTotal = n.GoldRightLegTotal,
|
||||
silverLeftLegTotal = n.SilverLeftLegTotal,
|
||||
silverRightLegTotal = n.SilverRightLegTotal
|
||||
}).ToArray();
|
||||
var flatData = _networkTree.ToFlatArray();
|
||||
|
||||
await JSRuntime.InvokeVoidAsync("OrgChart.init", "org-chart-container", flatData, _dotNetHelper);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<div class="wallet-strip-item">
|
||||
<div class="wallet-strip-label">
|
||||
<MudIcon Icon="@Icons.Material.Outlined.CreditCard" Size="Size.Small" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">اصلی</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">اعتباری</MudText>
|
||||
</div>
|
||||
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@_walletCredit</MudText>
|
||||
</div>
|
||||
@@ -53,7 +53,7 @@
|
||||
<div class="wallet-strip-item">
|
||||
<div class="wallet-strip-label">
|
||||
<MudIcon Icon="@Icons.Material.Outlined.Discount" Size="Size.Small" Color="Color.Success" />
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">کیف اعتباری</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">تخفیف باشگاه</MudText>
|
||||
</div>
|
||||
<MudText Typo="Typo.subtitle2" Color="Color.Success">@_walletDiscount</MudText>
|
||||
</div>
|
||||
@@ -61,7 +61,7 @@
|
||||
<div class="wallet-strip-item">
|
||||
<div class="wallet-strip-label">
|
||||
<MudIcon Icon="@Icons.Material.Outlined.Groups" Size="Size.Small" Color="Color.Warning" />
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">پاداش های دریافتی</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">پاداش تیمی</MudText>
|
||||
</div>
|
||||
<MudText Typo="Typo.subtitle2" Color="Color.Warning">@_walletNetwork</MudText>
|
||||
</div>
|
||||
@@ -119,7 +119,7 @@
|
||||
لینک دعوت شما هنوز فعال نشده است
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-1" Style="max-width:560px; margin:0 auto;">
|
||||
برای فعالسازی لینک دعوت، ابتدا یکی از <strong>پکیجها</strong> را تهیه کنید و سپس در <strong>باشگاه مشتریان</strong> عضو شوید.
|
||||
برای فعالسازی لینک دعوت، ابتدا <strong>پکیج پایه</strong> را تهیه کنید و سپس در <strong>باشگاه مشتریان</strong> عضو شوید.
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-4" Style="max-width:560px; margin:0 auto;">
|
||||
از مزایای ویژه عضویت بهرهمند شده و با فعالیت در زمینه توسعه فروشگاهها پاداش دریافت کنید.
|
||||
@@ -130,7 +130,7 @@
|
||||
Class="rounded-pill"
|
||||
StartIcon="@Icons.Material.Filled.ShoppingCart"
|
||||
OnClick="OpenPurchaseOptions">
|
||||
تهیه پکیج
|
||||
شروع فرآیند تامین اعتبار
|
||||
</MudButton>
|
||||
</MudPaper>
|
||||
}
|
||||
@@ -195,4 +195,49 @@
|
||||
</MudGrid>
|
||||
</MudContainer>
|
||||
|
||||
@* ── Purchase Options Dialog ── *@
|
||||
<MudDialog @bind-Visible="_showPurchaseBottomSheet">
|
||||
<DialogContent>
|
||||
<MudStack Spacing="3" Class="pa-2">
|
||||
<MudText Typo="Typo.h6" Align="Align.Center">
|
||||
خرید پکیج پایه ۵۶ میلیون تومان
|
||||
</MudText>
|
||||
<MudDivider />
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">
|
||||
برای خرید پکیج پایه، یکی از روشهای زیر را انتخاب کنید:
|
||||
</MudText>
|
||||
|
||||
<MudPaper Elevation="0" Class="pa-4 rounded-lg" Style="border: 2px solid var(--mud-palette-primary);">
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.CreditCard" Color="Color.Primary" Size="Size.Medium" />
|
||||
<MudText Typo="Typo.subtitle1" Color="Color.Primary"><b>پرداخت مستقیم</b></MudText>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">پرداخت آنی از طریق درگاه بانکی</MudText>
|
||||
<MudButton Variant="Variant.Filled" Disabled="true" Color="Color.Primary" FullWidth="true"
|
||||
StartIcon="@Icons.Material.Filled.Payment" OnClick="DirectPayment">
|
||||
پرداخت با کارت بانکی(بزودی)
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<MudPaper Elevation="0" Class="pa-4 rounded-lg" Style="border: 2px solid var(--mud-palette-tertiary);">
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Diamond" Color="Color.Tertiary" Size="Size.Medium" />
|
||||
<MudText Typo="Typo.subtitle1" Color="Color.Tertiary"><b>اعتبار الماسی دایا</b></MudText>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">تأمین اعتبار از طریق خرید توکن الماس و دریافت وام</MudText>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Tertiary" FullWidth="true"
|
||||
StartIcon="@Icons.Material.Filled.AccountBalance" OnClick="DayaLoanPayment">
|
||||
تأمین اعتبار الماسی
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<MudButton Variant="Variant.Text" Color="Color.Default" FullWidth="true" OnClick="ClosePurchaseOptions">
|
||||
بستن
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</DialogContent>
|
||||
</MudDialog>
|
||||
|
||||
@@ -42,19 +42,18 @@ public partial class Index
|
||||
private bool _isClubMemberActive;
|
||||
private bool CanShowReferralLink => _hasPurchasedPackage && _isClubMemberActive;
|
||||
|
||||
// Purchase flow
|
||||
// Purchase Options Bottom Sheet
|
||||
private bool _showPurchaseBottomSheet;
|
||||
private bool _isProcessingPayment;
|
||||
private int _purchaseCycleCount; // تعداد دورهای خرید — اگر > 0 فقط IPG مجاز
|
||||
|
||||
// Dashboard Tiles
|
||||
private readonly List<DashTile> _dashTiles = new()
|
||||
{
|
||||
new(RouteConstants.Profile.Personal, Icons.Material.Filled.Person, "اطلاعات شخصی", "نمایش و ویرایش", "background:rgba(99,102,241,.12); color:#6366f1;"),
|
||||
new(RouteConstants.Profile.Addresses, Icons.Material.Filled.LocationOn, "آدرسها", "مدیریت آدرسها", "background:rgba(99,102,241,.12); color:#6366f1;"),
|
||||
new(RouteConstants.Profile.Tree, Icons.Material.Filled.AccountTree, "سازمان فروش", "ساختار تیم", "background:rgba(99,102,241,.12); color:#6366f1;"),
|
||||
new(RouteConstants.Profile.Tree, Icons.Material.Filled.AccountTree, "شجرهنامه", "ساختار تیم", "background:rgba(99,102,241,.12); color:#6366f1;"),
|
||||
new(RouteConstants.Gateway.StoreChooser, Icons.Material.Filled.Store, "فروشگاهها", "ورود به فروشگاه", "background:rgba(16,185,129,.12); color:#10b981;"),
|
||||
new(RouteConstants.Profile.Wallet, Icons.Material.Filled.AccountBalanceWallet, "کیف پول", "مدیریت و تاریخچه", "background:rgba(16,185,129,.12); color:#10b981;"),
|
||||
new(RouteConstants.Profile.MagicWallet, Icons.Material.Filled.AutoAwesome, "کیفپول جادویی", "شارژ چندبرابری", "background:rgba(168,85,247,.12); color:#a855f7;"),
|
||||
new(RouteConstants.Profile.WithdrawalRequests, Icons.Material.Filled.RequestPage, "درخواست برداشت", "ثبت و پیگیری", "background:rgba(245,158,11,.12); color:#f59e0b;"),
|
||||
new(RouteConstants.Club.Membership, Icons.Material.Filled.CardMembership, "باشگاه مشتریان", "عضویت و مزایا", "background:rgba(168,85,247,.12); color:#a855f7;"),
|
||||
new(RouteConstants.Network.Statistics, Icons.Material.Filled.Groups, "آمار باشگاه", "جزئیات باشگاه", "background:rgba(59,130,246,.12); color:#3b82f6;"),
|
||||
@@ -148,11 +147,6 @@ public partial class Index
|
||||
var discount = FormatPrice(b.DiscountBalance);
|
||||
_walletNetwork = FormatPrice(b.NetworkBalance);
|
||||
_walletDiscount = discount;
|
||||
|
||||
// بارگذاری تعداد دورهای خرید برای کنترل روشهای پرداخت
|
||||
var magicStatus = await WalletService.GetMagicWalletStatusAsync();
|
||||
_purchaseCycleCount = magicStatus.PurchaseCycleCount;
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
@@ -333,7 +327,6 @@ public partial class Index
|
||||
if (!result.Canceled)
|
||||
{
|
||||
await LoadAddresses();
|
||||
await AuthService.RefreshTokenAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,7 +379,6 @@ public partial class Index
|
||||
});
|
||||
Snackbar.Add("آدرس با موفقیت حذف شد.", Severity.Success);
|
||||
await LoadAddresses();
|
||||
await AuthService.RefreshTokenAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -395,62 +387,41 @@ public partial class Index
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenPurchaseOptions()
|
||||
private void OpenPurchaseOptions()
|
||||
{
|
||||
var options = new DialogOptions
|
||||
{
|
||||
MaxWidth = MaxWidth.Small,
|
||||
FullWidth = true,
|
||||
CloseOnEscapeKey = true,
|
||||
BackdropClick = true
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters<Shared.PackagePurchaseDialog>
|
||||
{
|
||||
{ x => x.PurchaseCycleCount, _purchaseCycleCount }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<Shared.PackagePurchaseDialog>(
|
||||
string.Empty, parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result.Canceled) return;
|
||||
|
||||
if (result.Data is Shared.PackagePurchaseDialog.PackagePurchaseResult purchase)
|
||||
{
|
||||
switch (purchase.Method)
|
||||
{
|
||||
case Shared.PackagePurchaseDialog.PaymentMethodType.DirectPayment:
|
||||
await ProcessDirectPayment(purchase.Package.Id);
|
||||
break;
|
||||
case Shared.PackagePurchaseDialog.PaymentMethodType.DayaLoan:
|
||||
await ProcessDayaLoanPayment();
|
||||
break;
|
||||
}
|
||||
}
|
||||
_showPurchaseBottomSheet = true;
|
||||
}
|
||||
|
||||
private async Task ProcessDirectPayment(long packageId)
|
||||
private void ClosePurchaseOptions()
|
||||
{
|
||||
_showPurchaseBottomSheet = false;
|
||||
}
|
||||
|
||||
private async Task DirectPayment()
|
||||
{
|
||||
if (_isProcessingPayment) return;
|
||||
|
||||
|
||||
_isProcessingPayment = true;
|
||||
StateHasChanged();
|
||||
|
||||
_showPurchaseBottomSheet = false;
|
||||
|
||||
try
|
||||
{
|
||||
var response = await PackageContract.CustomerPurchasePackageAsync(new CustomerPurchasePackageRequest
|
||||
// Create callback URL for payment verification
|
||||
var callbackUrl = $"{Navigation.BaseUri}profile/payment-callback";
|
||||
|
||||
// Call BFF to initiate payment (UserId is taken from JWT token in BFF)
|
||||
var response = await PackageContract.InitiateBasePackagePaymentAsync(new InitiateBasePackagePaymentRequest
|
||||
{
|
||||
PackageId = packageId,
|
||||
PurchaseMethod = PurchaseMethodEnum.PurchaseMethodGateway
|
||||
CallbackUrl = callbackUrl
|
||||
});
|
||||
|
||||
|
||||
if (!response.Success)
|
||||
{
|
||||
Snackbar.Add(response.Message ?? "خطا در آغاز فرآیند پرداخت", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Redirect to payment gateway
|
||||
if (!string.IsNullOrEmpty(response.PaymentGatewayUrl))
|
||||
{
|
||||
Navigation.NavigateTo(response.PaymentGatewayUrl, forceLoad: true);
|
||||
@@ -471,29 +442,41 @@ public partial class Index
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessDayaLoanPayment()
|
||||
private async Task DayaLoanPayment()
|
||||
{
|
||||
// Validate user info before redirecting
|
||||
if (string.IsNullOrWhiteSpace(_updateUserRequest.FirstName))
|
||||
{
|
||||
Snackbar.Add("لطفا اطلاعات شخصی خود را تکمیل کنید. (نام وارد نشده)", Severity.Error);
|
||||
_showPurchaseBottomSheet = false;
|
||||
Snackbar.Add($"لطفا اطلاعات شخصی خود را تکمیل کنید. (نام وارد نشده)", Severity.Error);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(_updateUserRequest.LastName))
|
||||
{
|
||||
Snackbar.Add("لطفا اطلاعات شخصی خود را تکمیل کنید. (نام خانوادگی وارد نشده)", Severity.Error);
|
||||
_showPurchaseBottomSheet = false;
|
||||
Snackbar.Add($"لطفا اطلاعات شخصی خود را تکمیل کنید. (نام خانوادگی وارد نشده)", Severity.Error);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(_updateUserRequest.NationalCode))
|
||||
{
|
||||
Snackbar.Add("لطفا اطلاعات شخصی خود را تکمیل کنید. (کدملی وارد نشده)", Severity.Error);
|
||||
_showPurchaseBottomSheet = false;
|
||||
Snackbar.Add($"لطفا اطلاعات شخصی خود را تکمیل کنید. (کدملی وارد نشده)", Severity.Error);
|
||||
return;
|
||||
}
|
||||
if (_updateUserRequest.BirthDate == null)
|
||||
{
|
||||
Snackbar.Add("لطفا اطلاعات شخصی خود را تکمیل کنید. (تاریخ تولد وارد نشده)", Severity.Error);
|
||||
_showPurchaseBottomSheet = false;
|
||||
Snackbar.Add($"لطفا اطلاعات شخصی خود را تکمیل کنید. (تاریخ تولد وارد نشده)", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_addresses.Any())
|
||||
{
|
||||
_showPurchaseBottomSheet = false;
|
||||
Snackbar.Add($"آدرس محل سکونت شما الزامی است!", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
_showPurchaseBottomSheet = false;
|
||||
var url = "https://dayadiamond.ir/profile/creditpurchase/?merchantcode=56146364";
|
||||
await JSRuntime.InvokeVoidAsync("open", url, "_blank");
|
||||
}
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
@attribute [Route(RouteConstants.Profile.MagicWallet)]
|
||||
@attribute [Authorize]
|
||||
|
||||
<PageTitle>کیفپول جادویی</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
|
||||
<MudStack Spacing="3">
|
||||
<PageHeader Title="🪄 کیفپول جادویی" BackHref="@RouteConstants.Profile.Wallet" />
|
||||
|
||||
@if (_isLoading)
|
||||
{
|
||||
<MudPaper Elevation="2" Class="pa-6 rounded-lg text-center">
|
||||
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
|
||||
<MudText Class="mt-3">در حال بارگذاری...</MudText>
|
||||
</MudPaper>
|
||||
}
|
||||
else if (_status.WalletMode != 1)
|
||||
{
|
||||
@* ═══ حالت عادی — Magic فعال نیست ═══ *@
|
||||
<MudPaper Elevation="2" Class="pa-6 rounded-lg text-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Lock" Color="Color.Default" Size="Size.Large" Style="font-size: 64px;" />
|
||||
<MudText Typo="Typo.h5" Class="mt-4">کیفپول جادویی فعال نیست</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mt-2 mud-text-secondary">
|
||||
برای فعال شدن کیفپول جادویی، ابتدا پکیج پایه را خریداری کنید و سپس
|
||||
تمام موجودی اصلی خود را از فروشگاه خرج کنید. وقتی موجودی اصلی به صفر برسد،
|
||||
کیفپول جادویی فعال میشود.
|
||||
</MudText>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="mt-4 rounded-pill"
|
||||
Href="@RouteConstants.Profile.Wallet"
|
||||
StartIcon="@Icons.Material.Filled.AccountBalanceWallet">
|
||||
بازگشت به کیف پول
|
||||
</MudButton>
|
||||
</MudPaper>
|
||||
}
|
||||
else
|
||||
{
|
||||
@* ═══ حالت جادویی فعال ═══ *@
|
||||
|
||||
@* ── G4: هشدار تنظیمات پکیجبیس (Q28) ── *@
|
||||
<MudAlert Severity="Severity.Warning" Variant="Variant.Text" Dense="true" Icon="@Icons.Material.Filled.Warning">
|
||||
سقف واریز و ضریب اعتبار کیفپول جادویی بر اساس پکیج شما تنظیم شده است. پکیجهای بالاتر سقف و ضریب بیشتری دارند.
|
||||
</MudAlert>
|
||||
|
||||
@* ─── وضعیت و پیشرفت ─── *@
|
||||
<MudPaper Elevation="2" Class="pa-5 rounded-xl" Style="background: linear-gradient(135deg, #7c3aed 0%, #a855f7 100%); color: white;">
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.AutoAwesome" Size="Size.Large" />
|
||||
<MudText Typo="Typo.h5">حالت جادویی فعال</MudText>
|
||||
</MudStack>
|
||||
|
||||
<MudGrid Spacing="2">
|
||||
<MudItem xs="6" sm="3">
|
||||
<MudText Typo="Typo.caption" Style="opacity:0.8">موجودی فعلی</MudText>
|
||||
<MudText Typo="Typo.h6">@FormatPrice(_status.Balance)</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="6" sm="3">
|
||||
<MudText Typo="Typo.caption" Style="opacity:0.8">مجموع واریزی</MudText>
|
||||
<MudText Typo="Typo.h6">@FormatPrice(_status.MagicTotalDeposited)</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="6" sm="3">
|
||||
<MudText Typo="Typo.caption" Style="opacity:0.8">مجموع اعتبار</MudText>
|
||||
<MudText Typo="Typo.h6">@FormatPrice(_status.MagicTotalCredited)</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="6" sm="3">
|
||||
<MudText Typo="Typo.caption" Style="opacity:0.8">سقف باقیمانده</MudText>
|
||||
<MudText Typo="Typo.h6">@FormatPrice(_status.MagicRemainingDeposit)</MudText>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
@* نوار پیشرفت *@
|
||||
<MudStack Spacing="0" Class="mt-1">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.caption" Style="opacity:0.8">پیشرفت سقف واریز</MudText>
|
||||
<MudText Typo="Typo.caption" Style="opacity:0.8">@(_progressPercent.ToString("0"))%</MudText>
|
||||
</MudStack>
|
||||
<MudProgressLinear Value="@_progressPercent" Color="Color.Warning" Size="Size.Medium"
|
||||
Rounded="true" Class="mt-1" Style="background: rgba(255,255,255,0.3);" />
|
||||
</MudStack>
|
||||
|
||||
@if (_status.MagicActivatedAt.HasValue)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Style="opacity:0.7">
|
||||
فعال از: @_status.MagicActivatedAt.Value.MiladiToJalaliWithTime()
|
||||
</MudText>
|
||||
}
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
@* ─── فرم شارژ ─── *@
|
||||
@if (_status.MagicRemainingDeposit > 0)
|
||||
{
|
||||
<MudPaper Elevation="2" Class="pa-5 rounded-lg">
|
||||
<MudText Typo="Typo.h6" Class="mb-3">
|
||||
<MudIcon Icon="@Icons.Material.Filled.CreditCard" Class="ml-1" />
|
||||
شارژ کیفپول جادویی
|
||||
</MudText>
|
||||
|
||||
<MudAlert Severity="MudBlazor.Severity.Info" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info">
|
||||
مبلغ واریزی شما <strong>×@_status.MagicMultiplier.ToString("0.#")</strong> به موجودی اضافه میشود.
|
||||
مثلاً ۱۰ میلیون واریز = @FormatToman((long)(10_000_000 * (decimal)_status.MagicMultiplier)) اعتبار.
|
||||
</MudAlert>
|
||||
|
||||
<MudStack Spacing="2">
|
||||
<MudNumericField @bind-Value="_chargeAmount"
|
||||
Label="مبلغ واریز (تومان)"
|
||||
Variant="Variant.Outlined"
|
||||
Min="10_000"
|
||||
Max="@_status.MagicRemainingDeposit"
|
||||
Format="N0"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Payments"
|
||||
HelperText="@($"حداکثر: {FormatPrice(_status.MagicRemainingDeposit)} (تومان)")" />
|
||||
|
||||
@if (_chargeAmount > 0)
|
||||
{
|
||||
<MudPaper Outlined="true" Class="pa-3 rounded-lg" Style="background: #f0fdf4;">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2">مبلغ واریز:</MudText>
|
||||
<MudText Typo="Typo.body2">@FormatToman(_chargeAmount)</MudText>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2" Color="Color.Success"><strong>اعتبار دریافتی (×@_status.MagicMultiplier.ToString("0.#")):</strong></MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Success"><strong>@FormatToman((long)(_chargeAmount * (decimal)_status.MagicMultiplier))</strong></MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
@* دکمههای مبلغ سریع *@
|
||||
<MudStack Row="true" Spacing="1" Class="flex-wrap">
|
||||
@foreach (var preset in _presetAmounts)
|
||||
{
|
||||
if (preset <= _status.MagicRemainingDeposit)
|
||||
{
|
||||
<MudButton Variant="Variant.Outlined" Size="Size.Small"
|
||||
Color="@(_chargeAmount == preset ? Color.Primary : Color.Default)"
|
||||
OnClick="() => _chargeAmount = preset"
|
||||
Class="rounded-pill">
|
||||
@FormatToman(preset)
|
||||
</MudButton>
|
||||
}
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Large"
|
||||
FullWidth="true"
|
||||
Disabled="@(_isProcessing || _chargeAmount < 10_000)"
|
||||
OnClick="StartMagicCharge"
|
||||
StartIcon="@Icons.Material.Filled.Payment"
|
||||
Class="rounded-lg mt-2">
|
||||
@if (_isProcessing)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="me-2" />
|
||||
<span>در حال انتقال به درگاه...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>پرداخت با کارت بانکی</span>
|
||||
}
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudAlert Severity="MudBlazor.Severity.Warning" Class="rounded-lg">
|
||||
<MudStack Spacing="2">
|
||||
<MudText>
|
||||
سقف شارژ جادویی در این دور پر شده است.
|
||||
با خرج کردن موجودی و خرید مجدد پکیج، سقف ریست میشود.
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body2">
|
||||
اگر برای تکمیل خرید به موجودی بیشتری نیاز دارید، میتوانید بهصورت استثنایی کیف اصلی را بدون ضریب (۱:۱) شارژ کنید.
|
||||
</MudText>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.AccountBalanceWallet"
|
||||
Href="@RouteConstants.Profile.ChargeCreditWallet"
|
||||
Class="rounded-lg align-self-start">
|
||||
شارژ عادی بدون ضریب
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
@* ─── قوانین ─── *@
|
||||
<MudExpansionPanels Elevation="1" Class="rounded-lg">
|
||||
<MudExpansionPanel Text="قوانین کیفپول جادویی" MaxHeight="500" IsInitiallyExpanded="false">
|
||||
<MudList T="string" Dense="true">
|
||||
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
|
||||
هر مبلغ واریزی ×@_status.MagicMultiplier.ToString("0.#") به موجودی اضافه میشود
|
||||
</MudListItem>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
|
||||
سقف واریز در هر دور: @FormatPrice(_status.MagicMaxDeposit)
|
||||
</MudListItem>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
|
||||
حداکثر اعتبار: @FormatPrice(_status.MagicMaxCredit)
|
||||
</MudListItem>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Filled.Warning" IconColor="Color.Warning">
|
||||
در حالت جادویی، کمیسیون و پاداش های دریافتی غیرفعال است
|
||||
</MudListItem>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Filled.Info" IconColor="Color.Info">
|
||||
بعد از اتمام سقف و خرج موجودی، با خرید مجدد پکیج دور جدید شروع میشود
|
||||
</MudListItem>
|
||||
</MudList>
|
||||
</MudExpansionPanel>
|
||||
</MudExpansionPanels>
|
||||
}
|
||||
|
||||
@* ─── نتیجه پرداخت (اگه از callback برگشته) ─── *@
|
||||
@if (_paymentResult != null)
|
||||
{
|
||||
<MudAlert Severity="@(_paymentResult == "success" ? MudBlazor.Severity.Success : MudBlazor.Severity.Error)"
|
||||
Class="rounded-lg" Variant="Variant.Filled">
|
||||
@if (_paymentResult == "success")
|
||||
{
|
||||
<span>شارژ جادویی با موفقیت انجام شد! ✅</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>پرداخت ناموفق بود. لطفاً دوباره تلاش کنید.</span>
|
||||
}
|
||||
</MudAlert>
|
||||
}
|
||||
</MudStack>
|
||||
</MudContainer>
|
||||
@@ -1,77 +0,0 @@
|
||||
using DateTimeConverterCL;
|
||||
using FrontOffice.Main.Utilities;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace FrontOffice.Main.Pages.Profile;
|
||||
|
||||
public partial class MagicWallet : ComponentBase
|
||||
{
|
||||
private MagicWalletStatus _status = new(0, 0, 0, 0, 0, 0, null, 0);
|
||||
private bool _isLoading = true;
|
||||
private bool _isProcessing;
|
||||
private long _chargeAmount;
|
||||
private double _progressPercent;
|
||||
private string? _paymentResult;
|
||||
|
||||
private readonly long[] _presetAmounts = { 1_000_000, 5_000_000, 10_000_000, 20_000_000, 50_000_000, 100_000_000 };
|
||||
|
||||
[SupplyParameterFromQuery(Name = "payment")]
|
||||
public string? PaymentQueryParam { get; set; }
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_paymentResult = PaymentQueryParam;
|
||||
await LoadStatus();
|
||||
}
|
||||
|
||||
private async Task LoadStatus()
|
||||
{
|
||||
_isLoading = true;
|
||||
_status = await WalletService.GetMagicWalletStatusAsync();
|
||||
|
||||
_progressPercent = _status.MagicMaxDeposit > 0
|
||||
? (double)_status.MagicTotalDeposited / _status.MagicMaxDeposit * 100
|
||||
: 0;
|
||||
|
||||
_isLoading = false;
|
||||
}
|
||||
|
||||
private async Task StartMagicCharge()
|
||||
{
|
||||
if (_chargeAmount <= 0 || _isProcessing) return;
|
||||
|
||||
_isProcessing = true;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
// مبلغ به تومان — CMS خودش موقع ارسال به درگاه ×۱۰ میکنه
|
||||
var (success, gatewayUrl, error) = await WalletService.InitiateMagicChargeAsync(_chargeAmount);
|
||||
|
||||
if (success && !string.IsNullOrEmpty(gatewayUrl))
|
||||
{
|
||||
Navigation.NavigateTo(gatewayUrl, forceLoad: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add(error ?? "خطا در ایجاد درخواست پرداخت", Severity.Error);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isProcessing = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatPrice(long price)
|
||||
=> string.Format("{0:N0} تومان", price);
|
||||
|
||||
private static string FormatToman(long toman)
|
||||
=> string.Format("{0:N0} تومان", toman);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
@using Blazored.LocalStorage
|
||||
@using CMSMicroservice.Protobuf.Protos.Package
|
||||
@using CMSMicroservice.Protobuf.Protos.User
|
||||
@using FrontOffice.Main.Utilities
|
||||
@inject IJSRuntime JS
|
||||
|
||||
|
||||
<PageTitle>نتیجه پرداخت | کارا بازار سلامت</PageTitle>
|
||||
@@ -21,10 +21,10 @@
|
||||
<MudText Typo="Typo.h5" Color="Color.Success" Class="mt-4">پرداخت موفق</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mt-2">@_message</MudText>
|
||||
|
||||
@if (_transactionId > 0)
|
||||
@if (!string.IsNullOrEmpty(_refId))
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mt-3">
|
||||
<strong>کد ارجاعی:</strong> @_transactionId
|
||||
<strong>کد پیگیری:</strong> @_refId
|
||||
</MudText>
|
||||
}
|
||||
|
||||
@@ -33,10 +33,13 @@
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">
|
||||
موجودی کیف پول: @FormatPrice(_walletBalance)
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">
|
||||
موجودی اعتبار تخفیف: @FormatPrice(_discountBalance)
|
||||
</MudText>
|
||||
|
||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" Class="mt-6"
|
||||
Href="@_successReturnUrl">
|
||||
@_successReturnText
|
||||
OnClick="GoBack">
|
||||
بازگشت به پروفایل
|
||||
</MudButton>
|
||||
}
|
||||
else
|
||||
@@ -46,8 +49,8 @@
|
||||
<MudText Typo="Typo.body1" Class="mt-2">@_message</MudText>
|
||||
|
||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" Class="mt-6"
|
||||
Href="@_failReturnUrl">
|
||||
@_failReturnText
|
||||
OnClick="GoBack">
|
||||
بازگشت به پروفایل
|
||||
</MudButton>
|
||||
|
||||
<MudButton Color="Color.Secondary" Variant="Variant.Outlined" Class="mt-3 mr-2"
|
||||
@@ -61,19 +64,12 @@
|
||||
@code {
|
||||
[Inject] private PackageContract.PackageContractClient PackageContractClient { get; set; } = default!;
|
||||
[Inject] private UserContract.UserContractClient UserContractClient { get; set; } = default!;
|
||||
[Inject] private DiscountOrderService DiscountOrderService { get; set; } = default!;
|
||||
[Inject] private ILocalStorageService LocalStorage { get; set; } = default!;
|
||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||
[Inject] private NavigationManager NavManager { get; set; } = default!;
|
||||
|
||||
private const string TokenStorageKey = "auth:token";
|
||||
|
||||
/// <summary>
|
||||
/// نوع پرداخت: package (پیشفرض) | magic-wallet | discount-wallet | discount-order
|
||||
/// </summary>
|
||||
[SupplyParameterFromQuery(Name = "type")]
|
||||
private string? PaymentType { get; set; }
|
||||
|
||||
[SupplyParameterFromQuery(Name = "orderId")]
|
||||
private long OrderId { get; set; }
|
||||
|
||||
@@ -89,42 +85,15 @@
|
||||
private bool _isLoading = true;
|
||||
private bool _isSuccess;
|
||||
private string _message = string.Empty;
|
||||
private long _transactionId;
|
||||
private string? _refId;
|
||||
private long _walletBalance;
|
||||
private bool _verifyCompleted;
|
||||
private readonly SemaphoreSlim _verifyGate = new(1, 1);
|
||||
|
||||
// دکمههای بازگشت بسته به نوع پرداخت
|
||||
private string _successReturnUrl = "/profile";
|
||||
private string _successReturnText = "بازگشت به پروفایل";
|
||||
private string _failReturnUrl = "/profile";
|
||||
private string _failReturnText = "بازگشت به پروفایل";
|
||||
private long _discountBalance;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender && !_verifyCompleted)
|
||||
if (firstRender)
|
||||
{
|
||||
await VerifyPaymentOnceAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Blazor Server may invoke render/verify twice in quick succession — gate with semaphore.
|
||||
/// </summary>
|
||||
private async Task VerifyPaymentOnceAsync()
|
||||
{
|
||||
if (_verifyCompleted) return;
|
||||
|
||||
await _verifyGate.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (_verifyCompleted) return;
|
||||
await VerifyPayment();
|
||||
_verifyCompleted = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_verifyGate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,26 +101,35 @@
|
||||
{
|
||||
try
|
||||
{
|
||||
// تعیین نوع پرداخت — اگر type نباشد، پیشفرض خرید پکیج
|
||||
var type = PaymentType?.ToLowerInvariant() ?? "package";
|
||||
|
||||
switch (type)
|
||||
if (OrderId <= 0 || TransactionId <= 0 || string.IsNullOrEmpty(Authority))
|
||||
{
|
||||
case "magic-wallet":
|
||||
await VerifyMagicWalletCharge();
|
||||
break;
|
||||
case "discount-wallet":
|
||||
await VerifyDiscountWalletCharge();
|
||||
break;
|
||||
case "credit-wallet":
|
||||
await VerifyCreditWalletCharge();
|
||||
break;
|
||||
case "discount-order":
|
||||
await VerifyDiscountOrderPayment();
|
||||
break;
|
||||
default: // "package" or missing
|
||||
await VerifyPackagePurchase();
|
||||
break;
|
||||
_isSuccess = false;
|
||||
_message = "پارامترهای پرداخت نامعتبر است";
|
||||
_isLoading = false;
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
var response = await PackageContractClient.VerifyBasePackagePaymentAsync(new VerifyBasePackagePaymentRequest
|
||||
{
|
||||
OrderId = OrderId,
|
||||
TransactionId = TransactionId,
|
||||
PaymentSuccess = Status == "OK",
|
||||
RefId = !string.IsNullOrEmpty(Authority) ? Authority : null,
|
||||
Message = !string.IsNullOrEmpty(Status) ? Status : null
|
||||
});
|
||||
|
||||
_isSuccess = response.Success;
|
||||
_message = response.Message;
|
||||
_refId = response.ReferenceCode ?? "";
|
||||
_walletBalance = response.WalletBalance;
|
||||
_discountBalance = response.DiscountBalance;
|
||||
|
||||
// اگر پرداخت موفق بود، توکن را refresh میکنیم
|
||||
// چون PackagePurchaseMethod تغییر کرده و باید در claims جدید باشد
|
||||
if (_isSuccess)
|
||||
{
|
||||
await RefreshTokenAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -165,186 +143,10 @@
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// تأیید خرید پکیج — رفتار فعلی
|
||||
/// </summary>
|
||||
private async Task VerifyPackagePurchase()
|
||||
{
|
||||
_successReturnUrl = "/profile";
|
||||
_successReturnText = "بازگشت به پروفایل";
|
||||
_failReturnUrl = "/profile";
|
||||
_failReturnText = "بازگشت به پروفایل";
|
||||
|
||||
if (OrderId <= 0 || string.IsNullOrEmpty(Authority))
|
||||
{
|
||||
_isSuccess = false;
|
||||
_message = "پارامترهای پرداخت نامعتبر است";
|
||||
return;
|
||||
}
|
||||
|
||||
var response = await PackageContractClient.CustomerVerifyPackagePurchaseAsync(new CustomerVerifyPackagePurchaseRequest
|
||||
{
|
||||
OrderId = OrderId,
|
||||
Authority = Authority ?? string.Empty,
|
||||
Status = Status ?? string.Empty
|
||||
});
|
||||
|
||||
_isSuccess = response.Success;
|
||||
_message = response.Message;
|
||||
_transactionId = response.TransactionId;
|
||||
|
||||
if (_isSuccess)
|
||||
{
|
||||
await UpdateWalletBalanceAndRefreshToken();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// تأیید شارژ کیفپول جادویی
|
||||
/// </summary>
|
||||
private async Task VerifyMagicWalletCharge()
|
||||
{
|
||||
_successReturnUrl = "/profile/magic-wallet";
|
||||
_successReturnText = "بازگشت به کیف پول جادویی";
|
||||
_failReturnUrl = "/profile/magic-wallet";
|
||||
_failReturnText = "بازگشت به کیف پول جادویی";
|
||||
|
||||
if (string.IsNullOrEmpty(Authority))
|
||||
{
|
||||
_isSuccess = false;
|
||||
_message = "کد Authority نامعتبر است";
|
||||
return;
|
||||
}
|
||||
|
||||
var (success, message) = await WalletService.VerifyMagicChargeAsync(
|
||||
Authority, Status ?? "NOK");
|
||||
|
||||
_isSuccess = success;
|
||||
_message = message;
|
||||
|
||||
if (_isSuccess)
|
||||
{
|
||||
await UpdateWalletBalance();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// تأیید شارژ کیف پول تخفیفی
|
||||
/// </summary>
|
||||
private async Task VerifyDiscountWalletCharge()
|
||||
{
|
||||
_successReturnUrl = "/profile/charge-discount-wallet";
|
||||
_successReturnText = "بازگشت به کیف پول تخفیفی";
|
||||
_failReturnUrl = "/profile/charge-discount-wallet";
|
||||
_failReturnText = "بازگشت به کیف پول تخفیفی";
|
||||
|
||||
if (string.IsNullOrEmpty(Authority))
|
||||
{
|
||||
_isSuccess = false;
|
||||
_message = "کد Authority نامعتبر است";
|
||||
return;
|
||||
}
|
||||
|
||||
var (success, message) = await WalletService.VerifyDiscountChargeAsync(
|
||||
Authority, Status ?? "NOK");
|
||||
|
||||
_isSuccess = success;
|
||||
_message = message;
|
||||
|
||||
if (_isSuccess)
|
||||
{
|
||||
await UpdateWalletBalance();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// تأیید شارژ کیف پول اصلی
|
||||
/// </summary>
|
||||
private async Task VerifyCreditWalletCharge()
|
||||
{
|
||||
_successReturnUrl = "/profile/charge-credit-wallet?payment=success";
|
||||
_successReturnText = "بازگشت به شارژ کیف پول اصلی";
|
||||
_failReturnUrl = "/profile/charge-credit-wallet?payment=failed";
|
||||
_failReturnText = "بازگشت به شارژ کیف پول اصلی";
|
||||
|
||||
if (string.IsNullOrEmpty(Authority))
|
||||
{
|
||||
_isSuccess = false;
|
||||
_message = "کد Authority نامعتبر است";
|
||||
return;
|
||||
}
|
||||
|
||||
var (success, message) = await WalletService.VerifyCreditChargeAsync(
|
||||
Authority, Status ?? "NOK");
|
||||
|
||||
_isSuccess = success;
|
||||
_message = message;
|
||||
|
||||
if (_isSuccess)
|
||||
{
|
||||
await UpdateWalletBalance();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// تأیید پرداخت سفارش فروشگاه تخفیفی
|
||||
/// </summary>
|
||||
private async Task VerifyDiscountOrderPayment()
|
||||
{
|
||||
_successReturnUrl = $"/discount-store/order/{OrderId}";
|
||||
_successReturnText = "مشاهده سفارش";
|
||||
_failReturnUrl = $"/discount-store/order/{OrderId}";
|
||||
_failReturnText = "مشاهده سفارش";
|
||||
|
||||
if (OrderId <= 0 || string.IsNullOrEmpty(Authority))
|
||||
{
|
||||
_isSuccess = false;
|
||||
_message = "پارامترهای پرداخت نامعتبر است";
|
||||
return;
|
||||
}
|
||||
|
||||
var (success, message, _) = await DiscountOrderService.VerifyDiscountOrderPaymentAsync(
|
||||
OrderId, Authority, Status ?? "NOK");
|
||||
|
||||
_isSuccess = success;
|
||||
_message = message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// بروزرسانی موجودی کیف پول
|
||||
/// </summary>
|
||||
private async Task UpdateWalletBalance()
|
||||
{
|
||||
try
|
||||
{
|
||||
var balances = await WalletService.GetBalancesAsync();
|
||||
_walletBalance = balances.CreditBalance;
|
||||
}
|
||||
catch { /* اگر خطا شد، صفر نمایش بده */ }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// بروزرسانی موجودی + refresh توکن (فقط برای خرید پکیج)
|
||||
/// </summary>
|
||||
private async Task UpdateWalletBalanceAndRefreshToken()
|
||||
{
|
||||
await UpdateWalletBalance();
|
||||
await RefreshTokenAsync();
|
||||
}
|
||||
|
||||
private void RetryPayment()
|
||||
{
|
||||
var type = PaymentType?.ToLowerInvariant() ?? "package";
|
||||
var url = type switch
|
||||
{
|
||||
"magic-wallet" => "/profile/magic-wallet",
|
||||
"discount-wallet" => "/profile/charge-discount-wallet",
|
||||
"credit-wallet" => "/profile/charge-credit-wallet",
|
||||
"discount-order" => "/discount-store",
|
||||
_ => "/profile"
|
||||
};
|
||||
NavManager.NavigateTo(url, forceLoad: true);
|
||||
NavManager.NavigateTo("/profile");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -372,4 +174,6 @@
|
||||
}
|
||||
|
||||
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
|
||||
|
||||
private async Task GoBack() => await JS.InvokeVoidAsync("history.back");
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
@attribute [Route(RouteConstants.Profile.Tree)]
|
||||
@using FrontOffice.Main.Pages.Profile.Components
|
||||
|
||||
<PageTitle>سازمان فروش</PageTitle>
|
||||
<PageTitle>شجرهنامه</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
|
||||
<MudStack Spacing="3">
|
||||
<PageHeader Title="سازمان فروش" BackHref="@RouteConstants.Profile.Index" />
|
||||
<PageHeader Title="شجرهنامه" BackHref="@RouteConstants.Profile.Index" />
|
||||
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
||||
<OrganizationChart />
|
||||
|
||||
@@ -9,19 +9,19 @@
|
||||
<MudGrid Spacing="2">
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
||||
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">موجودی اصلی</MudText>
|
||||
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">موجودی عادی</MudText>
|
||||
<MudText Typo="Typo.h4" Color="Color.Primary">@_balances.Credit</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
||||
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">پاداش های دریافتی</MudText>
|
||||
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">موجودی شبکه</MudText>
|
||||
<MudText Typo="Typo.h4" Color="Color.Success">@_balances.Network</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
||||
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">موجودی اعتباری</MudText>
|
||||
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">موجودی تخفیفی</MudText>
|
||||
<MudText Typo="Typo.h4" Color="Color.Warning">@_balances.Discount</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
@@ -38,104 +38,6 @@
|
||||
درخواستهای برداشت
|
||||
</MudButton>
|
||||
|
||||
<!-- دکمه شارژ کیفپول اصلی / جادویی -->
|
||||
@if (_isClubMemberActive)
|
||||
{
|
||||
@if (_isMagicWallet)
|
||||
{
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Secondary"
|
||||
Size="Size.Large"
|
||||
FullWidth="true"
|
||||
StartIcon="@Icons.Material.Filled.AutoAwesome"
|
||||
Href="@RouteConstants.Profile.MagicWallet"
|
||||
Class="rounded-lg">
|
||||
شارژ کیفپول جادویی
|
||||
</MudButton>
|
||||
@if (_magicCeilingFull)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Dense="true" Variant="Variant.Outlined">
|
||||
سقف شارژ جادویی این دور پر است. در صورت نیاز میتوانید بهصورت استثنایی بدون ضریب شارژ کنید.
|
||||
</MudAlert>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Large"
|
||||
FullWidth="true"
|
||||
StartIcon="@Icons.Material.Filled.AccountBalanceWallet"
|
||||
Href="@RouteConstants.Profile.ChargeCreditWallet"
|
||||
Class="rounded-lg">
|
||||
شارژ عادی بدون ضریب
|
||||
</MudButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined">
|
||||
کیف پول شما در حالت جادویی است؛ شارژ از مسیر جادویی با ضریب چندبرابر اعمال میشود.
|
||||
</MudAlert>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Large"
|
||||
FullWidth="true"
|
||||
StartIcon="@Icons.Material.Filled.AccountBalanceWallet"
|
||||
Href="@RouteConstants.Profile.ChargeCreditWallet"
|
||||
Class="rounded-lg">
|
||||
شارژ کیفپول اصلی
|
||||
</MudButton>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper Elevation="0" Class="pa-3 rounded-lg" Outlined="true">
|
||||
<MudStack Spacing="2">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Large"
|
||||
FullWidth="true"
|
||||
StartIcon="@Icons.Material.Filled.AccountBalanceWallet"
|
||||
Disabled="true"
|
||||
Class="rounded-lg">
|
||||
شارژ کیفپول اصلی
|
||||
</MudButton>
|
||||
<MudAlert Severity="Severity.Warning" Dense="true" Variant="Variant.Outlined">
|
||||
برای شارژ کیف پول اصلی ابتدا باید پکیج را خریداری کرده و قرارداد باشگاه مشتریان را امضا کنید.
|
||||
</MudAlert>
|
||||
<MudStack Row="true" Spacing="1" Class="flex-wrap">
|
||||
@if (!_hasPurchasedPackage)
|
||||
{
|
||||
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
|
||||
Href="@RouteConstants.Package.List"
|
||||
StartIcon="@Icons.Material.Filled.CardGiftcard">
|
||||
مشاهده پکیجها
|
||||
</MudButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
|
||||
Href="@RouteConstants.Club.Membership"
|
||||
StartIcon="@Icons.Material.Filled.Handshake">
|
||||
امضای قرارداد باشگاه
|
||||
</MudButton>
|
||||
}
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
<!-- دکمه شارژ کیفپول اعتباری -->
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Info"
|
||||
Size="Size.Large"
|
||||
FullWidth="true"
|
||||
StartIcon="@Icons.Material.Filled.CreditCard"
|
||||
Href="@RouteConstants.Profile.ChargeDiscountWallet"
|
||||
Class="rounded-lg">
|
||||
شارژ کیفپول اعتباری
|
||||
</MudButton>
|
||||
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
||||
<MudText Typo="Typo.h6" Class="mb-2">تراکنشها و مسیرهای شارژ</MudText>
|
||||
|
||||
@@ -169,9 +71,9 @@
|
||||
<MudTable Items="_txs" Dense="true" Hover="true" Striped="true">
|
||||
<HeaderContent>
|
||||
<MudTh>تاریخ</MudTh>
|
||||
<MudTh>اصلی (تغییرات / مانده)</MudTh>
|
||||
<MudTh>پاداش های دریافتی (تغییرات / مانده)</MudTh>
|
||||
<MudTh>اعتباری (تغییرات / مانده)</MudTh>
|
||||
<MudTh>عادی (تغییرات / مانده)</MudTh>
|
||||
<MudTh>شبکه (تغییرات / مانده)</MudTh>
|
||||
<MudTh>تخفیفی (تغییرات / مانده)</MudTh>
|
||||
<MudTh>شناسه ارجاع</MudTh>
|
||||
<MudTh>توضیحات</MudTh>
|
||||
</HeaderContent>
|
||||
@@ -179,7 +81,7 @@
|
||||
<MudTd DataLabel="تاریخ">
|
||||
<MudText Typo="Typo.caption">@context.Date</MudText>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="اصلی">
|
||||
<MudTd DataLabel="عادی">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Color="@(context.CreditChange > 0 ? Color.Success : context.CreditChange < 0 ? Color.Error : Color.Default)" Typo="Typo.body2">
|
||||
@(context.CreditChange != 0 ? (context.CreditChange > 0 ? "+" : "") + FormatPrice(context.CreditChange) : "-")
|
||||
@@ -187,7 +89,7 @@
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">@FormatPrice(context.CreditBalance)</MudText>
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="پاداش های دریافتی">
|
||||
<MudTd DataLabel="شبکه">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Color="@(context.NetworkChange > 0 ? Color.Success : context.NetworkChange < 0 ? Color.Error : Color.Default)" Typo="Typo.body2">
|
||||
@(context.NetworkChange != 0 ? (context.NetworkChange > 0 ? "+" : "") + FormatPrice(context.NetworkChange) : "-")
|
||||
@@ -195,7 +97,7 @@
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">@FormatPrice(context.NetworkBalance)</MudText>
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="اعتباری">
|
||||
<MudTd DataLabel="تخفیفی">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Color="@(context.DiscountChange > 0 ? Color.Success : context.DiscountChange < 0 ? Color.Error : Color.Default)" Typo="Typo.body2">
|
||||
@(context.DiscountChange != 0 ? (context.DiscountChange > 0 ? "+" : "") + FormatPrice(context.DiscountChange) : "-")
|
||||
@@ -223,10 +125,10 @@
|
||||
<MudDivider />
|
||||
|
||||
<MudStack Row="true" Spacing="2" Justify="Justify.SpaceBetween">
|
||||
<!-- کیف پول اصلی -->
|
||||
<!-- کیف پول عادی -->
|
||||
<MudPaper Class="pa-2 flex-grow-1" Outlined="true">
|
||||
<MudStack Spacing="1" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.caption" Color="Color.Primary">اصلی</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Primary">عادی</MudText>
|
||||
<MudText Color="@(tx.CreditChange > 0 ? Color.Success : tx.CreditChange < 0 ? Color.Error : Color.Default)" Style="font-weight: 600; font-size: 0.75rem;">
|
||||
@(tx.CreditChange != 0 ? (tx.CreditChange > 0 ? "+" : "") + FormatPrice(tx.CreditChange) : "-")
|
||||
</MudText>
|
||||
@@ -234,10 +136,10 @@
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<!-- پاداش های دریافتی -->
|
||||
<!-- کیف پول شبکه -->
|
||||
<MudPaper Class="pa-2 flex-grow-1" Outlined="true">
|
||||
<MudStack Spacing="1" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.caption" Color="Color.Success">پاداش های دریافتی</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Success">شبکه</MudText>
|
||||
<MudText Color="@(tx.NetworkChange > 0 ? Color.Success : tx.NetworkChange < 0 ? Color.Error : Color.Default)" Style="font-weight: 600; font-size: 0.75rem;">
|
||||
@(tx.NetworkChange != 0 ? (tx.NetworkChange > 0 ? "+" : "") + FormatPrice(tx.NetworkChange) : "-")
|
||||
</MudText>
|
||||
@@ -245,10 +147,10 @@
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<!-- کیف پول اعتباری -->
|
||||
<!-- کیف پول تخفیفی -->
|
||||
<MudPaper Class="pa-2 flex-grow-1" Outlined="true">
|
||||
<MudStack Spacing="1" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.caption" Color="Color.Warning">اعتباری</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Warning">تخفیفی</MudText>
|
||||
<MudText Color="@(tx.DiscountChange > 0 ? Color.Success : tx.DiscountChange < 0 ? Color.Error : Color.Default)" Style="font-weight: 600; font-size: 0.75rem;">
|
||||
@(tx.DiscountChange != 0 ? (tx.DiscountChange > 0 ? "+" : "") + FormatPrice(tx.DiscountChange) : "-")
|
||||
</MudText>
|
||||
|
||||
@@ -6,43 +6,13 @@ namespace FrontOffice.Main.Pages.Profile;
|
||||
|
||||
public partial class Wallet : ComponentBase
|
||||
{
|
||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||
|
||||
private (string Credit, string Discount, string Network) _balances = ("-", "-", "-");
|
||||
private List<WalletTransaction> _txs = new();
|
||||
private string? _filterReferenceId;
|
||||
private string _filterType = "all";
|
||||
private bool _isClubMemberActive;
|
||||
private bool _hasPurchasedPackage;
|
||||
private bool _isMagicWallet;
|
||||
private bool _magicCeilingFull;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var userInfo = await AuthService.GetUserAuthInfo();
|
||||
_isClubMemberActive = userInfo.IsClubMemberActive;
|
||||
_hasPurchasedPackage = userInfo.HasPurchasedPackage;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_isClubMemberActive = false;
|
||||
_hasPurchasedPackage = false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var magicStatus = await WalletService.GetMagicWalletStatusAsync();
|
||||
_isMagicWallet = magicStatus.WalletMode == 1;
|
||||
_magicCeilingFull = _isMagicWallet && magicStatus.MagicRemainingDeposit <= 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_isMagicWallet = false;
|
||||
_magicCeilingFull = false;
|
||||
}
|
||||
|
||||
var b = await WalletService.GetBalancesAsync();
|
||||
_balances = (FormatPrice(b.CreditBalance), FormatPrice(b.DiscountBalance), FormatPrice(b.NetworkBalance));
|
||||
_txs = await WalletService.GetTransactionsAsync();
|
||||
|
||||
@@ -58,19 +58,8 @@ public partial class WithdrawalRequests : ComponentBase
|
||||
try
|
||||
{
|
||||
_isSubmittingWithdrawal = true;
|
||||
|
||||
string? normalizedIban = null;
|
||||
if (_withdrawMethod == WithdrawalMethodClient.Cash)
|
||||
{
|
||||
normalizedIban = NormalizeIranianIban(_withdrawIban);
|
||||
if (normalizedIban is null)
|
||||
{
|
||||
Snackbar.Add("فرمت شماره شبا معتبر نیست. باید IR و ۲۴ رقم باشد (خط تیره اختیاری است).", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await WalletService.RequestWithdrawalAsync(_selectedPayout.Id, _withdrawMethod, normalizedIban);
|
||||
_withdrawIban=_withdrawIban!.Trim().ToUpper().Replace("IR", "").Replace(" ", "");
|
||||
await WalletService.RequestWithdrawalAsync(_selectedPayout.Id, _withdrawMethod, "IR"+_withdrawIban);
|
||||
Snackbar.Add("درخواست برداشت ثبت شد.", Severity.Success);
|
||||
|
||||
// بروزرسانی لیست
|
||||
@@ -136,26 +125,4 @@ public partial class WithdrawalRequests : ComponentBase
|
||||
1 => "الماس",
|
||||
_ => "-"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// فاصله و خط تیره را حذف میکند و شبا را به شکل IR + ۲۴ رقم برمیگرداند.
|
||||
/// </summary>
|
||||
private static string? NormalizeIranianIban(string? iban)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(iban))
|
||||
return null;
|
||||
|
||||
var normalized = iban.Trim().ToUpperInvariant()
|
||||
.Replace(" ", "", StringComparison.Ordinal)
|
||||
.Replace("-", "", StringComparison.Ordinal);
|
||||
|
||||
if (normalized.StartsWith("IR", StringComparison.Ordinal))
|
||||
normalized = normalized[2..];
|
||||
|
||||
normalized = "IR" + normalized;
|
||||
|
||||
return System.Text.RegularExpressions.Regex.IsMatch(normalized, @"^IR\d{24}$")
|
||||
? normalized
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<MudStack Spacing="1">
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
||||
<AppImage Path="@GetProductImageUrl(context.ImageUrl)" Alt="@context.Title"
|
||||
ImgWidth="64" ImgHeight="64" Class="rounded-lg" ObjectFit="ObjectFit.Cover" />
|
||||
ImgWidth="64" ImgHeight="64" Class="product-thumb" />
|
||||
<MudText>@context.Title</MudText>
|
||||
</MudStack>
|
||||
@if (context.Discount > 0 || !string.IsNullOrWhiteSpace(context.Created) || !string.IsNullOrWhiteSpace(context.Description))
|
||||
@@ -87,7 +87,7 @@
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
||||
<AppImage Path="@GetProductImageUrl(item.ImageUrl)" Alt="@item.Title"
|
||||
ImgWidth="50" ImgHeight="50" Class="rounded-lg" ObjectFit="ObjectFit.Cover" />
|
||||
ImgWidth="50" ImgHeight="50" Class="rounded-circle" />
|
||||
<MudText Typo="Typo.subtitle2">@item.Title</MudText>
|
||||
</MudStack>
|
||||
@if (item.Discount > 0)
|
||||
|
||||
@@ -8,17 +8,11 @@ public partial class Cart : ComponentBase, IDisposable
|
||||
{
|
||||
[Inject] private CartService CartService { get; set; } = default!;
|
||||
[Inject] private VATService VAT { get; set; } = default!;
|
||||
[Inject] private AuthDialogService AuthDialogService { get; set; } = default!;
|
||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||
// Navigation and Snackbar are available via _Imports.razor
|
||||
private CartService CartData => CartService;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (!await AuthService.IsAuthenticatedAsync())
|
||||
{
|
||||
await AuthDialogService.ShowAuthDialogAsync();
|
||||
}
|
||||
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
|
||||
await CartService.EnsureInitializedAsync();
|
||||
CartService.OnChange += StateHasChanged;
|
||||
|
||||
@@ -21,11 +21,9 @@
|
||||
else if (_addresses.Count == 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning">
|
||||
هیچ آدرسی ثبت نشده است. میتوانید همینجا آدرس جدید اضافه کنید.
|
||||
هیچ آدرسی ثبت نشده است. لطفاً از بخش پروفایل آدرس خود را اضافه کنید.
|
||||
</MudAlert>
|
||||
<MudButton Class="mt-2" Variant="Variant.Outlined" Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="OpenAddAddressDialog">افزودن آدرس</MudButton>
|
||||
<MudButton Class="mt-2" Variant="Variant.Outlined" Href="/profile/addresses">افزودن آدرس</MudButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -42,25 +40,13 @@
|
||||
<MudText Typo="Typo.subtitle2">@address.Title</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">@address.Address</MudText>
|
||||
</MudStack>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
@if (address.IsDefault)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Success" Variant="Variant.Outlined" Size="Size.Small">پیشفرض</MudChip>
|
||||
}
|
||||
<span @onclick:stopPropagation="true">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Size="Size.Small"
|
||||
Color="Color.Primary"
|
||||
aria-label="ویرایش آدرس"
|
||||
OnClick="@(() => OpenEditAddressDialog(address))" />
|
||||
</span>
|
||||
</MudStack>
|
||||
@if (address.IsDefault)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Success" Variant="Variant.Outlined">پیشفرض</MudChip>
|
||||
}
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
<MudButton Class="mt-2" Variant="Variant.Text" Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="OpenAddAddressDialog">افزودن آدرس جدید</MudButton>
|
||||
</MudStack>
|
||||
}
|
||||
</MudPaper>
|
||||
@@ -100,7 +86,7 @@
|
||||
<MudStack Spacing="1">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<AppImage Path="@GetProductImageUrl(item.ImageUrl)" Alt="@item.Title"
|
||||
ImgWidth="48" ImgHeight="48" Class="rounded-lg" ObjectFit="ObjectFit.Cover" />
|
||||
ImgWidth="48" ImgHeight="48" Class="rounded-circle" />
|
||||
<MudText Typo="Typo.subtitle2">@item.Title</MudText>
|
||||
<MudText Typo="Typo.subtitle2">@FormatPrice(item.LineTotal)</MudText>
|
||||
</MudStack>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
||||
using CMSMicroservice.Protobuf.Protos.UserOrder;
|
||||
using FrontOffice.Main.Pages.Profile.Components;
|
||||
using FrontOffice.Main.Shared;
|
||||
using FrontOffice.Main.Utilities;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
@@ -16,8 +14,6 @@ public partial class CheckoutSummary : ComponentBase
|
||||
[Inject] private VATService VAT { get; set; } = default!;
|
||||
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
||||
[Inject] private UserOrderContract.UserOrderContractClient UserOrderContract { get; set; } = default!;
|
||||
[Inject] private AuthDialogService AuthDialogService { get; set; } = default!;
|
||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||
// Snackbar and Navigation are injected via _Imports.razor
|
||||
|
||||
private List<CustomerAddressModel> _addresses = new();
|
||||
@@ -31,16 +27,9 @@ public partial class CheckoutSummary : ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (!await AuthService.IsAuthenticatedAsync())
|
||||
{
|
||||
await AuthDialogService.ShowAuthDialogAsync();
|
||||
}
|
||||
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
|
||||
await Cart.EnsureInitializedAsync();
|
||||
var userInfo = await AuthService.GetUserAuthInfo();
|
||||
if (userInfo.HasAddress)
|
||||
await LoadAddresses();
|
||||
else
|
||||
_addresses = new();
|
||||
await LoadAddresses();
|
||||
await LoadWalletBalance();
|
||||
}
|
||||
|
||||
@@ -48,6 +37,7 @@ public partial class CheckoutSummary : ComponentBase
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
// بارگذاری نرخ VAT
|
||||
await VAT.LoadAsync();
|
||||
}
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
@@ -56,7 +46,9 @@ public partial class CheckoutSummary : ComponentBase
|
||||
private async Task LoadWalletBalance()
|
||||
{
|
||||
var walletResult = await WalletService.GetBalancesAsync();
|
||||
walletBalance = walletResult.CreditBalance;
|
||||
walletBalance = walletResult.CreditBalance
|
||||
// + walletResult.NetworkBalance
|
||||
;
|
||||
}
|
||||
|
||||
private async Task LoadAddresses()
|
||||
@@ -73,14 +65,12 @@ public partial class CheckoutSummary : ComponentBase
|
||||
else
|
||||
{
|
||||
_addresses = new();
|
||||
_selectedAddress = null;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری آدرسها: {ex.Message}", Severity.Error);
|
||||
_addresses = new();
|
||||
_selectedAddress = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -89,30 +79,6 @@ public partial class CheckoutSummary : ComponentBase
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenAddAddressDialog()
|
||||
{
|
||||
var dialog = await DialogService.ShowAsync<AddAddressDialog>("افزودن آدرس جدید");
|
||||
var result = await dialog.Result;
|
||||
if (result is { Canceled: false })
|
||||
{
|
||||
await AuthService.RefreshTokenAsync();
|
||||
await LoadAddresses();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenEditAddressDialog(CustomerAddressModel address)
|
||||
{
|
||||
var dialog = await DialogService.ShowAsync<EditAddressDialog>("ویرایش آدرس", new DialogParameters<EditAddressDialog>
|
||||
{
|
||||
{ x => x.Model, address }
|
||||
});
|
||||
var result = await dialog.Result;
|
||||
if (result is { Canceled: false })
|
||||
{
|
||||
await LoadAddresses();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PlaceOrder()
|
||||
{
|
||||
if (!CanPlaceOrder || _selectedAddress is null)
|
||||
@@ -121,15 +87,11 @@ public partial class CheckoutSummary : ComponentBase
|
||||
return;
|
||||
}
|
||||
|
||||
var totalRequired = VAT.AddVAT(Cart.Total);
|
||||
if (await TryHandleInsufficientBalanceAsync(totalRequired))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var request = new SubmitShopBuyOrderRequest
|
||||
{
|
||||
TotalAmount = totalRequired
|
||||
TotalAmount = VAT.AddVAT(Cart.Total)
|
||||
};
|
||||
|
||||
var response = await UserOrderContract.SubmitShopBuyOrderAsync(request);
|
||||
@@ -139,87 +101,17 @@ public partial class CheckoutSummary : ComponentBase
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (CreditChargeNavigation.IsInsufficientWalletBalance(ex))
|
||||
{
|
||||
await LoadWalletBalance();
|
||||
await TryHandleInsufficientBalanceAsync(totalRequired);
|
||||
return;
|
||||
}
|
||||
|
||||
Snackbar.Add($"خطا در ثبت سفارش: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> TryHandleInsufficientBalanceAsync(long totalRequired)
|
||||
{
|
||||
if (walletBalance >= totalRequired)
|
||||
return false;
|
||||
|
||||
var shortfall = totalRequired - walletBalance;
|
||||
var allowCharge = false;
|
||||
var hasPurchasedPackage = false;
|
||||
var isMagicWallet = false;
|
||||
var magicCeilingFull = false;
|
||||
try
|
||||
{
|
||||
var userInfo = await AuthService.GetUserAuthInfo();
|
||||
allowCharge = userInfo.IsClubMemberActive;
|
||||
hasPurchasedPackage = userInfo.HasPurchasedPackage;
|
||||
}
|
||||
catch
|
||||
{
|
||||
allowCharge = false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var magicStatus = await WalletService.GetMagicWalletStatusAsync();
|
||||
isMagicWallet = magicStatus.WalletMode == 1;
|
||||
magicCeilingFull = isMagicWallet && magicStatus.MagicRemainingDeposit <= 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
isMagicWallet = false;
|
||||
magicCeilingFull = false;
|
||||
}
|
||||
|
||||
var parameters = new DialogParameters<InsufficientCreditDialog>
|
||||
{
|
||||
{ x => x.CurrentBalance, walletBalance },
|
||||
{ x => x.RequiredAmount, totalRequired },
|
||||
{ x => x.ShortfallAmount, shortfall },
|
||||
{ x => x.AllowChargeCredit, allowCharge },
|
||||
{ x => x.HasPurchasedPackage, hasPurchasedPackage },
|
||||
{ x => x.IsMagicWallet, isMagicWallet },
|
||||
{ x => x.MagicCeilingFull, magicCeilingFull }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<InsufficientCreditDialog>(
|
||||
"موجودی کافی نیست",
|
||||
parameters,
|
||||
new DialogOptions
|
||||
{
|
||||
CloseOnEscapeKey = true,
|
||||
MaxWidth = MaxWidth.Small,
|
||||
FullWidth = true
|
||||
});
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (allowCharge && result is { Canceled: false } && result.Data is long chargeShortfall)
|
||||
{
|
||||
var chargeAmount = CreditChargeNavigation.NormalizeChargeAmount(chargeShortfall);
|
||||
Navigation.NavigateTo(CreditChargeNavigation.BuildChargeUrl(
|
||||
chargeAmount,
|
||||
RouteConstants.Store.CheckoutSummary));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
|
||||
|
||||
/// <summary>
|
||||
/// محاسبه مالیات بر ارزش افزوده
|
||||
/// </summary>
|
||||
private long CalculateVAT() => VAT.CalculateVAT(Cart.Total);
|
||||
|
||||
private static string GetProductImageUrl(string? imageUrl)
|
||||
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/');
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ else
|
||||
<MudTd>
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
||||
<AppImage Path="@GetProductImageUrl(context.ProductThumbnailPath)" Alt="@context.ProductTitle"
|
||||
ImgWidth="60" ImgHeight="60" Class="rounded-lg" ObjectFit="ObjectFit.Cover" />
|
||||
ImgWidth="60" ImgHeight="60" Class="rounded-circle" />
|
||||
<MudText>@context.ProductTitle</MudText>
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
@@ -66,7 +66,7 @@ else
|
||||
<MudStack Spacing="1">
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
||||
<AppImage Path="@GetProductImageUrl(it.ProductThumbnailPath)" Alt="@it.ProductTitle"
|
||||
ImgWidth="60" ImgHeight="60" Class="rounded-lg" ObjectFit="ObjectFit.Cover" />
|
||||
ImgWidth="60" ImgHeight="60" Class="rounded-circle" />
|
||||
<MudText>@it.ProductTitle</MudText>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
|
||||
@@ -47,3 +47,4 @@ public partial class OrderDetail : ComponentBase
|
||||
private static string GetProductImageUrl(string? imageUrl)
|
||||
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/');
|
||||
}
|
||||
|
||||
|
||||
@@ -46,19 +46,12 @@ public partial class OrderTracking : ComponentBase
|
||||
private void BuildTrackingSteps()
|
||||
{
|
||||
if (_order is null) return;
|
||||
|
||||
|
||||
// Build tracking steps based on PaymentStatus
|
||||
var isPaid = _order.PaymentStatus == Messages.PaymentStatus.Success;
|
||||
// Domain/public_messages: None=0, Pending=1, InTransit=2, Delivered=3, Returned=4, Cancelled=5, ReadyForOfficePickup=6
|
||||
var delivery = (int)_order.DeliveryStatus;
|
||||
var isCancelled = delivery == 5;
|
||||
var isReturned = delivery == 4;
|
||||
var isOfficePickup = delivery == 6;
|
||||
var isInTransit = delivery == 2;
|
||||
var isDelivered = delivery == 3;
|
||||
var isPendingDelivery = delivery is 0 or 1;
|
||||
|
||||
_trackingSteps =
|
||||
[
|
||||
|
||||
_trackingSteps = new List<TrackingStep>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Title = "ثبت سفارش",
|
||||
@@ -68,77 +61,45 @@ public partial class OrderTracking : ComponentBase
|
||||
},
|
||||
new()
|
||||
{
|
||||
Title = "پرداخت",
|
||||
Description = isPaid ? "پرداخت شما تایید شد" : "منتظر تایید پرداخت هستیم",
|
||||
Title = "در انتظار پرداخت",
|
||||
Description = "منتظر تایید پرداخت هستیم",
|
||||
IsCompleted = isPaid,
|
||||
IsCurrent = !isPaid && !isCancelled,
|
||||
IsCurrent = !isPaid,
|
||||
Date = isPaid ? FormatDate(_order.PaymentDate) : ""
|
||||
},
|
||||
new()
|
||||
{
|
||||
Title = "تایید پرداخت",
|
||||
Description = "پرداخت شما تایید شد",
|
||||
IsCompleted = isPaid,
|
||||
IsCurrent = false,
|
||||
Date = isPaid ? FormatDate(_order.PaymentDate) : ""
|
||||
},
|
||||
new()
|
||||
{
|
||||
Title = "در حال پردازش",
|
||||
Description = "سفارش شما در حال آمادهسازی است",
|
||||
IsCompleted = false,
|
||||
IsCurrent = isPaid,
|
||||
Date = ""
|
||||
},
|
||||
new()
|
||||
{
|
||||
Title = "ارسال شده",
|
||||
Description = "سفارش شما به پست تحویل داده شد",
|
||||
IsCompleted = false,
|
||||
IsCurrent = false,
|
||||
Date = ""
|
||||
},
|
||||
new()
|
||||
{
|
||||
Title = "تحویل داده شده",
|
||||
Description = "سفارش به دست شما رسید",
|
||||
IsCompleted = false,
|
||||
IsCurrent = false,
|
||||
Date = ""
|
||||
}
|
||||
];
|
||||
|
||||
if (isCancelled)
|
||||
{
|
||||
_trackingSteps.Add(new()
|
||||
{
|
||||
Title = "لغو شده",
|
||||
Description = "ارسال این سفارش لغو شده است",
|
||||
IsCompleted = true,
|
||||
IsCurrent = true,
|
||||
Date = ""
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isReturned)
|
||||
{
|
||||
_trackingSteps.Add(new()
|
||||
{
|
||||
Title = "مرجوع شده",
|
||||
Description = "سفارش مرجوع شده است",
|
||||
IsCompleted = true,
|
||||
IsCurrent = true,
|
||||
Date = ""
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isOfficePickup)
|
||||
{
|
||||
_trackingSteps.Add(new()
|
||||
{
|
||||
Title = "آماده تحویل در دفتر",
|
||||
Description = "سفارش برای تحویل حضوری در دفتر آماده است",
|
||||
IsCompleted = true,
|
||||
IsCurrent = true,
|
||||
Date = ""
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
_trackingSteps.Add(new()
|
||||
{
|
||||
Title = "در حال آمادهسازی",
|
||||
Description = "سفارش شما در حال آمادهسازی است",
|
||||
IsCompleted = isInTransit || isDelivered,
|
||||
IsCurrent = isPaid && isPendingDelivery,
|
||||
Date = ""
|
||||
});
|
||||
_trackingSteps.Add(new()
|
||||
{
|
||||
Title = "ارسال شده",
|
||||
Description = "سفارش شما به پست تحویل داده شد",
|
||||
IsCompleted = isDelivered,
|
||||
IsCurrent = isInTransit,
|
||||
Date = ""
|
||||
});
|
||||
_trackingSteps.Add(new()
|
||||
{
|
||||
Title = "تحویل داده شده",
|
||||
Description = "سفارش به دست شما رسید",
|
||||
IsCompleted = isDelivered,
|
||||
IsCurrent = false,
|
||||
Date = ""
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatDate(Google.Protobuf.WellKnownTypes.Timestamp? timestamp)
|
||||
|
||||
@@ -1,102 +1,80 @@
|
||||
@attribute [Route(RouteConstants.Store.Orders)]
|
||||
@* Injection is handled in code-behind *@
|
||||
|
||||
<PageTitle>سفارشات من</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
|
||||
<MudStack Spacing="3">
|
||||
<PageHeader Title="سفارشات من" BackHref="@RouteConstants.Store.Products" />
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<LoadingState />
|
||||
}
|
||||
else if (_orders.Count == 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info">هنوز سفارشی ثبت نشده است.</MudAlert>
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Store"
|
||||
Href="@RouteConstants.Store.Products">
|
||||
مشاهده فروشگاه
|
||||
</MudButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Desktop Table -->
|
||||
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
|
||||
<MudPaper Elevation="1" Class="pa-4 rounded-lg">
|
||||
<MudTable Items="_orders">
|
||||
<HeaderContent>
|
||||
<MudTh>شماره سفارش</MudTh>
|
||||
<MudTh>تعداد اقلام</MudTh>
|
||||
<MudTh>مبلغ کل</MudTh>
|
||||
<MudTh>وضعیت پرداخت</MudTh>
|
||||
<MudTh>وضعیت ارسال</MudTh>
|
||||
<MudTh>تاریخ</MudTh>
|
||||
<MudTh></MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.Id</MudTd>
|
||||
<MudTd>@GetItemsCount(context)</MudTd>
|
||||
<MudTd>@FormatPrice(GetTotalAmount(context))</MudTd>
|
||||
<MudTd>
|
||||
<MudChip T="string" Size="Size.Small"
|
||||
Color="@GetPaymentStatusColor(context.PaymentStatus)"
|
||||
Variant="Variant.Filled">
|
||||
@GetPaymentStatusText(context.PaymentStatus)
|
||||
</MudChip>
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudChip T="string" Size="Size.Small"
|
||||
Color="@GetDeliveryStatusColor((int)context.DeliveryStatus)"
|
||||
Variant="Variant.Outlined">
|
||||
@GetDeliveryStatusText((int)context.DeliveryStatus)
|
||||
</MudChip>
|
||||
</MudTd>
|
||||
<MudTd>@FormatDate(context)</MudTd>
|
||||
<MudTd>
|
||||
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
|
||||
OnClick="() => ViewOrder(context.Id)">
|
||||
جزئیات
|
||||
</MudButton>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
</MudPaper>
|
||||
</MudHidden>
|
||||
|
||||
<!-- Mobile Cards -->
|
||||
<MudHidden Breakpoint="Breakpoint.MdAndUp">
|
||||
<MudStack Spacing="2">
|
||||
@foreach (var order in _orders)
|
||||
<PageHeader Title="سفارشات من" BackHref="@RouteConstants.Store.Products" />
|
||||
@if (_loading)
|
||||
{
|
||||
<LoadingState />
|
||||
}
|
||||
else if (_orders.Count == 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info">هنوز سفارشی ثبت نکردهاید.</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
|
||||
<MudTable Items="_orders" Dense="true">
|
||||
<HeaderContent>
|
||||
<MudTh>شناسه</MudTh>
|
||||
<MudTh>تاریخ</MudTh>
|
||||
<MudTh>وضعیت</MudTh>
|
||||
<MudTh>مبلغ</MudTh>
|
||||
<MudTh></MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.Id</MudTd>
|
||||
<MudTd>
|
||||
@if (context.PaymentDate != null)
|
||||
{
|
||||
<MudPaper Class="pa-3 rounded-lg cursor-pointer" Outlined="true"
|
||||
@onclick="() => ViewOrder(order.Id)">
|
||||
<MudStack Spacing="1">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.subtitle2">سفارش #@order.Id</MudText>
|
||||
<MudChip T="string" Size="Size.Small"
|
||||
Color="@GetPaymentStatusColor(order.PaymentStatus)"
|
||||
Variant="Variant.Filled">
|
||||
@GetPaymentStatusText(order.PaymentStatus)
|
||||
</MudChip>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">@FormatDate(order)</MudText>
|
||||
<MudChip T="string" Size="Size.Small"
|
||||
Color="@GetDeliveryStatusColor((int)order.DeliveryStatus)"
|
||||
Variant="Variant.Outlined">
|
||||
@GetDeliveryStatusText((int)order.DeliveryStatus)
|
||||
</MudChip>
|
||||
</MudStack>
|
||||
<MudDivider />
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2">@GetItemsCount(order) قلم</MudText>
|
||||
<MudText Typo="Typo.body2" Class="fw-semibold">@FormatPrice(GetTotalAmount(order)) تومان</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
@context.PaymentDate.ToDateTime().MiladiToJalaliWithTime()
|
||||
}
|
||||
</MudStack>
|
||||
</MudHidden>
|
||||
}
|
||||
</MudStack>
|
||||
else
|
||||
{
|
||||
<text>در انتظار پرداخت</text>
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd>@GetStatusText(context.PaymentStatus)</MudTd>
|
||||
<MudTd>@FormatPrice(context.FactorDetails.Sum(s=>(s.UnitPrice ?? 0) * (s.Count ?? 0)))</MudTd>
|
||||
<MudTd>
|
||||
<MudButton Variant="Variant.Text" Href="@($"{RouteConstants.Store.OrderDetail}{context.Id}")" StartIcon="@Icons.Material.Filled.Receipt">جزئیات</MudButton>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
</MudHidden>
|
||||
|
||||
<MudHidden Breakpoint="Breakpoint.MdAndUp">
|
||||
<MudStack Spacing="2">
|
||||
@foreach (var o in _orders)
|
||||
{
|
||||
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
|
||||
<MudStack Spacing="1">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText>سفارش #@o.Id</MudText>
|
||||
<MudText Class="mud-text-secondary">
|
||||
@if (o.PaymentDate != null)
|
||||
{
|
||||
@o.PaymentDate.ToDateTime().MiladiToJalaliWithTime()
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>در انتظار پرداخت</text>
|
||||
}
|
||||
</MudText>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText>وضعیت: @GetStatusText(o.PaymentStatus)</MudText>
|
||||
<MudText Color="Color.Primary">@FormatPrice(o.FactorDetails.Sum(s=>(s.UnitPrice ?? 0) * (s.Count ?? 0)))</MudText>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Justify="Justify.FlexEnd">
|
||||
<MudButton Size="Size.Small" Variant="Variant.Outlined" Href="@($"{RouteConstants.Store.OrderDetail}{o.Id}")" StartIcon="@Icons.Material.Filled.Receipt">جزئیات</MudButton>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
</MudStack>
|
||||
</MudHidden>
|
||||
}
|
||||
</MudContainer>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System;
|
||||
using CMSMicroservice.Protobuf.Protos.UserOrder;
|
||||
using DateTimeConverterCL;
|
||||
using FrontOffice.Main.Utilities;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Messages = CMSMicroservice.Protobuf.Protos;
|
||||
@@ -10,6 +10,7 @@ namespace FrontOffice.Main.Pages.Store;
|
||||
public partial class Orders : ComponentBase
|
||||
{
|
||||
[Inject] private OrderService OrderService { get; set; } = default!;
|
||||
[Inject] private VATService VAT { get; set; } = default!;
|
||||
|
||||
private List<GetUserOrderResponse> _orders = new();
|
||||
private bool _loading;
|
||||
@@ -32,81 +33,28 @@ public partial class Orders : ComponentBase
|
||||
}
|
||||
}
|
||||
|
||||
private void ViewOrder(long orderId)
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
Navigation.NavigateTo($"{RouteConstants.Store.OrderDetail}{orderId}");
|
||||
}
|
||||
|
||||
private static string FormatPrice(long price) => $"{price:N0}";
|
||||
|
||||
private static string FormatDate(GetUserOrderResponse order)
|
||||
{
|
||||
try
|
||||
if (firstRender)
|
||||
{
|
||||
var timestamp = order.PaymentDate;
|
||||
if (timestamp is null)
|
||||
return "در انتظار پرداخت";
|
||||
|
||||
return timestamp.ToDateTime().ToLocalTime().MiladiToJalaliWithTime();
|
||||
// بارگذاری نرخ VAT
|
||||
await VAT.LoadAsync();
|
||||
}
|
||||
catch
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
|
||||
|
||||
|
||||
private string GetStatusText(Messages.PaymentStatus contextPaymentStatus)
|
||||
{
|
||||
return contextPaymentStatus switch
|
||||
{
|
||||
return "—";
|
||||
}
|
||||
Messages.PaymentStatus.Pending => "در انتظار پرداخت",
|
||||
Messages.PaymentStatus.Success => "پرداخت شده",
|
||||
Messages.PaymentStatus.Reject => "پرداخت ناموفق",
|
||||
_ => "نامشخص",
|
||||
};
|
||||
}
|
||||
|
||||
private static int GetItemsCount(GetUserOrderResponse order) =>
|
||||
order.FactorDetails.Sum(f => f.Count ?? 0);
|
||||
|
||||
private static long GetTotalAmount(GetUserOrderResponse order)
|
||||
{
|
||||
if (order.VatInfo is not null && order.VatInfo.TotalAmount > 0)
|
||||
return order.VatInfo.TotalAmount;
|
||||
|
||||
if (order.Amount > 0)
|
||||
return order.Amount;
|
||||
|
||||
return order.FactorDetails.Sum(f => (f.UnitPrice ?? 0) * (f.Count ?? 0));
|
||||
}
|
||||
|
||||
private static string GetPaymentStatusText(Messages.PaymentStatus status) => status switch
|
||||
{
|
||||
Messages.PaymentStatus.Pending => "در انتظار پرداخت",
|
||||
Messages.PaymentStatus.Success => "پرداخت شده",
|
||||
Messages.PaymentStatus.Reject => "پرداخت ناموفق",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
|
||||
private static Color GetPaymentStatusColor(Messages.PaymentStatus status) => status switch
|
||||
{
|
||||
Messages.PaymentStatus.Pending => Color.Warning,
|
||||
Messages.PaymentStatus.Success => Color.Success,
|
||||
Messages.PaymentStatus.Reject => Color.Error,
|
||||
_ => Color.Default
|
||||
};
|
||||
|
||||
// Domain/public_messages: None=0, Pending=1, InTransit=2, Delivered=3, Returned=4, Cancelled=5, ReadyForOfficePickup=6
|
||||
private static string GetDeliveryStatusText(int status) => status switch
|
||||
{
|
||||
0 => "بدون ارسال",
|
||||
1 => "در انتظار ارسال",
|
||||
2 => "ارسال شده",
|
||||
3 => "تحویل داده شده",
|
||||
4 => "مرجوع شده",
|
||||
5 => "لغو شده",
|
||||
6 => "آماده تحویل در دفتر",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
|
||||
private static Color GetDeliveryStatusColor(int status) => status switch
|
||||
{
|
||||
0 => Color.Default,
|
||||
1 => Color.Warning,
|
||||
2 => Color.Primary,
|
||||
3 => Color.Success,
|
||||
4 => Color.Error,
|
||||
5 => Color.Dark,
|
||||
6 => Color.Primary,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ else
|
||||
{
|
||||
<AppImage Path="@MainImageUrl" Alt="@MainImageAlt"
|
||||
ObjectFit="ObjectFit.Cover"
|
||||
Style="width:100%; aspect-ratio:1/1; border-radius:12px;" />
|
||||
Style="width:100%; max-height:400px; border-radius:12px;" />
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -78,24 +78,15 @@ else
|
||||
@* </MudStack> *@
|
||||
@* } *@
|
||||
<MudDivider Class="my-2"/>
|
||||
@if (_isAuthenticated)
|
||||
{
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.h5" Color="Color.Primary">@FormatPrice(_product.Price)</MudText>
|
||||
@if (VAT.IsEnabled)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
||||
(شامل @VAT.VatPercentage% مالیات بر ارزش افزوده)
|
||||
</MudText>
|
||||
}
|
||||
</MudStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined" Icon="@Icons.Material.Filled.Lock">
|
||||
برای مشاهده قیمت ابتدا وارد شوید
|
||||
</MudAlert>
|
||||
}
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.h5" Color="Color.Primary">@FormatPrice(_product.Price)</MudText>
|
||||
@if (VAT.IsEnabled)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
||||
(شامل @VAT.VatPercentage% مالیات بر ارزش افزوده)
|
||||
</MudText>
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
<!-- نمایش وضعیت موجودی -->
|
||||
@if (IsInStock)
|
||||
@@ -150,27 +141,18 @@ else
|
||||
{
|
||||
<MudGrid Class="align-center" Justify="Justify.SpaceBetween">
|
||||
<MudItem xs="6">
|
||||
@if (_isAuthenticated)
|
||||
{
|
||||
<MudStack Spacing="1">
|
||||
@if (HasDiscount && OriginalPrice is not null)
|
||||
{
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudText Typo="Typo.caption"
|
||||
Class="mud-text-secondary mud-line-through">@FormatPrice(OriginalPrice.Value)</MudText>
|
||||
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small"
|
||||
Label="true">@($"٪{_product!.Discount}")</MudChip>
|
||||
</MudStack>
|
||||
}
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary">@FormatPrice(TotalPrice)</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Small" Class="me-1"/>برای مشاهده قیمت وارد شوید
|
||||
</MudText>
|
||||
}
|
||||
<MudStack Spacing="1">
|
||||
@if (HasDiscount && OriginalPrice is not null)
|
||||
{
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudText Typo="Typo.caption"
|
||||
Class="mud-text-secondary mud-line-through">@FormatPrice(OriginalPrice.Value)</MudText>
|
||||
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small"
|
||||
Label="true">@($"٪{_product!.Discount}")</MudChip>
|
||||
</MudStack>
|
||||
}
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary">@FormatPrice(TotalPrice)</MudText>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
|
||||
@if (IsInCart)
|
||||
|
||||
@@ -12,16 +12,11 @@ public partial class ProductDetail : ComponentBase, IDisposable
|
||||
[Inject] private ProductService ProductService { get; set; } = default!;
|
||||
[Inject] private CartService Cart { get; set; } = default!;
|
||||
[Inject] private VATService VAT { get; set; } = default!;
|
||||
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||
|
||||
private bool _isAuthenticated;
|
||||
|
||||
[Parameter] public long id { get; set; }
|
||||
|
||||
private Product? _product;
|
||||
private bool _loading;
|
||||
private bool _initialized;
|
||||
private int _qty = 1;
|
||||
private const int MinQty = 1;
|
||||
|
||||
@@ -49,35 +44,29 @@ public partial class ProductDetail : ComponentBase, IDisposable
|
||||
private bool IsInCart => CurrentCartItem is not null;
|
||||
private int CurrentCartQuantity => CurrentCartItem?.Quantity ?? 0;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
|
||||
await Cart.EnsureInitializedAsync();
|
||||
Cart.OnChange += HandleCartChanged;
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (!_initialized || id <= 0)
|
||||
return;
|
||||
|
||||
await LoadProductAsync();
|
||||
|
||||
}
|
||||
|
||||
private async Task LoadProductAsync()
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
|
||||
await Cart.EnsureInitializedAsync();
|
||||
Cart.OnChange += HandleCartChanged;
|
||||
|
||||
|
||||
|
||||
_loading = true;
|
||||
_product = await ProductService.GetByIdAsync(id);
|
||||
_loading = false;
|
||||
|
||||
if (_product is not null)
|
||||
{
|
||||
_galleryItems = BuildGalleryItems(_product);
|
||||
_selectedGalleryImage = _galleryItems.FirstOrDefault();
|
||||
_categoryPaths = _product.Categories;
|
||||
UpdateBreadcrumb();
|
||||
_qty = Math.Clamp(CurrentCartItem?.Quantity ?? MinQty, MinQty, Math.Max(MinQty, MaxQty));
|
||||
_qty = Math.Clamp(CurrentCartItem?.Quantity ?? _qty, MinQty, MaxQty);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -85,33 +74,32 @@ public partial class ProductDetail : ComponentBase, IDisposable
|
||||
_categoryPaths = Array.Empty<ProductCategoryPathInfo>();
|
||||
_breadcrumbItems.Clear();
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
if (firstRender)
|
||||
{
|
||||
// بارگذاری نرخ VAT
|
||||
await VAT.LoadAsync();
|
||||
}
|
||||
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
private async Task AddToCart()
|
||||
{
|
||||
if (_product is null) return;
|
||||
var product = _product;
|
||||
await GuestGate.RunAsync(() => Cart.Add(product, 1));
|
||||
await Cart.Add(_product, 1);
|
||||
}
|
||||
|
||||
private async Task RemoveFromCart()
|
||||
{
|
||||
if (_product is null) return;
|
||||
await GuestGate.RunAsync(async () =>
|
||||
{
|
||||
_qty--;
|
||||
await Cart.UpdateQuantity(CurrentCartItem!.ProductId, _qty);
|
||||
});
|
||||
_qty--;
|
||||
await Cart.UpdateQuantity(CurrentCartItem.ProductId, _qty);
|
||||
}
|
||||
|
||||
private void IncreaseLocalQty()
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Inventory2" Size="Size.Small" Class="me-1"/>
|
||||
@(_totalCount) محصول
|
||||
@(_products.Count) محصول
|
||||
</MudText>
|
||||
}
|
||||
</MudItem>
|
||||
@@ -118,14 +118,13 @@
|
||||
@foreach (var p in _products)
|
||||
{
|
||||
<MudItem xs="6" sm="6" md="3"
|
||||
onclick="@(() => OpenProduct(p.Id))">
|
||||
<div id="@($"shop-product-{p.Id}")" class="h-100">
|
||||
onclick="@(() => Navigation.NavigateTo($"{RouteConstants.Store.ProductDetail}{p.Id}"))">
|
||||
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden"
|
||||
Style="cursor:pointer;">
|
||||
Style="cursor:pointer;height: 300px">
|
||||
|
||||
|
||||
<MudCardContent Class="d-flex flex-column pa-1 h-100">
|
||||
<div style="aspect-ratio:1/1;width:100%;background-image: url('@p.ImageUrl');background-size: cover; background-position: center;border-radius: 0.5rem; position: relative;">
|
||||
<div style="height: 60%;background-image: url('@p.ImageUrl');background-size: cover; background-position: center;border-radius: 0.5rem; position: relative;">
|
||||
@if (p.RemainingCount <= 0)
|
||||
{
|
||||
<div style="position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;border-radius:0.5rem;">
|
||||
@@ -142,16 +141,8 @@
|
||||
</div>
|
||||
<div class="pa-1 flex-grow-1 d-flex flex-column justify-space-between">
|
||||
<MudText Typo="Typo.subtitle1">@p.Title</MudText>
|
||||
@if (_isAuthenticated)
|
||||
{
|
||||
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatPrice(p.Price)</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Small" Class="me-1"/>برای مشاهده قیمت وارد شوید
|
||||
</MudText>
|
||||
}
|
||||
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatPrice(p.Price)</MudText>
|
||||
|
||||
</div>
|
||||
</MudCardContent>
|
||||
<MudCardActions Class="mt-auto d-flex justify-space-between pa-2">
|
||||
@@ -165,34 +156,8 @@
|
||||
</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
</div>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
|
||||
@* Lazy Load — بارگذاری بیشتر *@
|
||||
@if (_loadingMore)
|
||||
{
|
||||
<MudStack AlignItems="AlignItems.Center" Class="py-4">
|
||||
<MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true"/>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">بارگذاری بیشتر...</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
else if (_hasMore)
|
||||
{
|
||||
<div class="d-flex justify-center py-4">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.ExpandMore"
|
||||
OnClick="LoadMore">
|
||||
نمایش محصولات بیشتر
|
||||
</MudButton>
|
||||
</div>
|
||||
}
|
||||
else if (_products.Count > 0)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Align="Align.Center" Class="py-4 mud-text-secondary">
|
||||
همه @(_products.Count) محصول نمایش داده شد
|
||||
</MudText>
|
||||
}
|
||||
}
|
||||
</MudContainer>
|
||||
|
||||
@@ -2,166 +2,90 @@ using System.Linq;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Routing;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.JSInterop;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using FrontOffice.Main.Utilities;
|
||||
|
||||
namespace FrontOffice.Main.Pages.Store;
|
||||
|
||||
public enum ProductSortOption
|
||||
{
|
||||
PriceDesc, // گرانترین (پیشفرض)
|
||||
PriceAsc, // ارزانترین
|
||||
Newest, // جدیدترین
|
||||
Title // الفبایی
|
||||
}
|
||||
|
||||
public partial class Products : ComponentBase, IDisposable
|
||||
{
|
||||
[Inject] private ProductService ProductService { get; set; } = default!;
|
||||
[Inject] private CategoryService CategoryService { get; set; } = default!;
|
||||
[Inject] private CartService Cart { get; set; } = default!;
|
||||
[Inject] private VATService VAT { get; set; } = default!;
|
||||
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||
[Inject] private IJSRuntime Js { get; set; } = default!;
|
||||
|
||||
private bool _isAuthenticated;
|
||||
private string _query = string.Empty;
|
||||
private bool _loading;
|
||||
private bool _loadingMore;
|
||||
private bool _hasMore = true;
|
||||
private int _currentPage = 1;
|
||||
private int _totalCount;
|
||||
private const int PageSize = 12;
|
||||
private List<Product> _products = new();
|
||||
private long? _activeCategoryId;
|
||||
private string? _activeCategoryTitle;
|
||||
private ProductSortOption _sortOption = ProductSortOption.PriceDesc;
|
||||
private bool _ignoreNextLocationChange;
|
||||
private bool _pendingScrollRestore;
|
||||
|
||||
private ProductSortOption _sortOption = ProductSortOption.PriceDesc; // پیشفرض: گرانترین
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
|
||||
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
|
||||
await Cart.EnsureInitializedAsync();
|
||||
Cart.OnChange += StateHasChanged;
|
||||
Navigation.LocationChanged += HandleLocationChanged;
|
||||
ApplyStateFromUri();
|
||||
await LoadPages(_currentPage);
|
||||
_pendingScrollRestore = true;
|
||||
await Load();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
await VAT.LoadAsync();
|
||||
|
||||
if (_pendingScrollRestore && !_loading && _products.Count > 0)
|
||||
{
|
||||
_pendingScrollRestore = false;
|
||||
var payload = await ShopListScrollRestore.TakeAsync(Js, ShopListScrollRestore.StoreKey);
|
||||
if (payload is not null)
|
||||
await ShopListScrollRestore.RestoreAsync(Js, payload);
|
||||
// بارگذاری نرخ VAT
|
||||
await VAT.LoadAsync();
|
||||
}
|
||||
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
private void ApplyStateFromUri()
|
||||
{
|
||||
var state = ShopListQueryState.Parse(Navigation.ToAbsoluteUri(Navigation.Uri));
|
||||
_query = state.Query;
|
||||
_sortOption = ShopListQueryState.ParseSortOption(state.Sort);
|
||||
_activeCategoryId = state.CategoryId;
|
||||
_currentPage = state.Pages;
|
||||
}
|
||||
|
||||
private ShopListQueryState CaptureState() => new()
|
||||
{
|
||||
Query = _query,
|
||||
Sort = ShopListQueryState.ToSortKey(_sortOption),
|
||||
CategoryId = _activeCategoryId,
|
||||
Pages = Math.Max(1, _currentPage)
|
||||
};
|
||||
|
||||
private void SyncUrl()
|
||||
{
|
||||
var target = CaptureState().ToRelativeUrl(RouteConstants.Store.Products);
|
||||
var currentPathAndQuery = Navigation.ToAbsoluteUri(Navigation.Uri).PathAndQuery;
|
||||
if (string.Equals(currentPathAndQuery, target, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(currentPathAndQuery, target + "/", StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
_ignoreNextLocationChange = true;
|
||||
Navigation.NavigateTo(target, replace: true);
|
||||
}
|
||||
|
||||
private async Task LoadPages(int pagesToLoad)
|
||||
private async Task Load()
|
||||
{
|
||||
_loading = true;
|
||||
_products.Clear();
|
||||
_hasMore = true;
|
||||
pagesToLoad = Math.Max(1, pagesToLoad);
|
||||
var sortBy = ShopListQueryState.ToApiSortBy(_sortOption);
|
||||
|
||||
for (var page = 1; page <= pagesToLoad; page++)
|
||||
{
|
||||
var result = await ProductService.GetProductsPagedAsync(
|
||||
_query, _activeCategoryId, sortBy, page, PageSize);
|
||||
if (page == 1)
|
||||
{
|
||||
_products = result.Products;
|
||||
_totalCount = result.TotalCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
_products.AddRange(result.Products);
|
||||
}
|
||||
|
||||
_currentPage = page;
|
||||
_hasMore = result.HasNext;
|
||||
if (!_hasMore)
|
||||
break;
|
||||
}
|
||||
|
||||
UpdateCategoryFilterFromUri();
|
||||
var sortBy = GetSortByValue();
|
||||
_products = await ProductService.GetProductsAsync(_query, _activeCategoryId, sortBy);
|
||||
_activeCategoryTitle = _activeCategoryId is { } categoryId
|
||||
? (await CategoryService.GetByIdAsync(categoryId))?.Title
|
||||
: null;
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private async Task ReloadFromFilters()
|
||||
private string GetSortByValue()
|
||||
{
|
||||
_currentPage = 1;
|
||||
await LoadPages(1);
|
||||
SyncUrl();
|
||||
StateHasChanged();
|
||||
return _sortOption switch
|
||||
{
|
||||
ProductSortOption.PriceDesc => "price desc",
|
||||
ProductSortOption.PriceAsc => "price asc",
|
||||
ProductSortOption.Newest => "id desc",
|
||||
ProductSortOption.Title => "title asc",
|
||||
_ => "price desc"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task LoadMore()
|
||||
private async Task OnSortChanged()
|
||||
{
|
||||
if (_loadingMore || !_hasMore) return;
|
||||
|
||||
_loadingMore = true;
|
||||
StateHasChanged();
|
||||
|
||||
_currentPage++;
|
||||
var sortBy = ShopListQueryState.ToApiSortBy(_sortOption);
|
||||
var result = await ProductService.GetProductsPagedAsync(
|
||||
_query, _activeCategoryId, sortBy, _currentPage, PageSize);
|
||||
_products.AddRange(result.Products);
|
||||
_hasMore = result.HasNext;
|
||||
SyncUrl();
|
||||
|
||||
_loadingMore = false;
|
||||
StateHasChanged();
|
||||
await Load();
|
||||
}
|
||||
|
||||
private async Task OnSortChanged() => await ReloadFromFilters();
|
||||
|
||||
private async Task OnQueryChanged(KeyboardEventArgs _) => await ReloadFromFilters();
|
||||
private async Task OnQueryChanged(KeyboardEventArgs _)
|
||||
{
|
||||
await Load();
|
||||
}
|
||||
|
||||
private async Task AddToCart(Product p)
|
||||
{
|
||||
await GuestGate.RunAsync(() => Cart.Add(p, 1));
|
||||
}
|
||||
|
||||
private async Task OpenProduct(long productId)
|
||||
{
|
||||
await ShopListScrollRestore.SaveAsync(Js, ShopListScrollRestore.StoreKey, productId);
|
||||
Navigation.NavigateTo($"{RouteConstants.Store.ProductDetail}{productId}");
|
||||
await Cart.Add(p, 1);
|
||||
}
|
||||
|
||||
private string FormatPrice(long price) => $"{VAT.AddVAT(price):N0} تومان";
|
||||
@@ -174,33 +98,24 @@ public partial class Products : ComponentBase, IDisposable
|
||||
|
||||
private void HandleLocationChanged(object? sender, LocationChangedEventArgs args)
|
||||
{
|
||||
if (_ignoreNextLocationChange)
|
||||
_ = InvokeAsync(Load);
|
||||
}
|
||||
|
||||
private void UpdateCategoryFilterFromUri()
|
||||
{
|
||||
var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
|
||||
if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("category", out var values) &&
|
||||
long.TryParse(values.FirstOrDefault(), out var categoryId))
|
||||
{
|
||||
_ignoreNextLocationChange = false;
|
||||
_activeCategoryId = categoryId;
|
||||
return;
|
||||
}
|
||||
|
||||
var uri = Navigation.ToAbsoluteUri(args.Location);
|
||||
if (!uri.AbsolutePath.Equals(RouteConstants.Store.Products, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
var incoming = ShopListQueryState.Parse(uri);
|
||||
if (incoming.Matches(CaptureState()))
|
||||
return;
|
||||
|
||||
_ = InvokeAsync(async () =>
|
||||
{
|
||||
ApplyStateFromUri();
|
||||
await LoadPages(_currentPage);
|
||||
_pendingScrollRestore = true;
|
||||
StateHasChanged();
|
||||
});
|
||||
_activeCategoryId = null;
|
||||
}
|
||||
|
||||
private async Task ClearCategoryFilter()
|
||||
private void ClearCategoryFilter()
|
||||
{
|
||||
_activeCategoryId = null;
|
||||
_activeCategoryTitle = null;
|
||||
await ReloadFromFilters();
|
||||
Navigation.NavigateTo(RouteConstants.Store.Products);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@page "/"
|
||||
@model FrontOffice.Main.Pages.HostModel
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@namespace FrontOffice.Main.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@@ -9,26 +8,6 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>@Model.Seo.Title</title>
|
||||
<meta name="description" content="@Model.Seo.Description" />
|
||||
<link rel="canonical" href="@Model.Seo.CanonicalUrl" />
|
||||
@if (Model.Seo.NoIndex)
|
||||
{
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<meta name="robots" content="index, follow" />
|
||||
}
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:locale" content="fa_IR" />
|
||||
<meta property="og:title" content="@Model.Seo.Title" />
|
||||
<meta property="og:description" content="@Model.Seo.Description" />
|
||||
<meta property="og:url" content="@Model.Seo.CanonicalUrl" />
|
||||
<meta property="og:site_name" content="کارابازار سلامت" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:title" content="@Model.Seo.Title" />
|
||||
<meta name="twitter:description" content="@Model.Seo.Description" />
|
||||
<meta name="theme-color" content="#7c4dff" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
@@ -36,7 +15,7 @@
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
<component type="typeof(HeadOutlet)" render-mode="Server" />
|
||||
|
||||
@* <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" /> *@
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
|
||||
<!-- MudBlazor CSS (باید قبل از site.css باشه تا override بشه) -->
|
||||
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" asp-append-version="true" />
|
||||
<!-- Custom styles (بعد از MudBlazor برای override) -->
|
||||
@@ -44,26 +23,8 @@
|
||||
<link href="FrontOffice.Main.styles.css" rel="stylesheet" />
|
||||
<!-- d3-org-chart custom styles -->
|
||||
<link href="css/org-chart.css" rel="stylesheet" asp-append-version="true" />
|
||||
<style>
|
||||
.seo-crawlable {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Seo.CrawlableContent))
|
||||
{
|
||||
<div class="seo-crawlable">@Model.Seo.CrawlableContent</div>
|
||||
}
|
||||
|
||||
<component type="typeof(App)" render-mode="Server" />
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
using FrontOffice.Main.Utilities.Seo;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace FrontOffice.Main.Pages;
|
||||
|
||||
public class HostModel : PageModel
|
||||
{
|
||||
private readonly SeoMetadataProvider _seoMetadataProvider;
|
||||
|
||||
public SeoPageMetadata Seo { get; private set; } = default!;
|
||||
|
||||
public HostModel(SeoMetadataProvider seoMetadataProvider)
|
||||
{
|
||||
_seoMetadataProvider = seoMetadataProvider;
|
||||
}
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
Seo = await _seoMetadataProvider.GetForPathAsync(Request.Path);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using FluentValidation;
|
||||
using FrontOffice.Main.Utilities;
|
||||
using FrontOffice.Main.Utilities.Seo;
|
||||
using System.Net;
|
||||
using FrontOffice.Main.Utilities.Pdf;
|
||||
|
||||
@@ -13,7 +12,6 @@ builder.Services.AddServerSideBlazor();
|
||||
#region AddCommonServices
|
||||
|
||||
builder.Services.AddCommonServices();
|
||||
builder.Services.Configure<SeoSettings>(builder.Configuration.GetSection(SeoSettings.SectionName));
|
||||
builder.Services.AddSingleton<MainService>();
|
||||
|
||||
#endregion
|
||||
@@ -57,17 +55,6 @@ webApp.UseStaticFiles();
|
||||
webApp.UseRouting();
|
||||
|
||||
webApp.MapBlazorHub();
|
||||
|
||||
webApp.MapGet("/sitemap.xml", async (SitemapGenerator sitemapGenerator, CancellationToken cancellationToken) =>
|
||||
{
|
||||
var bytes = await sitemapGenerator.GenerateBytesAsync(cancellationToken);
|
||||
return Results.File(bytes, "application/xml; charset=utf-8");
|
||||
});
|
||||
|
||||
// Legacy URLs from old site / common e-commerce paths — redirect to homepage
|
||||
webApp.MapGet("/wishlist", () => Results.Redirect("/", permanent: true));
|
||||
webApp.MapGet("/wishlist/", () => Results.Redirect("/", permanent: true));
|
||||
|
||||
webApp.MapFallbackToPage("/_Host");
|
||||
|
||||
|
||||
|
||||
@@ -22,9 +22,7 @@
|
||||
PhoneNumber="@_phoneNumber"
|
||||
ResendRemaining="_resendRemaining"
|
||||
OnChangePhone="ChangePhoneAsync"
|
||||
OnResendOtp="ResendOtpAsync"
|
||||
ReferralCode="@_referralCode"
|
||||
ReferralCodeChanged="OnReferralCodeChanged" />
|
||||
OnResendOtp="ResendOtpAsync" />
|
||||
</div>
|
||||
}
|
||||
else
|
||||
@@ -53,9 +51,7 @@ else
|
||||
PhoneNumber="@_phoneNumber"
|
||||
ResendRemaining="_resendRemaining"
|
||||
OnChangePhone="ChangePhoneAsync"
|
||||
OnResendOtp="ResendOtpAsync"
|
||||
ReferralCode="@_referralCode"
|
||||
ReferralCodeChanged="OnReferralCodeChanged" />
|
||||
OnResendOtp="ResendOtpAsync" />
|
||||
|
||||
<MudStack Class="mt-4" Spacing="2">
|
||||
@if (_currentStep == AuthStep.Phone)
|
||||
|
||||
@@ -42,9 +42,6 @@ public partial class AuthDialog : IDisposable
|
||||
private string? _captchaCode;
|
||||
private string? _captchaInput;
|
||||
|
||||
// Referral code field
|
||||
private string? _referralCode;
|
||||
|
||||
[Inject] private ILocalStorageService LocalStorage { get; set; } = default!;
|
||||
[Inject] private UserContract.UserContractClient UserClient { get; set; } = default!;
|
||||
|
||||
@@ -69,20 +66,9 @@ public partial class AuthDialog : IDisposable
|
||||
{
|
||||
_phoneRequest.Mobile = storedPhone;
|
||||
}
|
||||
|
||||
_referralCode = await LocalStorage.GetItemAsync<string>("referral:code");
|
||||
// await LocalStorage.RemoveItemAsync(TokenStorageKey);
|
||||
}
|
||||
|
||||
private async Task OnReferralCodeChanged(string? value)
|
||||
{
|
||||
_referralCode = value;
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
await LocalStorage.SetItemAsync("referral:code", value);
|
||||
else
|
||||
await LocalStorage.RemoveItemAsync("referral:code");
|
||||
}
|
||||
|
||||
private void GenerateCaptcha()
|
||||
{
|
||||
_captchaCode = Guid.NewGuid().ToString("N")[..6].ToUpperInvariant();
|
||||
@@ -196,12 +182,9 @@ public partial class AuthDialog : IDisposable
|
||||
_verifyRequest.Mobile = _phoneNumber;
|
||||
|
||||
var storedReferralCode = await LocalStorage.GetItemAsync<string>("referral:code");
|
||||
var effectiveReferralCode = !string.IsNullOrWhiteSpace(_referralCode)
|
||||
? _referralCode
|
||||
: storedReferralCode;
|
||||
if (!string.IsNullOrWhiteSpace(effectiveReferralCode))
|
||||
if (!string.IsNullOrWhiteSpace(storedReferralCode))
|
||||
{
|
||||
_verifyRequest.ParentReferralCode = effectiveReferralCode;
|
||||
_verifyRequest.ParentReferralCode = storedReferralCode;
|
||||
}
|
||||
|
||||
var validationResult = true; // _verifyRequestValidator.Validate(_verifyRequest);
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
@inject AuthService AuthService
|
||||
@inject ISnackbar Snackbar
|
||||
@inject NavigationManager Navigation
|
||||
@inject PackageService PackageService
|
||||
|
||||
<MudDialog DisableSidePadding="true">
|
||||
<DialogContent>
|
||||
@@ -124,23 +123,6 @@
|
||||
private readonly Guid _signGuid = Guid.NewGuid();
|
||||
private System.Timers.Timer? _timer;
|
||||
private const string TokenStorageKey = "auth:token";
|
||||
private string _packageDisplayName = "پکیج پایه";
|
||||
private string _packageDisplayPrice = "۵۶ میلیون تومان";
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var packages = await PackageService.GetAllPackagesAsync();
|
||||
var basePackage = packages.FirstOrDefault(p => p.IsBasePackage) ?? packages.FirstOrDefault();
|
||||
if (basePackage != null)
|
||||
{
|
||||
_packageDisplayName = basePackage.Title;
|
||||
_packageDisplayPrice = $"{basePackage.Price:N0} تومان";
|
||||
}
|
||||
}
|
||||
catch { /* fallback to defaults */ }
|
||||
}
|
||||
|
||||
// این Modal غیرقابل بسته شدن است - Options در ShowAsync تنظیم میشوند
|
||||
|
||||
@@ -260,7 +242,7 @@
|
||||
<p>۱-۳. پاداش معرفی: مبلغی که به ازای هر معرفی موفق به کاربر تعلق میگیرد.</p>
|
||||
|
||||
<p><strong>ماده ۲ - شرایط عضویت</strong></p>
|
||||
<p>۲-۱. کاربر با پرداخت مبلغ {_packageDisplayPrice} ({_packageDisplayName})، امکان عضویت در باشگاه مشتریان را خواهد داشت.</p>
|
||||
<p>۲-۱. کاربر با پرداخت مبلغ ۵۶ میلیون تومان (پکیج پایه)، امکان عضویت در باشگاه مشتریان را خواهد داشت.</p>
|
||||
<p>۲-۲. پس از امضای این قرارداد، لینک دعوت کاربر فعال خواهد شد.</p>
|
||||
<p>۲-۳. کاربر میتواند حداکثر ۲ نفر را مستقیماً دعوت کند (یک نفر در تیم اول و یک نفر در تیم دوم).</p>
|
||||
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
<MudLink Href="@(RouteConstants.FAQ.Index)" Class="footer-link">سوالات متداول</MudLink>
|
||||
<MudLink Href="@(RouteConstants.Contact.Index)" Class="footer-link">ارتباط با ما</MudLink>
|
||||
<MudLink Href="@(RouteConstants.About.Index)" Class="footer-link">درباره ما</MudLink>
|
||||
<MudLink Href="@(RouteConstants.Licenses.Index)" Class="footer-link">مجوزها</MudLink>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
@using FrontOffice.Main.Utilities
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Color="Color.Warning" />
|
||||
<MudText Typo="Typo.h6">موجودی کیف پول اصلی کافی نیست</MudText>
|
||||
</MudStack>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudStack Spacing="2">
|
||||
@if (AllowChargeCredit)
|
||||
{
|
||||
@if (IsMagicWallet && !MagicCeilingFull)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">
|
||||
برای تکمیل این سفارش موجودی شما کافی نیست. کیف پول شما در حالت جادویی است؛ برای دریافت ضریب، از مسیر شارژ جادویی اقدام کنید.
|
||||
</MudText>
|
||||
}
|
||||
else if (IsMagicWallet && MagicCeilingFull)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Dense="true" Variant="Variant.Outlined">
|
||||
سقف شارژ جادویی شما در این دور پر است. میتوانید بهصورت استثنایی کیف اصلی را بدون ضریب (۱:۱) شارژ کنید.
|
||||
</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">
|
||||
برای تکمیل این سفارش، موجودی اصلی شما کافی نیست. میتوانید کیف پول را شارژ کنید و سپس سفارش را ثبت کنید.
|
||||
</MudText>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Dense="true" Variant="Variant.Outlined">
|
||||
برای شارژ کیف پول اصلی ابتدا باید پکیج را خریداری کرده و قرارداد باشگاه مشتریان را امضا کنید.
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
<MudPaper Outlined="true" Class="pa-3 rounded-lg">
|
||||
<MudStack Spacing="1">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2">موجودی فعلی:</MudText>
|
||||
<MudText Typo="Typo.body2"><strong>@FormatPrice(CurrentBalance)</strong></MudText>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2">مبلغ سفارش:</MudText>
|
||||
<MudText Typo="Typo.body2"><strong>@FormatPrice(RequiredAmount)</strong></MudText>
|
||||
</MudStack>
|
||||
<MudDivider Class="my-1" />
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2" Color="Color.Error">کمبود:</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Error"><strong>@FormatPrice(ShortfallAmount)</strong></MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudStack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel">بستن</MudButton>
|
||||
@if (AllowChargeCredit)
|
||||
{
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="@(IsMagicWallet && !MagicCeilingFull ? Color.Secondary : Color.Primary)"
|
||||
StartIcon="@(IsMagicWallet && !MagicCeilingFull ? Icons.Material.Filled.AutoAwesome : Icons.Material.Filled.Payment)"
|
||||
OnClick="GoToCharge">
|
||||
@ChargeButtonLabel
|
||||
</MudButton>
|
||||
}
|
||||
else if (!HasPurchasedPackage)
|
||||
{
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.CardGiftcard"
|
||||
OnClick="GoToPackages">
|
||||
مشاهده پکیجها
|
||||
</MudButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Handshake"
|
||||
OnClick="GoToClubMembership">
|
||||
امضای قرارداد باشگاه
|
||||
</MudButton>
|
||||
}
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public long CurrentBalance { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public long RequiredAmount { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public long ShortfallAmount { get; set; }
|
||||
|
||||
/// <summary>اگر false باشد، به صفحه شارژ هدایت نمیشود.</summary>
|
||||
[Parameter]
|
||||
public bool AllowChargeCredit { get; set; } = true;
|
||||
|
||||
[Parameter]
|
||||
public bool HasPurchasedPackage { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool IsMagicWallet { get; set; }
|
||||
|
||||
/// <summary>سقف واریز جادویی این دور پر شده است.</summary>
|
||||
[Parameter]
|
||||
public bool MagicCeilingFull { get; set; }
|
||||
|
||||
private string ChargeButtonLabel =>
|
||||
IsMagicWallet && !MagicCeilingFull
|
||||
? "شارژ کیف جادویی"
|
||||
: IsMagicWallet && MagicCeilingFull
|
||||
? "شارژ عادی بدون ضریب (سقف جادویی پر است)"
|
||||
: "شارژ حساب اصلی";
|
||||
|
||||
private void Cancel() => MudDialog.Cancel();
|
||||
|
||||
private void GoToCharge()
|
||||
{
|
||||
if (IsMagicWallet && !MagicCeilingFull)
|
||||
{
|
||||
MudDialog.Cancel();
|
||||
Navigation.NavigateTo(RouteConstants.Profile.MagicWallet);
|
||||
return;
|
||||
}
|
||||
|
||||
MudDialog.Close(DialogResult.Ok(ShortfallAmount));
|
||||
}
|
||||
|
||||
private void GoToPackages()
|
||||
{
|
||||
MudDialog.Cancel();
|
||||
Navigation.NavigateTo(RouteConstants.Package.List);
|
||||
}
|
||||
|
||||
private void GoToClubMembership()
|
||||
{
|
||||
MudDialog.Cancel();
|
||||
Navigation.NavigateTo(RouteConstants.Club.Membership);
|
||||
}
|
||||
|
||||
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
</MudHidden>
|
||||
|
||||
<div class="d-flex align-center gap-2">
|
||||
<MudLink Href="@(_isAuthenticated ? RouteConstants.Profile.Index : RouteConstants.Main.MainPage)" Underline="Underline.None">
|
||||
<MudLink Href="@(RouteConstants.Main.MainPage)" Underline="Underline.None">
|
||||
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center">
|
||||
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
|
||||
<MudImage ObjectFit="ObjectFit.Cover"
|
||||
@@ -42,7 +42,7 @@
|
||||
else if (IsInDiscountStore)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small"
|
||||
Class="d-none d-md-flex" Icon="@Icons.Material.Filled.Loyalty">فروشگاه اعتباری</MudChip>
|
||||
Class="d-none d-md-flex" Icon="@Icons.Material.Filled.Loyalty">فروشگاه تخفیفی</MudChip>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
else if (IsInDiscountStore && _isAuthenticated)
|
||||
{
|
||||
@* ── Discount store nav ── *@
|
||||
<MudLink Href="@(RouteConstants.DiscountStore.Products)" Typo="Typo.subtitle1" Class="mud-link">محصولات اعتباری</MudLink>
|
||||
<MudLink Href="@(RouteConstants.DiscountStore.Products)" Typo="Typo.subtitle1" Class="mud-link">محصولات تخفیفی</MudLink>
|
||||
<MudLink Href="@(RouteConstants.DiscountStore.Cart)" Typo="Typo.subtitle1" Class="mud-link">سبد خرید</MudLink>
|
||||
<MudLink Href="@(RouteConstants.DiscountStore.Orders)" Typo="Typo.subtitle1" Class="mud-link">سفارشات من</MudLink>
|
||||
}
|
||||
@@ -73,7 +73,6 @@
|
||||
<MudLink Href="@(RouteConstants.Blog.Index)" Typo="Typo.subtitle1" Class="mud-link">بلاگ</MudLink>
|
||||
<MudLink Href="@(RouteConstants.Contact.Index)" Typo="Typo.subtitle1" Class="mud-link">ارتباط با ما</MudLink>
|
||||
<MudLink Href="@(RouteConstants.About.Index)" Typo="Typo.subtitle1" Class="mud-link">درباره ما</MudLink>
|
||||
<MudLink Href="@(RouteConstants.Licenses.Index)" Typo="Typo.subtitle1" Class="mud-link">مجوزها</MudLink>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center gap-2">
|
||||
@@ -160,9 +159,9 @@
|
||||
}
|
||||
else if (IsInDiscountStore)
|
||||
{
|
||||
<MudText Typo="Typo.overline" Style="color:var(--mud-palette-error);">فروشگاه اعتباری</MudText>
|
||||
<MudText Typo="Typo.overline" Style="color:var(--mud-palette-error);">فروشگاه تخفیفی</MudText>
|
||||
<MudLink Href="@(RouteConstants.DiscountStore.Products)" Typo="Typo.subtitle1"
|
||||
OnClick="() => _drawerOpen=false">محصولات اعتباری</MudLink>
|
||||
OnClick="() => _drawerOpen=false">محصولات تخفیفی</MudLink>
|
||||
<MudLink Href="@(RouteConstants.DiscountStore.Cart)" Typo="Typo.subtitle1"
|
||||
OnClick="() => _drawerOpen=false">سبد خرید</MudLink>
|
||||
<MudLink Href="@(RouteConstants.DiscountStore.Orders)" Typo="Typo.subtitle1"
|
||||
@@ -192,9 +191,6 @@
|
||||
<MudLink Href="@(RouteConstants.Contact.Index)" Typo="Typo.subtitle1" OnClick="() => _drawerOpen=false">
|
||||
ارتباط با ما
|
||||
</MudLink>
|
||||
<MudLink Href="@(RouteConstants.Licenses.Index)" Typo="Typo.subtitle1" OnClick="() => _drawerOpen=false">
|
||||
مجوزها
|
||||
</MudLink>
|
||||
|
||||
<MudDivider Class="my-2"/>
|
||||
@if (_isAuthenticated)
|
||||
@@ -285,9 +281,9 @@
|
||||
else
|
||||
{
|
||||
@* ── Default bottom nav (outside stores) ── *@
|
||||
<MudLink Href="@(_isAuthenticated ? RouteConstants.Profile.Index : RouteConstants.Main.MainPage)" Class="bottom-nav-item">
|
||||
<MudLink Href="@(RouteConstants.Profile.Index)" Class="bottom-nav-item">
|
||||
<MudIcon Icon="@Icons.Material.Outlined.Home" Size="Size.Medium" />
|
||||
<MudText Typo="Typo.caption">@(_isAuthenticated ? "داشبورد" : "خانه")</MudText>
|
||||
<MudText Typo="Typo.caption">خانه</MudText>
|
||||
</MudLink>
|
||||
@if (_isAuthenticated)
|
||||
{
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
@using FrontOffice.Main.Utilities
|
||||
@inject PackageService PackageService
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
@if (_selectedPackage != null)
|
||||
{
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ArrowForward"
|
||||
Size="Size.Small"
|
||||
OnClick="BackToList" />
|
||||
<MudText Typo="Typo.h6">انتخاب روش پرداخت</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ShoppingCart" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.h6">انتخاب پکیج</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
@if (_isLoading)
|
||||
{
|
||||
<MudStack AlignItems="AlignItems.Center" Class="py-8">
|
||||
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-2">در حال بارگذاری پکیجها...</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
else if (_selectedPackage != null)
|
||||
{
|
||||
@* ── Step 2: Payment method selection ── *@
|
||||
<MudStack Spacing="3">
|
||||
@* Selected Package Summary *@
|
||||
<MudPaper Elevation="0" Class="pa-3 rounded-lg" Style="background:rgba(99,102,241,.06);">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudAvatar Size="Size.Medium" Style="background:rgba(99,102,241,.15); color:#6366f1;">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Inventory2" />
|
||||
</MudAvatar>
|
||||
<MudStack Spacing="0" Class="flex-grow-1">
|
||||
<MudText Typo="Typo.subtitle1"><b>@_selectedPackage.Title</b></MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Success">@_selectedPackage.FormattedPrice</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<MudDivider />
|
||||
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">
|
||||
روش پرداخت خود را انتخاب کنید:
|
||||
</MudText>
|
||||
|
||||
@* Direct Payment — always available *@
|
||||
<MudPaper Elevation="0" Class="pa-4 rounded-lg pkg-payment-option"
|
||||
Style="border:2px solid var(--mud-palette-primary); cursor:pointer;"
|
||||
@onclick="() => SelectPaymentMethod(PaymentMethodType.DirectPayment)">
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.CreditCard" Color="Color.Primary" Size="Size.Medium" />
|
||||
<MudStack Spacing="0" Class="flex-grow-1">
|
||||
<MudText Typo="Typo.subtitle1" Color="Color.Primary"><b>پرداخت مستقیم</b></MudText>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">پرداخت آنی از طریق درگاه بانکی</MudText>
|
||||
</MudStack>
|
||||
<MudIcon Icon="@Icons.Material.Filled.ArrowBack" Color="Color.Primary" Size="Size.Small" />
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
@* Daya Loan — only for base package AND first purchase cycle *@
|
||||
@if (_selectedPackage.SupportsDayaPurchase && PurchaseCycleCount == 0)
|
||||
{
|
||||
<MudPaper Elevation="0" Class="pa-4 rounded-lg pkg-payment-option"
|
||||
Style="border:2px solid var(--mud-palette-tertiary); cursor:pointer;"
|
||||
@onclick="() => SelectPaymentMethod(PaymentMethodType.DayaLoan)">
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Diamond" Color="Color.Tertiary" Size="Size.Medium" />
|
||||
<MudStack Spacing="0" Class="flex-grow-1">
|
||||
<MudText Typo="Typo.subtitle1" Color="Color.Tertiary"><b>اعتبار الماسی دایا</b></MudText>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">تأمین اعتبار از طریق خرید توکن الماس</MudText>
|
||||
</MudStack>
|
||||
<MudIcon Icon="@Icons.Material.Filled.ArrowBack" Color="Color.Tertiary" Size="Size.Small" />
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
else if (!_selectedPackage.SupportsDayaPurchase)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="mt-1">
|
||||
اعتبار الماسی دایا فقط برای پکیج پایه قابل استفاده است.
|
||||
</MudAlert>
|
||||
}
|
||||
else if (PurchaseCycleCount > 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="mt-1">
|
||||
از دور دوم به بعد، خرید پکیج فقط از طریق درگاه بانکی امکانپذیر است.
|
||||
</MudAlert>
|
||||
}
|
||||
</MudStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
@* ── Step 1: Package tile grid ── *@
|
||||
<MudStack Spacing="3">
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">
|
||||
پکیج مورد نظر خود را انتخاب کنید:
|
||||
</MudText>
|
||||
|
||||
<MudGrid Spacing="3">
|
||||
@foreach (var pkg in _packages)
|
||||
{
|
||||
<MudItem xs="12" sm="6" md="@(_packages.Count <= 2 ? 6 : 4)">
|
||||
<MudPaper Elevation="0" Class="pa-4 rounded-xl pkg-tile text-center"
|
||||
Style="@GetTileStyle(pkg)"
|
||||
@onclick="() => SelectPackage(pkg)">
|
||||
@if (pkg.IsBasePackage)
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Primary" Variant="Variant.Filled"
|
||||
Class="pkg-tile-badge">پایه</MudChip>
|
||||
}
|
||||
<MudStack Spacing="1" AlignItems="AlignItems.Center">
|
||||
<MudAvatar Size="Size.Large" Style="@GetAvatarStyle(pkg)" Class="mb-1">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Inventory2" Size="Size.Large" />
|
||||
</MudAvatar>
|
||||
<MudText Typo="Typo.subtitle1"><b>@pkg.Title</b></MudText>
|
||||
<MudText Typo="Typo.h6" Color="Color.Success">@pkg.FormattedPrice</MudText>
|
||||
@if (!string.IsNullOrWhiteSpace(pkg.Description))
|
||||
{
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary"
|
||||
Style="display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">
|
||||
@((MarkupString)pkg.Description)
|
||||
</MudText>
|
||||
}
|
||||
@* Key features *@
|
||||
<MudStack Spacing="0" Class="mt-2" Style="width:100%;">
|
||||
@if (pkg.MagicWalletMultiplier > 0)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
||||
<MudIcon Icon="@Icons.Material.Filled.AutoAwesome" Size="Size.Small" Class="me-1" Style="font-size:.85rem;vertical-align:middle;" />
|
||||
ضریب جادویی: @pkg.MagicWalletMultiplier.ToString("F1")x
|
||||
</MudText>
|
||||
}
|
||||
@if (pkg.DiscountMultiplier > 0)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Discount" Size="Size.Small" Class="me-1" Style="font-size:.85rem;vertical-align:middle;" />
|
||||
ضریب اعتبار: @pkg.DiscountMultiplier.ToString("F1")x
|
||||
</MudText>
|
||||
}
|
||||
@if (pkg.SupportsDayaPurchase)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Tertiary">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Diamond" Size="Size.Small" Class="me-1" Style="font-size:.85rem;vertical-align:middle;" />
|
||||
قابل خرید با دایا
|
||||
</MudText>
|
||||
}
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
</MudStack>
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel" Variant="Variant.Text" Color="Color.Default">بستن</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
/// <summary>Number of previous purchase cycles — controls Daya eligibility</summary>
|
||||
[Parameter] public int PurchaseCycleCount { get; set; }
|
||||
|
||||
private List<PackageDto> _packages = new();
|
||||
private PackageDto? _selectedPackage;
|
||||
private bool _isLoading = true;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_isLoading = true;
|
||||
try
|
||||
{
|
||||
_packages = await PackageService.GetAllPackagesAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add("خطا در بارگذاری پکیجها", MudBlazor.Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectPackage(PackageDto package)
|
||||
{
|
||||
_selectedPackage = package;
|
||||
}
|
||||
|
||||
private void BackToList()
|
||||
{
|
||||
_selectedPackage = null;
|
||||
}
|
||||
|
||||
private void SelectPaymentMethod(PaymentMethodType method)
|
||||
{
|
||||
var result = new PackagePurchaseResult(_selectedPackage!, method);
|
||||
MudDialog.Close(DialogResult.Ok(result));
|
||||
}
|
||||
|
||||
private void Cancel() => MudDialog.Cancel();
|
||||
|
||||
private static string GetTileStyle(PackageDto pkg)
|
||||
{
|
||||
return pkg.IsBasePackage
|
||||
? "border:2px solid var(--mud-palette-primary); background:rgba(99,102,241,.04); cursor:pointer;"
|
||||
: "border:2px solid var(--mud-palette-lines-default); background:var(--mud-palette-surface); cursor:pointer;";
|
||||
}
|
||||
|
||||
private static string GetAvatarStyle(PackageDto pkg)
|
||||
{
|
||||
return pkg.IsBasePackage
|
||||
? "background:rgba(99,102,241,.15); color:#6366f1;"
|
||||
: "background:rgba(16,185,129,.12); color:#10b981;";
|
||||
}
|
||||
|
||||
public enum PaymentMethodType { DirectPayment, DayaLoan }
|
||||
public record PackagePurchaseResult(PackageDto Package, PaymentMethodType Method);
|
||||
}
|
||||
@@ -36,14 +36,6 @@
|
||||
Color="Color.Primary"
|
||||
Class="mb-2" />
|
||||
|
||||
<MudTextField Value="ReferralCode"
|
||||
ValueChanged="@((string? v) => ReferralCodeChanged.InvokeAsync(v))"
|
||||
Label="کد معرف (اختیاری)"
|
||||
Variant="Variant.Outlined"
|
||||
Immediate="true"
|
||||
Placeholder="کد معرف را وارد کنید"
|
||||
Class="mb-3" />
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Dense="true" Elevation="0"
|
||||
|
||||
@@ -29,10 +29,6 @@ public partial class PhoneVerifyForm
|
||||
[Parameter] public string? PhoneNumber { get; set; }
|
||||
[Parameter] public int ResendRemaining { get; set; }
|
||||
|
||||
// ── Referral ──
|
||||
[Parameter] public string? ReferralCode { get; set; }
|
||||
[Parameter] public EventCallback<string?> ReferralCodeChanged { get; set; }
|
||||
|
||||
// ── Verify actions ──
|
||||
[Parameter] public EventCallback OnChangePhone { get; set; }
|
||||
[Parameter] public EventCallback OnResendOtp { get; set; }
|
||||
|
||||
@@ -79,7 +79,6 @@ public class AuthService
|
||||
var isSignMainContractStr = claims.FirstOrDefault(c => c.key == "IsSignMainContract").value;
|
||||
var hasPurchasedPackageStr = claims.FirstOrDefault(c => c.key == "HasPurchasedPackage").value;
|
||||
var isClubMemberActiveStr = claims.FirstOrDefault(c => c.key == "IsClubMemberActive").value;
|
||||
var hasAddressStr = claims.FirstOrDefault(c => c.key == "HasAddress").value;
|
||||
|
||||
_userAuthInfo.UserId = long.TryParse(userIdStr, out var userId) ? userId : 0;
|
||||
_userAuthInfo.FirstName = firstName ?? string.Empty;
|
||||
@@ -89,7 +88,6 @@ public class AuthService
|
||||
_userAuthInfo.IsSignMainContract = bool.TryParse(isSignMainContractStr, out var isSignMainContract) && isSignMainContract;
|
||||
_userAuthInfo.HasPurchasedPackage = bool.TryParse(hasPurchasedPackageStr, out var hasPurchased) && hasPurchased;
|
||||
_userAuthInfo.IsClubMemberActive = bool.TryParse(isClubMemberActiveStr, out var isClubActive) && isClubActive;
|
||||
_userAuthInfo.HasAddress = bool.TryParse(hasAddressStr, out var hasAddress) && hasAddress;
|
||||
}
|
||||
|
||||
private async Task<List<(string key, string value)>?> GetTokenClaimsAsync()
|
||||
|
||||
@@ -15,8 +15,6 @@ public class CommissionPayoutDto
|
||||
public string AmountFormatted { get; set; } = string.Empty;
|
||||
public int Status { get; set; }
|
||||
public string DatePersian { get; set; } = string.Empty;
|
||||
public long PackageId { get; set; }
|
||||
public string PackageTitle { get; set; } = string.Empty;
|
||||
|
||||
// Computed properties for UI
|
||||
public string StatusText => Status switch
|
||||
@@ -71,8 +69,6 @@ public class WeeklyBalanceDto
|
||||
public int RightNewMembers { get; set; }
|
||||
public DateTime StartDate { get; set; }
|
||||
public DateTime EndDate { get; set; }
|
||||
public long PackageId { get; set; }
|
||||
public string PackageTitle { get; set; } = string.Empty;
|
||||
|
||||
// Formatted properties
|
||||
public string LeftBalanceFormatted => $"{LeftBalance:N0} ";
|
||||
|
||||
@@ -78,8 +78,7 @@ public class CommissionService
|
||||
long? weekDefinitionId,
|
||||
string? status,
|
||||
int pageNumber,
|
||||
int pageSize,
|
||||
long? packageId = null)
|
||||
int pageSize)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -108,11 +107,6 @@ public class CommissionService
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
if (packageId.HasValue && packageId.Value > 0)
|
||||
{
|
||||
request.PackageId = packageId.Value;
|
||||
}
|
||||
|
||||
var response = await _client.GetMyCommissionPayoutsAsync(request);
|
||||
|
||||
@@ -127,9 +121,7 @@ public class CommissionService
|
||||
TotalAmount = p.TotalAmount,
|
||||
AmountFormatted = p.AmountFormatted,
|
||||
Status = p.Status,
|
||||
DatePersian = p.DatePersian,
|
||||
PackageId = p.PackageId,
|
||||
PackageTitle = p.PackageTitle
|
||||
DatePersian = p.DatePersian
|
||||
}).ToList(),
|
||||
TotalCount = (int)(response.MetaData?.TotalCount ?? 0),
|
||||
PageNumber = pageNumber,
|
||||
@@ -153,7 +145,7 @@ public class CommissionService
|
||||
/// Get weekly balance details for a specific week
|
||||
/// Maps to: CommissionCQ.GetMyWeeklyBalances
|
||||
/// </summary>
|
||||
public async Task<WeeklyBalanceDto> GetMyWeeklyBalanceAsync(long? weekDefinitionId = null, long? packageId = null)
|
||||
public async Task<WeeklyBalanceDto> GetMyWeeklyBalanceAsync(long? weekDefinitionId = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -171,11 +163,6 @@ public class CommissionService
|
||||
{
|
||||
request.WeekDefinitionId=7;
|
||||
}
|
||||
|
||||
if (packageId.HasValue && packageId.Value > 0)
|
||||
{
|
||||
request.PackageId = packageId.Value;
|
||||
}
|
||||
|
||||
var response = await _client.GetMyWeeklyBalancesAsync(request);
|
||||
|
||||
@@ -196,9 +183,7 @@ public class CommissionService
|
||||
LeftNewMembers = balance.LeftLegNewMembers,
|
||||
RightNewMembers = balance.RightLegNewMembers,
|
||||
StartDate = balance.CalculatedAt?.ToDateTime().AddDays(-7) ?? DateTime.Now.AddDays(-7),
|
||||
EndDate = balance.CalculatedAt?.ToDateTime() ?? DateTime.Now,
|
||||
PackageId = balance.PackageId,
|
||||
PackageTitle = balance.PackageTitle
|
||||
EndDate = balance.CalculatedAt?.ToDateTime() ?? DateTime.Now
|
||||
};
|
||||
}
|
||||
|
||||
@@ -242,9 +227,7 @@ public class CommissionService
|
||||
TotalAmount = p.TotalAmount,
|
||||
AmountFormatted = p.AmountFormatted,
|
||||
Status = p.Status,
|
||||
DatePersian = p.DatePersian,
|
||||
PackageId = p.PackageId,
|
||||
PackageTitle = p.PackageTitle
|
||||
DatePersian = p.DatePersian
|
||||
}).ToList();
|
||||
}
|
||||
catch
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
using Grpc.Core;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace FrontOffice.Main.Utilities;
|
||||
|
||||
public static class CreditChargeNavigation
|
||||
{
|
||||
public const long MinChargeAmount = 10_000;
|
||||
public const string ReturnUrlStorageKey = "credit_charge_return_url";
|
||||
private const string InsufficientBalanceMarker = "موجودی کیف پول کافی نیست";
|
||||
|
||||
public static long NormalizeChargeAmount(long shortfall) =>
|
||||
Math.Max(shortfall, MinChargeAmount);
|
||||
|
||||
public static string BuildChargeUrl(long amount, string? returnUrl)
|
||||
{
|
||||
var url = $"{RouteConstants.Profile.ChargeCreditWallet}?amount={amount}";
|
||||
if (IsValidReturnUrl(returnUrl))
|
||||
url += $"&returnUrl={Uri.EscapeDataString(returnUrl!)}";
|
||||
return url;
|
||||
}
|
||||
|
||||
public static bool IsValidReturnUrl(string? url) =>
|
||||
!string.IsNullOrWhiteSpace(url)
|
||||
&& url.StartsWith('/')
|
||||
&& !url.StartsWith("//", StringComparison.Ordinal);
|
||||
|
||||
public static bool IsInsufficientWalletBalanceMessage(string? message) =>
|
||||
message?.Contains(InsufficientBalanceMarker, StringComparison.Ordinal) == true;
|
||||
|
||||
public static bool IsInsufficientWalletBalance(Exception ex) =>
|
||||
ex switch
|
||||
{
|
||||
RpcException rpc => IsInsufficientWalletBalanceMessage(rpc.Status.Detail)
|
||||
|| IsInsufficientWalletBalanceMessage(rpc.Message),
|
||||
_ => IsInsufficientWalletBalanceMessage(ex.Message)
|
||||
};
|
||||
|
||||
public static async Task SaveReturnUrlAsync(IJSRuntime js, string returnUrl)
|
||||
{
|
||||
if (!IsValidReturnUrl(returnUrl)) return;
|
||||
await js.InvokeVoidAsync("sessionStorage.setItem", ReturnUrlStorageKey, returnUrl);
|
||||
}
|
||||
|
||||
public static async Task<string?> GetReturnUrlAsync(IJSRuntime js) =>
|
||||
await js.InvokeAsync<string?>("sessionStorage.getItem", ReturnUrlStorageKey);
|
||||
|
||||
public static async Task ClearReturnUrlAsync(IJSRuntime js) =>
|
||||
await js.InvokeVoidAsync("sessionStorage.removeItem", ReturnUrlStorageKey);
|
||||
}
|
||||
@@ -112,27 +112,6 @@ public class DiscountOrderService
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
public async Task<(bool Success, string Message, long OrderId)> VerifyDiscountOrderPaymentAsync(
|
||||
long orderId, string authority, string status)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _client.CustomerVerifyDiscountOrderPaymentAsync(
|
||||
new CustomerVerifyDiscountOrderPaymentRequest
|
||||
{
|
||||
OrderId = orderId,
|
||||
Authority = authority,
|
||||
Status = status
|
||||
});
|
||||
|
||||
return (response.Success, response.Message, response.OrderId);
|
||||
}
|
||||
catch (Grpc.Core.RpcException ex)
|
||||
{
|
||||
return (false, ex.Status.Detail ?? "خطا در بررسی وضعیت پرداخت", orderId);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<DiscountOrderListResult> GetUserOrdersAsync(int page = 1, int pageSize = 10)
|
||||
{
|
||||
try
|
||||
@@ -219,8 +198,6 @@ public class DiscountOrderService
|
||||
2 => "ارسال شده",
|
||||
3 => "تحویل داده شده",
|
||||
4 => "لغو شده",
|
||||
5 => "آماده تحویل در دفتر",
|
||||
6 => "مرجوع شده",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
|
||||
@@ -230,9 +207,7 @@ public class DiscountOrderService
|
||||
1 => Color.Info,
|
||||
2 => Color.Primary,
|
||||
3 => Color.Success,
|
||||
4 => Color.Dark,
|
||||
5 => Color.Primary,
|
||||
6 => Color.Error,
|
||||
4 => Color.Error,
|
||||
_ => Color.Default
|
||||
};
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ public class DiscountProductService
|
||||
public async Task<DiscountProductListResult> GetProductsAsync(
|
||||
int page = 1, int pageSize = 12,
|
||||
string? search = null, long? categoryId = null,
|
||||
bool? inStock = null, string? sortBy = null)
|
||||
bool? inStock = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -97,8 +97,6 @@ public class DiscountProductService
|
||||
request.CategoryId = categoryId.Value;
|
||||
if (inStock.HasValue)
|
||||
request.InStock = inStock.Value;
|
||||
if (!string.IsNullOrWhiteSpace(sortBy))
|
||||
request.SortBy = sortBy;
|
||||
|
||||
var response = await _productClient.GetDiscountProductsAsync(request);
|
||||
|
||||
@@ -127,9 +125,6 @@ public class DiscountProductService
|
||||
}
|
||||
}
|
||||
|
||||
public Task<DiscountProductListResult> GetTopSellingAsync(int count = 6, bool? inStock = null)
|
||||
=> GetProductsAsync(page: 1, pageSize: count, sortBy: "SaleCount desc", inStock: inStock);
|
||||
|
||||
public async Task<DiscountProductDetail?> GetByIdAsync(long productId)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
namespace FrontOffice.Main.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// هر action ای که نیاز به لاگین دارد را از طریق این سرویس اجرا کنید.
|
||||
/// اگر کاربر لاگین نباشد، مودال ورود نشان داده میشود.
|
||||
/// پس از ورود موفق، action به صورت خودکار اجرا میشود.
|
||||
/// </summary>
|
||||
public class GuestActionGate
|
||||
{
|
||||
private readonly AuthService _authService;
|
||||
private readonly AuthDialogService _authDialogService;
|
||||
|
||||
public GuestActionGate(AuthService authService, AuthDialogService authDialogService)
|
||||
{
|
||||
_authService = authService;
|
||||
_authDialogService = authDialogService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// اگر کاربر لاگین باشد action را اجرا میکند.
|
||||
/// در غیر این صورت مودال لاگین را باز کرده و پس از ورود موفق، action را اجرا میکند.
|
||||
/// </summary>
|
||||
/// <returns>true اگر action اجرا شد، false اگر کاربر لاگین نکرد.</returns>
|
||||
public async Task<bool> RunAsync(Func<Task> action)
|
||||
{
|
||||
if (await _authService.IsAuthenticatedAsync())
|
||||
{
|
||||
await action();
|
||||
return true;
|
||||
}
|
||||
|
||||
await _authDialogService.ShowAuthDialogAsync();
|
||||
|
||||
if (await _authService.IsAuthenticatedAsync())
|
||||
{
|
||||
await action();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -21,23 +21,6 @@ public class NetworkNodeDto
|
||||
/// کد معرف کاربر
|
||||
/// </summary>
|
||||
public string? ReferralCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// نام پکیج موثر (مثلاً پکیج طلایی / نقرهای)
|
||||
/// </summary>
|
||||
public string? PackageName { get; set; }
|
||||
|
||||
/// <summary>آخرین امتیاز پای چپ پکیج طلایی</summary>
|
||||
public int GoldLeftLegTotal { get; set; }
|
||||
|
||||
/// <summary>آخرین امتیاز پای راست پکیج طلایی</summary>
|
||||
public int GoldRightLegTotal { get; set; }
|
||||
|
||||
/// <summary>آخرین امتیاز پای چپ پکیج نقرهای</summary>
|
||||
public int SilverLeftLegTotal { get; set; }
|
||||
|
||||
/// <summary>آخرین امتیاز پای راست پکیج نقرهای</summary>
|
||||
public int SilverRightLegTotal { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -60,16 +43,6 @@ public class FlatNetworkNodeDto
|
||||
/// کد معرف کاربر - فقط برای کاربران فعال در باشگاه نمایش داده شود
|
||||
/// </summary>
|
||||
public string? ReferralCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// نام پکیج موثر (مثلاً پکیج طلایی / نقرهای)
|
||||
/// </summary>
|
||||
public string? PackageName { get; set; }
|
||||
|
||||
public int GoldLeftLegTotal { get; set; }
|
||||
public int GoldRightLegTotal { get; set; }
|
||||
public int SilverLeftLegTotal { get; set; }
|
||||
public int SilverRightLegTotal { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -108,12 +81,7 @@ public class NetworkTreeDto
|
||||
IsClubActive = node.IsClubActive,
|
||||
ActivationWeekNumber = node.ActivationWeekNumber,
|
||||
JoinedAt = node.JoinedAt,
|
||||
ReferralCode = node.ReferralCode,
|
||||
PackageName = node.PackageName,
|
||||
GoldLeftLegTotal = node.GoldLeftLegTotal,
|
||||
GoldRightLegTotal = node.GoldRightLegTotal,
|
||||
SilverLeftLegTotal = node.SilverLeftLegTotal,
|
||||
SilverRightLegTotal = node.SilverRightLegTotal
|
||||
ReferralCode = node.ReferralCode
|
||||
};
|
||||
|
||||
result.Add(flatNode);
|
||||
|
||||
@@ -137,11 +137,6 @@ public class NetworkMembershipService
|
||||
IsActive = node.IsActive,
|
||||
IsClubActive = node.IsClubActive,
|
||||
ReferralCode = node.ReferralCode,
|
||||
PackageName = string.IsNullOrWhiteSpace(node.PackageName) ? null : node.PackageName,
|
||||
GoldLeftLegTotal = node.GoldLeftLegTotal,
|
||||
GoldRightLegTotal = node.GoldRightLegTotal,
|
||||
SilverLeftLegTotal = node.SilverLeftLegTotal,
|
||||
SilverRightLegTotal = node.SilverRightLegTotal,
|
||||
ActivationWeekNumber = node.ActivationWeekDefinitionId?.ToString(),
|
||||
JoinedAt = node.JoinedAt?.ToDateTime(),
|
||||
LeftChild = MapNodeFromProto(node.LeftChild),
|
||||
|
||||
@@ -10,17 +10,8 @@ public record PackageDto(
|
||||
string Title,
|
||||
string Description,
|
||||
string ImageUrl,
|
||||
long Price,
|
||||
long ActivationFee = 0,
|
||||
double DiscountMultiplier = 0,
|
||||
double MagicWalletMultiplier = 0,
|
||||
long MagicWalletMaxDeposit = 0,
|
||||
long MagicWalletMaxCredit = 0,
|
||||
bool IsBasePackage = false,
|
||||
bool SupportsDayaPurchase = false,
|
||||
bool SupportsDirectPurchase = false)
|
||||
long Price)
|
||||
{
|
||||
/// <summary>قیمت فرمتشده به تومان</summary>
|
||||
public string FormattedPrice => string.Format("{0:N0} تومان", Price);
|
||||
}
|
||||
|
||||
@@ -29,7 +20,6 @@ public record PackageDto(
|
||||
/// </summary>
|
||||
public record UserPackageStatusDto(
|
||||
bool HasPurchasedPackage,
|
||||
string? PackageTitle,
|
||||
string? PurchaseMethod, // DayaLoan, DirectPurchase, or null
|
||||
bool IsClubMemberActive,
|
||||
long WalletBalance,
|
||||
@@ -65,15 +55,7 @@ public class PackageService
|
||||
m.Title,
|
||||
m.Description,
|
||||
m.ImagePath,
|
||||
m.Price,
|
||||
m.ActivationFee,
|
||||
m.DiscountMultiplier,
|
||||
m.MagicWalletMultiplier,
|
||||
m.MagicWalletMaxDeposit,
|
||||
m.MagicWalletMaxCredit,
|
||||
m.IsBasePackage,
|
||||
m.SupportsDayaPurchase,
|
||||
m.SupportsDirectPurchase))
|
||||
m.Price))
|
||||
.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -117,32 +99,15 @@ public class PackageService
|
||||
/// <summary>
|
||||
/// Get user's package purchase status
|
||||
/// </summary>
|
||||
public async Task<UserPackageStatusDto> GetUserPackageStatusAsync(CancellationToken ct = default)
|
||||
public Task<UserPackageStatusDto> GetUserPackageStatusAsync(CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _client.GetUserPackageStatusAsync(
|
||||
new GetUserPackageStatusRequest(), cancellationToken: ct);
|
||||
|
||||
return new UserPackageStatusDto(
|
||||
HasPurchasedPackage: response.HasPurchasedPackage,
|
||||
PackageTitle: null,
|
||||
PurchaseMethod: response.PackagePurchaseMethod,
|
||||
IsClubMemberActive: response.IsClubMemberActive,
|
||||
WalletBalance: response.WalletBalance,
|
||||
CanActivateClubMembership: response.CanActivateClubMembership,
|
||||
PurchaseDate: response.LastPurchaseDate?.ToDateTime());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new UserPackageStatusDto(
|
||||
HasPurchasedPackage: false,
|
||||
PackageTitle: null,
|
||||
PurchaseMethod: null,
|
||||
IsClubMemberActive: false,
|
||||
WalletBalance: 0,
|
||||
CanActivateClubMembership: false,
|
||||
PurchaseDate: null);
|
||||
}
|
||||
// TODO: Connect to GetUserPackageStatus RPC when available in FrontOffice.BFF
|
||||
return Task.FromResult(new UserPackageStatusDto(
|
||||
HasPurchasedPackage: false,
|
||||
PurchaseMethod: null,
|
||||
IsClubMemberActive: false,
|
||||
WalletBalance: 0,
|
||||
CanActivateClubMembership: false,
|
||||
PurchaseDate: null));
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user