Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 1m12s

Features merged:
- Discount store UI improvements (100% auto-discount, VAT breakdown)
- PaymentStatus display on orders (Pending/Success/Failed/Refunded)
- Failed payment UX with re-order link
- Product discount badges show actual percent
- Various UI and bug fixes

Production workflow preserved.
This commit is contained in:
masoodafar-web
2026-02-17 00:55:52 +03:30
125 changed files with 8164 additions and 1782 deletions
+37 -31
View File
@@ -1,4 +1,4 @@
name: Build and Deploy
name: Build and Deploy to Kubernetes
on:
push:
@@ -6,63 +6,69 @@ on:
- kub-stage
env:
REGISTRY: gitea-svc:3000
REGISTRY: 194.5.195.53:30080
IMAGE_NAME: admin/frontoffice
K8S_SERVER: 194.5.195.53
jobs:
build:
build-and-deploy:
runs-on: ubuntu-latest
container:
image: docker:latest
image: 194.5.195.53:32082/docker-sshpass:latest
options: --privileged
steps:
- name: Start Docker daemon with insecure registry
- name: Start Docker daemon
run: |
mkdir -p /etc/docker
cat > /etc/docker/daemon.json << 'DAEMON'
{
"insecure-registries": ["194.5.195.53:32500", "194.5.195.53:32082", "git.kbs1.ir", "gitea-svc:3000"],
"dns": ["0.0.0.0"]
"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 &
# بدون اینترنت - فقط از cached images
dockerd &
for i in $(seq 1 30); do
docker info >/dev/null 2>&1 && break || sleep 2
# Wait up to 3 minutes for Docker to be ready
for i in $(seq 1 90); do
if docker info >/dev/null 2>&1; then
echo "✅ Docker daemon is ready (attempt $i)"
docker version
break
else
echo "⏳ Waiting for Docker daemon... (attempt $i/90)"
sleep 2
fi
done
docker info
# بررسی وجود cached images
echo "📊 Checking cached base images..."
docker images | grep -E "mcr.microsoft.com|nginx" || echo "⚠️ WARNING: Base images not cached!"
# 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 .
git log -1 --format="%H %s"
- name: Login to Docker registries
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 194.5.195.53:32082 -u admin --password-stdin
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
- name: Build Docker Image
run: |
cd src
# استفاده از cached base images (بدون دانلود)
DOCKER_BUILDKIT=0 docker build -f FrontOffice.Main/Dockerfile \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
.
DOCKER_BUILDKIT=0 docker build --network host -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest -f FrontOffice.Main/Dockerfile .
- name: Push to Registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- name: Build and Push Complete
- name: Deploy to Kubernetes
run: |
echo "🎉 Build and push completed successfully!"
echo "📦 Image pushed to: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}"
echo "📦 Latest tag: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest"
echo ""
echo "🚀 To deploy manually on server:"
echo "kubectl set image deployment/frontoffice frontoffice=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}"
export SSHPASS="${{ secrets.SERVER_PASSWORD }}"
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
kubectl rollout restart deployment/frontoffice
kubectl rollout status deployment/frontoffice --timeout=180s
"
echo "✅ Deployed!"
-1
View File
@@ -1 +0,0 @@
# Test multi-remote push Sun Dec 7 19:09:00 UTC 2025
+273
View File
@@ -0,0 +1,273 @@
# 📋 تاریخچه تغییرات FrontOffice — کارا بازار سلامت
> **آخرین بروزرسانی**: بهمن ۱۴۰۴ (فوریه ۲۰۲۶)
> **فریمورک**: Blazor Server + MudBlazor 8.14 + .NET 9
---
## 🔖 نسخه ۲.۶.۰ — بهمن ۱۴۰۴ (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) |
+542
View File
@@ -0,0 +1,542 @@
# 🎨 پلن جامع یکپارچه‌سازی UI/UX — کارا بازار سلامت
> **تاریخ**: بهمن ۱۴۰۴
> **وضعیت**: ✅ **تمام ۶ فاز + فاز ۷ (ناوبری و امنیت) تکمیل شد**
> **هدف**: یکپارچه‌سازی طراحی 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 در هر فاز)
+64 -35
View File
@@ -5,22 +5,29 @@ using Grpc.Net.Client;
using MudBlazor.Services;
using System.Text.Json;
using System.Text.Json.Serialization;
using FrontOffice.BFF.Category.Protobuf.Protos.Category;
using FrontOffice.BFF.Package.Protobuf.Protos.Package;
using FrontOffice.BFF.Transaction.Protobuf.Protos.Transaction;
using FrontOffice.BFF.User.Protobuf.Protos.User;
using FrontOffice.BFF.UserAddress.Protobuf.Protos.UserAddress;
using FrontOffice.BFF.UserOrder.Protobuf.Protos.UserOrder;
using FrontOffice.BFF.UserWallet.Protobuf.Protos.UserWallet;
using FrontOffice.BFF.ShopingCart.Protobuf.Protos.ShopingCart;
// New Proto imports
using FrontOffice.BFF.ClubMembership.Protobuf.Protos.ClubMembership;
using FrontOffice.BFF.Commission.Protobuf.Protos.Commission;
using FrontOffice.BFF.NetworkMembership.Protobuf.Protos.NetworkMembership;
using FrontOffice.BFF.DiscountShop.Protobuf.Protos.DiscountShop;
using FrontOffice.BFF.City.Protobuf;
using FrontOffice.BFF.Configuration.Protobuf.Protos.Configuration;
using FrontOffice.BFF.Configuration.Protobuf.Protos.AppVersion;
// Updated imports to use CMS protobuf instead of FrontOffice.BFF
using CMSMicroservice.Protobuf.Protos.Category;
using CMSMicroservice.Protobuf.Protos.City;
using CMSMicroservice.Protobuf.Protos.Package;
using CMSMicroservice.Protobuf.Protos.Products;
using CMSMicroservice.Protobuf.Protos.Transactions;
using CMSMicroservice.Protobuf.Protos.User;
using CMSMicroservice.Protobuf.Protos.UserCarts;
using CMSMicroservice.Protobuf.Protos.UserOrder;
using CMSMicroservice.Protobuf.Protos.UserWallet;
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.BlogPost;
using CMSMicroservice.Protobuf.Protos.BlogCategory;
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
using CMSMicroservice.Protobuf.Protos.DiscountCategory;
using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
using FrontOffice.Main.Utilities;
namespace Microsoft.Extensions.DependencyInjection;
@@ -64,6 +71,18 @@ public static class ConfigureServices
services.AddScoped<CommissionService>();
// App Version Service for cache invalidation
services.AddScoped<AppVersionService>();
// Main Service for core functionality
services.AddScoped<MainService>();
// Site Page service for dynamic content (About, Contact)
services.AddScoped<SitePageService>();
// Blog Post service for landing page latest posts & blog pages
services.AddScoped<BlogPostService>();
// Blog Category service for blog sidebar filters
services.AddScoped<BlogCategoryService>();
// Discount Store services
services.AddScoped<DiscountProductService>();
services.AddScoped<DiscountCartService>();
services.AddScoped<DiscountOrderService>();
// Device detection: very light, dependency-free
services.AddTransient<IDeviceDetector, DeviceDetector>();
// PDF generation (Chromium only)
@@ -95,25 +114,35 @@ public static class ConfigureServices
};
});
// Register gRPC clients with authentication
services.AddScoped(CreateAuthenticatedClient<PackageContract.PackageContractClient>);
services.AddScoped(CreateAuthenticatedClient<UserContract.UserContractClient>);
services.AddScoped(CreateAuthenticatedClient<UserAddressContract.UserAddressContractClient>);
services.AddScoped(CreateAuthenticatedClient<UserOrderContract.UserOrderContractClient>);
services.AddScoped(CreateAuthenticatedClient<UserWalletContract.UserWalletContractClient>);
services.AddScoped(CreateAuthenticatedClient<CategoryContract.CategoryContractClient>);
// Products gRPC
services.AddScoped(CreateAuthenticatedClient<FrontOffice.BFF.Products.Protobuf.Protos.Products.ProductsContract.ProductsContractClient>);
services.AddScoped(CreateAuthenticatedClient<TransactionContract.TransactionContractClient>);
services.AddScoped(CreateAuthenticatedClient<ShopingCartContract.ShopingCartContractClient>);
// New gRPC clients for Club, Network, Commission, DiscountShop
services.AddScoped(CreateAuthenticatedClient<ClubMembershipContract.ClubMembershipContractClient>);
services.AddScoped(CreateAuthenticatedClient<CommissionContract.CommissionContractClient>);
services.AddScoped(CreateAuthenticatedClient<ConfigurationContract.ConfigurationContractClient>);
services.AddScoped(CreateAuthenticatedClient<NetworkMembershipContract.NetworkMembershipContractClient>);
services.AddScoped(CreateAuthenticatedClient<DiscountShopContract.DiscountShopContractClient>);
services.AddScoped(CreateAuthenticatedClient<CityContract.CityContractClient>);
services.AddScoped(CreateAuthenticatedClient<AppVersionContract.AppVersionContractClient>);
// Register gRPC clients with authentication - Updated for CMS
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.Package.PackageContract.PackageContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.User.UserContract.UserContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.UserOrder.UserOrderContract.UserOrderContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.UserWallet.UserWalletContract.UserWalletContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.Category.CategoryContract.CategoryContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.Products.ProductsContract.ProductsContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.Transactions.TransactionsContract.TransactionsContractClient>);
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.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>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.NetworkMembership.NetworkMembershipContract.NetworkMembershipContractClient>);
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<CMSMicroservice.Protobuf.Protos.BlogPost.BlogPostContract.BlogPostContractClient>);
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.BlogCategory.BlogCategoryContract.BlogCategoryContractClient>);
// Image Resolver Service
services.AddScoped(CreateAuthenticatedClient<CMSMicroservice.Protobuf.Protos.ImageResolver.ImageResolverContract.ImageResolverContractClient>);
services.AddScoped<ImageCacheService>();
// Discount Store gRPC clients
services.AddScoped(CreateAuthenticatedClient<DiscountProductContract.DiscountProductContractClient>);
services.AddScoped(CreateAuthenticatedClient<DiscountCategoryContract.DiscountCategoryContractClient>);
services.AddScoped(CreateAuthenticatedClient<DiscountShoppingCartContract.DiscountShoppingCartContractClient>);
services.AddScoped(CreateAuthenticatedClient<DiscountOrderContract.DiscountOrderContractClient>);
return services;
}
+3 -23
View File
@@ -10,29 +10,9 @@
<ItemGroup>
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
<PackageReference Include="Foursat.FrontOffice.BFF.City.Protobuf" Version="0.0.2" />
<PackageReference Include="Foursat.FrontOffice.BFF.ClubMembership.Protobuf" Version="0.0.4" />
<PackageReference Include="Foursat.FrontOffice.BFF.Commission.Protobuf" Version="0.0.6" />
<PackageReference Include="Foursat.FrontOffice.BFF.Configuration.Protobuf" Version="0.0.4" />
<!-- <PackageReference Include="Foursat.FrontOffice.BFF.ClubMembership.Protobuf" Version="0.0.3" /> -->
<!-- <PackageReference Include="Foursat.FrontOffice.BFF.Commission.Protobuf" Version="0.0.2" /> -->
<PackageReference Include="Foursat.FrontOffice.BFF.DiscountShop.Protobuf" Version="0.0.3" />
<PackageReference Include="Foursat.FrontOffice.BFF.NetworkMembership.Protobuf" Version="0.0.5" />
<PackageReference Include="Foursat.FrontOffice.BFF.Package.Protobuf" Version="0.0.114" />
<!-- <PackageReference Include="Foursat.FrontOffice.BFF.NetworkMembership.Protobuf" Version="0.0.2" />-->
<!-- <PackageReference Include="Foursat.FrontOffice.BFF.Package.Protobuf" Version="0.0.113" /> -->
<PackageReference Include="Foursat.FrontOffice.BFF.Products.Protobuf" Version="0.0.18" />
<PackageReference Include="Foursat.FrontOffice.BFF.Transaction.Protobuf" Version="0.0.113" />
<PackageReference Include="Foursat.FrontOffice.BFF.Category.Protobuf" Version="0.0.14" />
<PackageReference Include="Foursat.FrontOffice.BFF.User.Protobuf" Version="0.0.118" />
<!-- <PackageReference Include="Foursat.FrontOffice.BFF.User.Protobuf" Version="0.0.117" /> -->
<PackageReference Include="Foursat.FrontOffice.BFF.UserAddress.Protobuf" Version="0.0.116" />
<!-- <PackageReference Include="Foursat.FrontOffice.BFF.UserOrder.Protobuf" Version="0.0.115" />-->
<PackageReference Include="Foursat.FrontOffice.BFF.ShopingCart.Protobuf" Version="0.0.17" />
<PackageReference Include="Foursat.FrontOffice.BFF.UserOrder.Protobuf" Version="0.0.116" />
<PackageReference Include="Foursat.FrontOffice.BFF.UserWallet.Protobuf" Version="0.0.16" />
<!-- UserWallet moved to ProjectReference for latest proto with WeekDefinitionId -->
<!-- <PackageReference Include="Foursat.FrontOffice.BFF.UserWallet.Protobuf" Version="0.0.15" /> -->
<!-- Replace all FrontOffice.BFF protobuf packages with CMS protobuf -->
<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" />
+1 -12
View File
@@ -2,24 +2,13 @@
<configuration>
<packageSources>
<clear />
<!-- Nexus as primary source (proxies nuget.org + caches packages) -->
<!-- Nexus (hosts FourSat packages + proxies nuget.org) -->
<add key="Nexus" value="http://194.5.195.53:32081/repository/nuget-all/index.json" allowInsecureConnections="true" />
<!-- Backup: Direct Gitea registries -->
<add key="FourSat" value="https://git.afrino.co/api/packages/FourSat/nuget/index.json" />
<add key="Afrino" value="https://git.afrino.co/api/packages/Afrino/nuget/index.json" />
</packageSources>
<packageSourceCredentials>
<Nexus>
<add key="Username" value="admin" />
<add key="ClearTextPassword" value="87zH26nbqT" />
</Nexus>
<FourSat>
<add key="Username" value="masoud" />
<add key="ClearTextPassword" value="87zH26nbqT" />
</FourSat>
<Afrino>
<add key="Username" value="systemuser" />
<add key="ClearTextPassword" value="sZSA7PTiv3pUSQZ" />
</Afrino>
</packageSourceCredentials>
</configuration>
+261 -173
View File
@@ -1,192 +1,280 @@
@attribute [Route(RouteConstants.About.Index)]
@inject NavigationManager Navigation
@inject SitePageService SitePageService
<PageTitle>درباره ما | کارا بازار سلامت</PageTitle>
<!-- Hero Section -->
<section class="about-hero-section py-16">
<MudContainer MaxWidth="MaxWidth.Large">
<MudGrid Justify="Justify.Center" Spacing="4">
<MudItem xs="12" md="6">
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled" Class="mb-2">درباره کارا بازار سلامت</MudChip>
<MudStack Spacing="3">
<MudText Typo="Typo.h2" Class="mb-3">
پلتفرم هوشمند تیم‌سازی و مدیریت فروش
</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mb-4">
ما با ارائه ابزارهای نوآورانه، به کسب‌وکارها کمک می‌کنیم تا تیم‌های فروش خود را گسترش دهند و به صورت کارآمد مدیریت کنند.
</MudText>
<MudStack Row="true" Spacing="2">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
OnClick="() => Navigation.NavigateTo(RouteConstants.Main.MainPage)">
شروع کنید
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Large"
OnClick="ScrollToContact">
تماس با ما
</MudButton>
</MudStack>
</MudStack>
</MudItem>
<MudItem xs="12" md="6">
<MudPaper Class="pa-8 rounded-xl" Style="background: radial-gradient(600px 280px at 120% 0, #daccff 0, transparent 60%), radial-gradient(600px 280px at -10% 100%, #ffe2f2 0, transparent 60%), linear-gradient(180deg, #fff, #fbfaff);">
<MudImage Src="images/team-work.jpg"
Alt="جلسه تیم فروش"
ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Style="width:100%"
Class="rounded-xl" />
</MudPaper>
</MudItem>
</MudGrid>
@if (_loading)
{
<MudContainer MaxWidth="MaxWidth.Large" Class="py-16">
<LoadingState />
</MudContainer>
</section>
<MudStack Spacing="8">
<!-- Mission & Vision -->
<section class="py-12">
}
else
{
<!-- Hero Section -->
<section class="about-hero-section py-16">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">چشم‌انداز و مأموریت ما</MudText>
<MudGrid Spacing="4" Justify="Justify.Center">
<MudGrid Justify="Justify.Center" Spacing="4">
<MudItem xs="12" md="6">
<MudPaper Elevation="3" 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">چشم‌انداز</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
تبدیل شدن به پیشروترین پلتفرم مدیریت تیم‌های فروش در منطقه، با ارائه راهکارهای هوشمند و کاربرپسند برای کسب‌وکارهای کوچک و بزرگ.
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled" Class="mb-2">درباره کارا بازار سلامت</MudChip>
<MudStack Spacing="3">
<MudText Typo="Typo.h2" Class="mb-3">
@(_page?.HeroTitle ?? "پلتفرم هوشمند تیم‌سازی و مدیریت فروش")
</MudText>
</MudPaper>
<MudText Typo="Typo.body1" Class="mud-text-secondary mb-4">
@(_page?.HeroSubtitle ?? "ما با ارائه ابزارهای نوآورانه، به کسب‌وکارها کمک می‌کنیم تا تیم‌های فروش خود را گسترش دهند و به صورت کارآمد مدیریت کنند.")
</MudText>
<MudStack Row="true" Spacing="2" Class="flex-wrap">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
OnClick="() => Navigation.NavigateTo(RouteConstants.Main.MainPage)">
شروع کنید
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Large"
OnClick="() => Navigation.NavigateTo(RouteConstants.Contact.Index)">
تماس با ما
</MudButton>
</MudStack>
</MudStack>
</MudItem>
<MudItem xs="12" md="6">
<MudPaper Elevation="3" 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">مأموریت</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
توانمندسازی کسب‌وکارها از طریق فناوری‌های نوین، ساده‌سازی فرآیندهای پیچیده و ایجاد کارا بازار سلامت‌های جدید برای رشد و توسعه پایدار.
</MudText>
<MudPaper Class="pa-8 rounded-xl" Style="background: radial-gradient(600px 280px at 120% 0, #daccff 0, transparent 60%), radial-gradient(600px 280px at -10% 100%, #ffe2f2 0, transparent 60%), linear-gradient(180deg, #fff, #fbfaff);">
<AppImage Path="@(_page?.HeroImagePath ?? "images/team-work.jpg")"
Alt="جلسه تیم فروش"
ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Style="width:100%"
Class="rounded-xl" />
</MudPaper>
</MudItem>
</MudGrid>
</MudContainer>
</section>
<!-- Company Values -->
<section class="py-12 bg-grey-50">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">ارزش‌های ما</MudText>
<MudGrid Spacing="3" Justify="Justify.Center">
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.Security" Size="Size.Large" Color="Color.Info" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">صداقت و شفافیت</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
در تمامی تعاملات خود با مشتریان و همکاران، صداقت و شفافیت را سرلوحه کار خود قرار داده‌ایم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.Lightbulb" Size="Size.Large" Color="Color.Warning" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">نوآوری</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
همواره در پی یافتن راهکارهای جدید و بهبود فرآیندها هستیم تا بهترین تجربه را برای کاربران فراهم کنیم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.Group" Size="Size.Large" Color="Color.Success" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">مشتری‌مداری</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
رضایت و موفقیت مشتریان اولویت اصلی ما است و تمامی تصمیمات خود را بر اساس نیازهای آنها می‌گیریم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.Verified" Size="Size.Large" Color="Color.Primary" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">کیفیت</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
به کیفیت محصولات و خدمات خود افتخار می‌کنیم و همواره استانداردهای بالایی را رعایت می‌کنیم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.AccessTime" Size="Size.Large" Color="Color.Secondary" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">پاسخگویی</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
به سرعت به نیازهای مشتریان پاسخ می‌دهیم و پشتیبانی ۲۴ ساعته را فراهم کرده‌ایم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.EmergencyRecording" Size="Size.Large" Color="Color.Error" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">پایداری</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
به دنبال ایجاد ارزش بلندمدت برای مشتریان و جامعه هستیم و به توسعه پایدار متعهد هستیم.
</MudText>
</MudPaper>
</MudItem>
</MudGrid>
</MudContainer>
</section>
<MudStack Spacing="8">
<!-- Team Section -->
<section class="pb-12 bg-grey-50">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">تیم ما</MudText>
<MudGrid Spacing="4" Justify="Justify.Center">
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="3" Class="pa-4 text-center">
<MudAvatar Size="Size.Large" Class="mb-3">
<MudImage ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Src="images/avatar3.jpg" />
</MudAvatar>
<MudText Typo="Typo.h6" Class="mb-1">علی رضایی</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2">مدیرعامل</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
بیش از ۱۰ سال تجربه در حوزه فناوری و مدیریت کسب‌وکار
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="3" Class="pa-4 text-center">
<MudAvatar Size="Size.Large" Class="mb-3">
<MudImage ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Src="images/avatar4.jpg" />
</MudAvatar>
<MudText Typo="Typo.h6" Class="mb-1">مریم احمدی</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2">مدیر محصول</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
متخصص در طراحی تجربه کاربری و توسعه محصول
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="3" Class="pa-4 text-center">
<MudAvatar Size="Size.Large" Class="mb-3">
<MudImage ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Src="images/avatar5.jpg" />
</MudAvatar>
<MudText Typo="Typo.h6" Class="mb-1">حسن کریمی</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2">مدیر فنی</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
مهندس نرم‌افزار با تخصص در معماری سیستم‌های توزیع‌شده
</MudText>
</MudPaper>
</MudItem>
</MudGrid>
</MudContainer>
</section>
</MudStack>
<!-- Mission & Vision -->
<section class="py-12">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">چشم‌انداز و مأموریت ما</MudText>
<MudGrid Spacing="4" Justify="Justify.Center">
<MudItem xs="12" md="6">
<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">
@(_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">
@(_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>
</MudContainer>
</section>
<!-- Company Values -->
<section class="py-12 bg-grey-50">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">ارزش‌های ما</MudText>
<MudGrid Spacing="3" Justify="Justify.Center">
@if (_valueSections.Any())
{
@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="@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.HtmlContent))
{
<MudText Typo="Typo.body2" Class="mud-text-secondary">
@((MarkupString)value.HtmlContent)
</MudText>
}
</MudPaper>
</MudItem>
}
}
else
{
@* Fallback: hardcoded values *@
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.Security" Size="Size.Large" Color="Color.Info" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">صداقت و شفافیت</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
در تمامی تعاملات خود با مشتریان و همکاران، صداقت و شفافیت را سرلوحه کار خود قرار داده‌ایم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.Lightbulb" Size="Size.Large" Color="Color.Warning" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">نوآوری</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
همواره در پی یافتن راهکارهای جدید و بهبود فرآیندها هستیم تا بهترین تجربه را برای کاربران فراهم کنیم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.Group" Size="Size.Large" Color="Color.Success" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">مشتری‌مداری</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
رضایت و موفقیت مشتریان اولویت اصلی ما است و تمامی تصمیمات خود را بر اساس نیازهای آنها می‌گیریم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.Verified" Size="Size.Large" Color="Color.Primary" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">کیفیت</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
به کیفیت محصولات و خدمات خود افتخار می‌کنیم و همواره استانداردهای بالایی را رعایت می‌کنیم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.AccessTime" Size="Size.Large" Color="Color.Secondary" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">پاسخگویی</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
به سرعت به نیازهای مشتریان پاسخ می‌دهیم و پشتیبانی ۲۴ ساعته را فراهم کرده‌ایم.
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.EmergencyRecording" Size="Size.Large" Color="Color.Error" Class="mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">پایداری</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
به دنبال ایجاد ارزش بلندمدت برای مشتریان و جامعه هستیم و به توسعه پایدار متعهد هستیم.
</MudText>
</MudPaper>
</MudItem>
}
</MudGrid>
</MudContainer>
</section>
<!-- Team Section -->
<section class="pb-12 bg-grey-50">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">تیم ما</MudText>
<MudGrid Spacing="4" Justify="Justify.Center">
@if (_teamSections.Any())
{
@foreach (var member in _teamSections)
{
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 text-center">
<MudAvatar Size="Size.Large" Class="mb-3">
@if (!string.IsNullOrWhiteSpace(member.ImagePath))
{
<AppImage Path="@member.ImagePath"
ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center" />
}
else
{
<MudIcon Icon="@Icons.Material.Filled.Person" />
}
</MudAvatar>
<MudText Typo="Typo.h6" Class="mb-1">@member.Title</MudText>
@if (!string.IsNullOrWhiteSpace(member.Subtitle))
{
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2">@member.Subtitle</MudText>
}
@if (!string.IsNullOrWhiteSpace(member.HtmlContent))
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">
@((MarkupString)member.HtmlContent)
</MudText>
}
</MudPaper>
</MudItem>
}
}
else
{
@* Fallback: hardcoded team *@
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 text-center">
<MudAvatar Size="Size.Large" Class="mb-3">
<MudImage ObjectFit="ObjectFit.Cover" ObjectPosition="ObjectPosition.Center" Src="images/avatar3.jpg" />
</MudAvatar>
<MudText Typo="Typo.h6" Class="mb-1">علی رضایی</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2">مدیرعامل</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
بیش از ۱۰ سال تجربه در حوزه فناوری و مدیریت کسب‌وکار
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 text-center">
<MudAvatar Size="Size.Large" Class="mb-3">
<MudImage ObjectFit="ObjectFit.Cover" ObjectPosition="ObjectPosition.Center" Src="images/avatar4.jpg" />
</MudAvatar>
<MudText Typo="Typo.h6" Class="mb-1">مریم احمدی</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2">مدیر محصول</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
متخصص در طراحی تجربه کاربری و توسعه محصول
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 text-center">
<MudAvatar Size="Size.Large" Class="mb-3">
<MudImage ObjectFit="ObjectFit.Cover" ObjectPosition="ObjectPosition.Center" Src="images/avatar5.jpg" />
</MudAvatar>
<MudText Typo="Typo.h6" Class="mb-1">حسن کریمی</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2">مدیر فنی</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
مهندس نرم‌افزار با تخصص در معماری سیستم‌های توزیع‌شده
</MudText>
</MudPaper>
</MudItem>
}
</MudGrid>
</MudContainer>
</section>
</MudStack>
}
+31 -2
View File
@@ -1,8 +1,37 @@
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages;
public partial class About
{
private void ScrollToContact()
private bool _loading = true;
private SitePageDto? _page;
private SitePageSectionDto? _visionSection;
private SitePageSectionDto? _missionSection;
private List<SitePageSectionDto> _valueSections = new();
private List<SitePageSectionDto> _teamSections = new();
protected override async Task OnInitializedAsync()
{
// TODO: Implement smooth scroll to contact section
try
{
_page = await SitePageService.GetByKeyAsync("about");
if (_page != null)
{
_visionSection = _page.GetSection("vision");
_missionSection = _page.GetSection("mission");
_valueSections = _page.GetSections("value-").ToList();
_teamSections = _page.GetSections("team-").ToList();
}
}
catch
{
// Fallback: page remains null → hardcoded content will render
}
finally
{
_loading = false;
}
}
}
+172
View File
@@ -0,0 +1,172 @@
@attribute [Route(RouteConstants.Blog.Index)]
<PageTitle>بلاگ | کارا بازار سلامت</PageTitle>
@* ═══════════════════════════════════════════════
BLOG HEADER
═══════════════════════════════════════════════ *@
<section class="blog-hero">
<MudContainer MaxWidth="MaxWidth.Large">
<div class="text-center">
<MudText Typo="Typo.h3" Class="dash-hero-name">بلاگ</MudText>
<MudText Typo="Typo.body1" Class="dash-hero-sub mt-2">
آخرین مطالب، آموزش‌ها و اخبار کارا بازار سلامت
</MudText>
</div>
</MudContainer>
</section>
@* ═══════════════════════════════════════════════
SEARCH + FILTERS
═══════════════════════════════════════════════ *@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudPaper Elevation="0" Class="pa-4 rounded-xl mb-6" Style="border:1px solid var(--mud-palette-divider);">
<MudGrid Spacing="3" AlignItems="AlignItems.Center">
<MudItem xs="12" sm="6" md="5">
<MudTextField @bind-Value="_searchTerm"
Placeholder="جستجو در مقالات..."
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search"
Immediate="false"
OnKeyUp="OnSearchKeyUp"
Class="rounded-lg" />
</MudItem>
<MudItem xs="12" sm="6" md="5">
<MudSelect T="long?" Value="_selectedCategoryId"
Placeholder="همه دسته‌بندی‌ها"
Variant="Variant.Outlined"
Class="rounded-lg"
Clearable="true"
ValueChanged="OnCategoryChanged">
@foreach (var cat in _categories)
{
<MudSelectItem T="long?" Value="@cat.Id">
@cat.Title
@if (cat.PostCount > 0)
{
<MudText Typo="Typo.caption" Class="mud-text-secondary mr-2" Style="display:inline;">(@cat.PostCount)</MudText>
}
</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="12" md="2">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
FullWidth="true"
Class="rounded-lg"
StartIcon="@Icons.Material.Filled.Search"
OnClick="SearchPosts">
جستجو
</MudButton>
</MudItem>
</MudGrid>
</MudPaper>
@* ═══════════════════════════════════════════════
BLOG POSTS GRID
═══════════════════════════════════════════════ *@
@if (_isLoading)
{
<LoadingState />
}
else if (!_posts.Any())
{
<MudPaper Elevation="0" Class="pa-12 text-center rounded-xl" Style="border:1px dashed var(--mud-palette-divider);">
<MudIcon Icon="@Icons.Material.Outlined.Article" Size="Size.Large" Class="mud-text-secondary mb-3" Style="font-size:3.5rem;" />
<MudText Typo="Typo.h6" Class="mud-text-secondary">مقاله‌ای یافت نشد</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-1">
@if (!string.IsNullOrWhiteSpace(_searchTerm) || _selectedCategoryId.HasValue)
{
<span>فیلترها را تغییر دهید یا عبارت دیگری جستجو کنید.</span>
}
else
{
<span>به‌زودی مقالات جدید منتشر خواهد شد.</span>
}
</MudText>
</MudPaper>
}
else
{
<MudGrid Spacing="4">
@foreach (var post in _posts)
{
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="1" Class="rounded-xl blog-card-v2 cursor-pointer" Style="overflow:hidden;"
@onclick="() => NavigateToPost(post.Slug)">
@* Thumbnail *@
<div class="blog-thumb" style="height:200px; background:#f0f0f0;">
@if (!string.IsNullOrWhiteSpace(post.ThumbnailUrl))
{
<AppImage Path="@post.ThumbnailUrl"
Alt="@post.Title"
ObjectFit="ObjectFit.Cover"
Style="width:100%; height:200px;" />
}
else
{
<div style="height:200px; display:flex; align-items:center; justify-content:center; background:linear-gradient(135deg,#6366f1,#a78bfa);">
<MudIcon Icon="@Icons.Material.Filled.Article" Size="Size.Large" Style="color:rgba(255,255,255,.4);" />
</div>
}
</div>
@* Content *@
<div class="pa-4">
@if (post.Categories.Any())
{
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined"
Color="Color.Primary" Class="mb-2">
@post.Categories.First().Title
</MudChip>
}
<MudText Typo="Typo.h6" Class="mb-1 blog-title-clamp">
@post.Title
</MudText>
@if (!string.IsNullOrWhiteSpace(post.Summary))
{
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-3 blog-summary-clamp">
@post.Summary
</MudText>
}
<MudStack Row="true" AlignItems="AlignItems.Center" Class="mt-auto">
@if (post.PublishedAt.HasValue)
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">
<MudIcon Icon="@Icons.Material.Filled.CalendarToday" Size="Size.Small" Class="ml-1" />
@post.PublishedAt.Value.ToString("yyyy/MM/dd")
</MudText>
}
<MudSpacer />
<MudText Typo="Typo.caption" Class="mud-text-secondary">
<MudIcon Icon="@Icons.Material.Filled.Visibility" Size="Size.Small" Class="ml-1" />
@post.ViewCount.ToString("N0")
</MudText>
</MudStack>
</div>
</MudPaper>
</MudItem>
}
</MudGrid>
@* ── Pagination ── *@
@if (_totalPages > 1)
{
<div class="d-flex justify-center mt-8">
<MudPagination Count="@_totalPages"
Selected="@_currentPage"
SelectedChanged="OnPageChanged"
Color="Color.Primary"
Variant="Variant.Outlined"
Class="rounded-lg" />
</div>
}
}
</MudContainer>
@@ -0,0 +1,91 @@
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
namespace FrontOffice.Main.Pages.Blog;
public partial class Index
{
[Inject] private BlogPostService BlogPostService { get; set; } = default!;
[Inject] private BlogCategoryService BlogCategoryService { get; set; } = default!;
// ── State ──
private List<BlogPostCardDto> _posts = new();
private List<BlogCategoryDto> _categories = new();
private string? _searchTerm;
private long? _selectedCategoryId;
private int _currentPage = 1;
private int _totalPages;
private bool _isLoading = true;
private const int PageSize = 9;
protected override async Task OnInitializedAsync()
{
// Load categories and posts in parallel
var categoriesTask = BlogCategoryService.GetActiveCategoriesAsync();
var postsTask = LoadPostsAsync();
_categories = await categoriesTask;
await postsTask;
}
private async Task LoadPostsAsync()
{
_isLoading = true;
StateHasChanged();
try
{
var result = await BlogPostService.GetPublishedPostsAsync(
page: _currentPage,
pageSize: PageSize,
search: _searchTerm,
categoryId: _selectedCategoryId);
_posts = result.Posts;
_totalPages = result.TotalPages;
}
catch
{
_posts = new();
_totalPages = 0;
}
finally
{
_isLoading = false;
StateHasChanged();
}
}
private async Task SearchPosts()
{
_currentPage = 1;
await LoadPostsAsync();
}
private async Task OnSearchKeyUp(KeyboardEventArgs e)
{
if (e.Key == "Enter")
{
await SearchPosts();
}
}
private async Task OnCategoryChanged(long? categoryId)
{
_selectedCategoryId = categoryId;
_currentPage = 1;
await LoadPostsAsync();
}
private async Task OnPageChanged(int page)
{
_currentPage = page;
await LoadPostsAsync();
}
private void NavigateToPost(string slug)
{
Navigation.NavigateTo($"/blog/{slug}");
}
}
+122
View File
@@ -0,0 +1,122 @@
@page "/blog/{Slug}"
<PageTitle>@(_post?.Title ?? "مقاله") | بلاگ</PageTitle>
@if (_isLoading)
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-12">
<LoadingState />
</MudContainer>
}
else if (_post == null)
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-12">
<EmptyState Icon="@Icons.Material.Filled.SearchOff"
Title="مقاله یافت نشد"
Description="این مقاله حذف شده یا آدرس اشتباه است."
ActionText="بازگشت به بلاگ"
ActionHref="/blog" />
</MudContainer>
}
else
{
@* ═══ Featured Image Header ═══ *@
@if (!string.IsNullOrWhiteSpace(_post.FeaturedImagePath))
{
<div class="blog-detail-hero">
<AppImage Path="@_post.FeaturedImagePath"
Alt="@_post.Title"
ObjectFit="ObjectFit.Cover"
Style="width:100%; height:100%;" />
<div class="blog-detail-hero-overlay"></div>
</div>
}
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
@* ── Breadcrumb ── *@
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center" Class="mb-4">
<MudLink Href="/" Typo="Typo.caption" Class="mud-text-secondary">خانه</MudLink>
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Size="Size.Small" Class="mud-text-secondary" />
<MudLink Href="/blog" Typo="Typo.caption" Class="mud-text-secondary">بلاگ</MudLink>
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Size="Size.Small" Class="mud-text-secondary" />
<MudText Typo="Typo.caption" Class="mud-text-secondary">@_post.Title</MudText>
</MudStack>
@* ── Article Header ── *@
<MudPaper Elevation="0" Class="pa-0 mb-6">
@if (_post.Categories.Any())
{
<MudStack Row="true" Spacing="1" Class="mb-3 flex-wrap">
@foreach (var cat in _post.Categories)
{
<MudChip T="string" Size="Size.Small" Variant="Variant.Filled"
Color="Color.Primary" Class="rounded-pill"
OnClick="() => NavigateToCategoryFilter(cat.Id)">
@cat.Title
</MudChip>
}
</MudStack>
}
<MudText Typo="Typo.h4" Class="mb-3 fw-bold" Style="line-height:1.6;">
@_post.Title
</MudText>
@if (!string.IsNullOrWhiteSpace(_post.Summary))
{
<MudText Typo="Typo.subtitle1" Class="mud-text-secondary mb-4" Style="line-height:1.8;">
@_post.Summary
</MudText>
}
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center" Class="mb-4 flex-wrap">
@if (_post.PublishedAt.HasValue)
{
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.CalendarToday" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.body2" Class="mud-text-secondary">
@_post.PublishedAt.Value.ToString("yyyy/MM/dd")
</MudText>
</MudStack>
}
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Visibility" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.body2" Class="mud-text-secondary">
@_post.ViewCount.ToString("N0") بازدید
</MudText>
</MudStack>
</MudStack>
<MudDivider Class="mb-6" />
</MudPaper>
@* ── Article Content ── *@
<MudPaper Elevation="0" Class="blog-content mb-8">
@((MarkupString)_post.HtmlContent)
</MudPaper>
@* ── Tags ── *@
@if (_post.Tags.Any())
{
<MudDivider Class="mb-4" />
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center" Class="mb-6 flex-wrap">
<MudIcon Icon="@Icons.Material.Filled.LocalOffer" Size="Size.Small" Class="mud-text-secondary ml-2" />
@foreach (var tag in _post.Tags)
{
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined"
Class="rounded-pill">
@tag.Title
</MudChip>
}
</MudStack>
}
@* ── Back to Blog ── *@
<div class="text-center mt-6 mb-4">
<MudButton Variant="Variant.Outlined" Color="Color.Primary" OnClick="GoBack"
Class="rounded-pill" StartIcon="@Icons.Material.Filled.ArrowForward">
بازگشت به بلاگ
</MudButton>
</div>
</MudContainer>
}
@@ -0,0 +1,51 @@
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace FrontOffice.Main.Pages.Blog;
public partial class Post
{
[Inject] private IJSRuntime JS { get; set; } = default!;
[Parameter] public string Slug { get; set; } = string.Empty;
[Inject] private BlogPostService BlogPostService { get; set; } = default!;
private BlogPostDetailDto? _post;
private bool _isLoading = true;
protected override async Task OnParametersSetAsync()
{
if (string.IsNullOrWhiteSpace(Slug)) return;
_isLoading = true;
StateHasChanged();
try
{
_post = await BlogPostService.GetBySlugAsync(Slug);
// Fire-and-forget: increment view count
if (_post != null)
{
_ = BlogPostService.IncrementViewCountAsync(_post.Id);
}
}
catch
{
_post = null;
}
finally
{
_isLoading = false;
StateHasChanged();
}
}
private void NavigateToCategoryFilter(long categoryId)
{
Navigation.NavigateTo($"/blog?category={categoryId}");
}
private async Task GoBack() => await JS.InvokeVoidAsync("history.back");
}
+8 -7
View File
@@ -5,10 +5,11 @@
<PageTitle>تکمیل خرید</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<PageHeader Title="تکمیل خرید" BackHref="@RouteConstants.Package.List" />
<MudGrid Spacing="4">
<!-- Header -->
<MudItem xs="12">
<MudPaper Elevation="4" Class="pa-6">
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="3">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
<MudIcon Icon="@Icons.Material.Filled.ShoppingCart" Size="Size.Large" Color="Color.Primary" />
@@ -23,7 +24,7 @@
<MudItem xs="12" md="8">
<MudStack>
<!-- Package Details -->
<MudPaper Elevation="4" Class="pa-6">
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h5" Class="mb-4">جزئیات پکیج</MudText>
@if (_selectedPackage != null)
@@ -33,9 +34,9 @@
<MudGrid Spacing="3">
<MudItem xs="12" md="4">
<MudPaper Class="pa-2 rounded-xl" Style="background: radial-gradient(600px 280px at 120% 0, #daccff 0, transparent 60%), radial-gradient(600px 280px at -10% 100%, #ffe2f2 0, transparent 60%), linear-gradient(180deg, #fff, #fbfaff);">
<MudImage Src="@_selectedPackage.Image"
<AppImage Path="@_selectedPackage.Image"
Alt="@_selectedPackage.Title"
Height="150"
ImgHeight="150"
ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Style="width:100%"
@@ -67,7 +68,7 @@
</MudPaper>
<!-- Address Selection -->
<MudPaper Elevation="4" Class="pa-6">
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h5" Class="mb-4">انتخاب آدرس</MudText>
@if (_isLoadingAddresses)
@@ -149,7 +150,7 @@
<MudItem xs="12" md="4">
<MudStack Spacing="4">
<!-- Discount Code Section -->
<MudPaper Elevation="4" Class="pa-6">
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h5" Class="mb-4">کد تخفیف</MudText>
<MudStack Spacing="3">
<MudTextField @bind-Value="_discountCode"
@@ -176,7 +177,7 @@
<!-- Order Summary -->
@if (_selectedPackage != null)
{
<MudPaper Elevation="4" Class="pa-6">
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h5" Class="mb-4">خلاصه سفارش</MudText>
<MudStack Spacing="3">
+16 -15
View File
@@ -1,7 +1,8 @@
using FrontOffice.BFF.Package.Protobuf.Protos.Package;
using FrontOffice.BFF.Transaction.Protobuf.Protos.Transaction;
using FrontOffice.BFF.UserAddress.Protobuf.Protos.UserAddress;
using FrontOffice.BFF.UserOrder.Protobuf.Protos.UserOrder;
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;
@@ -15,13 +16,13 @@ public partial class Checkout
[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 TransactionContract.TransactionContractClient TransactionContract { get; set; } = default!;
[Inject] private TransactionsContract.TransactionsContractClient TransactionContract { get; set; } = default!;
[Parameter] public long? PackageId { get; set; }
private Pack? _selectedPackage;
private List<GetAllUserAddressByFilterResponseModel> _addresses = new();
private GetAllUserAddressByFilterResponseModel? _selectedAddress;
private List<CustomerAddressModel> _addresses = new();
private CustomerAddressModel? _selectedAddress;
private bool _isLoadingAddresses;
private bool _isProcessingPayment;
@@ -58,7 +59,7 @@ public partial class Checkout
Id: response.Id,
Title: response.Title,
Body: response.Description,
Image: UrlUtility.DownloadUrl + response.ImagePath,
Image: response.ImagePath ?? string.Empty,
Price: response.Price
);
_finalPrice = _selectedPackage.Price;
@@ -76,7 +77,7 @@ public partial class Checkout
_isLoadingAddresses = true;
try
{
var response = await UserAddressContract.GetAllUserAddressByFilterAsync(request: new());
var response = await UserAddressContract.GetCustomerAddressesAsync(request: new());
if (response?.Models?.Any() == true)
{
_addresses = response.Models.ToList();
@@ -85,13 +86,13 @@ public partial class Checkout
}
else
{
_addresses = new List<GetAllUserAddressByFilterResponseModel>();
_addresses = new List<CustomerAddressModel>();
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری آدرس‌ها: {ex.Message}", Severity.Error);
_addresses = new List<GetAllUserAddressByFilterResponseModel>();
_addresses = new List<CustomerAddressModel>();
}
finally
{
@@ -186,7 +187,7 @@ public partial class Checkout
try
{
// Step 1: Create payment request
var paymentRequest = new PaymentRequestRequest
var paymentRequest = new CustomerPaymentRequestRequest
{
Amount = _finalPrice,
CallbackUrl = $"{Navigation.BaseUri}checkout/callback",
@@ -195,7 +196,7 @@ public partial class Checkout
Type = TransactionTypeEnum.Real
};
var paymentResponse = await TransactionContract.PaymentRequestAsync(paymentRequest);
var paymentResponse = await TransactionContract.CustomerPaymentRequestAsync(paymentRequest);
if (string.IsNullOrEmpty(paymentResponse.PaymentGWUrl))
Snackbar.Add("آدرس درگاه پرداخت دریافت نشد.", Severity.Error);
@@ -203,9 +204,9 @@ public partial class Checkout
// Step 2: Create user order
var orderRequest = new CreateNewUserOrderRequest
{
Price = _finalPrice,
Amount = _finalPrice,
PackageId = _selectedPackage.Id,
PaymentStatus = false // Will be updated after payment verification
PaymentStatus = CMSMicroservice.Protobuf.Protos.PaymentStatus.Pending
};
var orderResponse = await UserOrderContract.CreateNewUserOrderAsync(orderRequest);
@@ -1,5 +1,5 @@
using FrontOffice.Main.Utilities;
using FrontOffice.BFF.User.Protobuf.Protos.User;
using CMSMicroservice.Protobuf.Protos.User;
using Microsoft.AspNetCore.Components;
using MudBlazor;
@@ -3,15 +3,13 @@
@using MudBlazor
@inject ClubConfigurationService ClubConfigService
@inject IDialogService DialogService
@inject IJSRuntime JS
<PageTitle>ویژگی‌های باشگاه مشتریان</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">ویژگی‌های باشگاه مشتریان</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Club.Membership">بازگشت</MudButton>
</MudStack>
<PageHeader Title="ویژگی‌های باشگاه مشتریان" BackHref="@RouteConstants.Club.Membership" />
<!-- توضیحات کلی باشگاه -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
@@ -30,13 +28,13 @@
@if (_isLoading)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
<LoadingState Message="در حال دریافت ویژگی‌ها..." />
}
else if (_features == null || !_features.Any())
{
<MudAlert Severity="Severity.Info">
هنوز ویژگی‌ای برای شما فعال نشده است.
</MudAlert>
<EmptyState Icon="@Icons.Material.Filled.Stars"
Title="هنوز ویژگی‌ای برای شما فعال نشده است"
Description="با خرید پکیج یا ارتقاء عضویت، ویژگی‌های اختصاصی فعال می‌شوند." />
}
else
{
@@ -73,7 +71,7 @@
FullWidth="true"
Size="Size.Large"
StartIcon="@Icons.Material.Filled.ArrowBack"
Href="@RouteConstants.Club.Membership">
OnClick="GoBack">
بازگشت به صفحه باشگاه
</MudButton>
</MudStack>
@@ -190,4 +188,6 @@
_selectedFeature = feature;
_showDetailDialog = true;
}
private async Task GoBack() => await JS.InvokeVoidAsync("history.back");
}
@@ -7,14 +7,11 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">عضویت باشگاه مشتریان</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<PageHeader Title="عضویت باشگاه مشتریان" BackHref="@RouteConstants.Profile.Index" />
@if (_isLoading)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
<LoadingState Message="در حال دریافت اطلاعات عضویت..." />
}
else if (_membership is not null)
{
@@ -56,19 +53,6 @@
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined">
<MudText>شما هنوز عضو باشگاه مشتریان نیستید. برای استفاده از مزایای ویژه، اکنون فعال کنید!</MudText>
</MudAlert>
@if (_clubConfig != null)
{
<MudPaper Elevation="0" Class="pa-3 mt-3 rounded-lg" Style="background-color: var(--mud-palette-info-lighten);">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudStack Spacing="1">
<MudText Typo="Typo.subtitle2" Color="Color.Info">هزینه عضویت در باشگاه مشتریان</MudText>
<MudText Typo="Typo.caption">شامل @((_clubConfig.MembershipGiftValue / 10000).ToString("N0")) تومان هدیه</MudText>
</MudStack>
<MudText Typo="Typo.h6" Color="Color.Info">@((_clubConfig.ActivationFee / 10000).ToString("N0")) تومان</MudText>
</MudStack>
</MudPaper>
}
}
</MudStack>
</MudPaper>
@@ -103,11 +87,6 @@
</MudStack>
</MudPaper>
<!-- فعال‌سازی یا تمدید عضویت -->
@if (!_membership.IsActive || (_membership.DaysRemaining.HasValue && _membership.DaysRemaining.Value <= 30))
{
<ActivationSection OnActivationSuccess="HandleActivationSuccess" />
}
<!-- دکمه مشاهده ویژگی‌های بیشتر -->
<MudButton Variant="Variant.Outlined"
@@ -8,10 +8,7 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">پاداش‌های من</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<PageHeader Title="پاداش‌های من" BackHref="@RouteConstants.Profile.Index" />
<!-- فیلترها -->
<MudPaper Elevation="2" Class="pa-3 rounded-lg">
@@ -52,7 +49,7 @@
@if (_isLoading)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
<LoadingState Message="در حال دریافت پاداش‌ها..." />
}
else
{
@@ -7,10 +7,7 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">گزارش هفتگی پاداش</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Commission.Dashboard">بازگشت</MudButton>
</MudStack>
<PageHeader Title="گزارش هفتگی پاداش" BackHref="@RouteConstants.Commission.Dashboard" />
<!-- انتخاب هفته -->
<MudPaper Elevation="2" Class="pa-3 rounded-lg">
@@ -50,7 +47,7 @@
@if (_isLoading)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
<LoadingState Message="در حال دریافت گزارش..." />
}
else if (_weeklyBalance is not null)
{
@@ -76,11 +73,11 @@
<MudGrid Spacing="2">
<MudItem xs="6">
<MudPaper Elevation="3" Class="pa-4 rounded-lg text-center" Style="background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);">
<MudPaper Elevation="2" Class="pa-4 rounded-lg text-center bg-success-soft stat-card-ds">
<MudStack Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.ChevronRight" Color="Color.Success" Size="Size.Large" />
<MudText Typo="Typo.subtitle2" Color="Color.Success">تیم دوم</MudText>
<MudText Typo="Typo.h5" Color="Color.Success" Style="font-weight: bold;">@_weeklyBalance.RightBalanceFormatted</MudText>
<MudText Typo="Typo.h5" Color="Color.Success" Class="dash-stat-value">@_weeklyBalance.RightBalanceFormatted</MudText>
<MudProgressLinear Color="Color.Success"
Value="@GetRightPercentage()"
Size="Size.Medium"
@@ -93,11 +90,11 @@
</MudPaper>
</MudItem>
<MudItem xs="6">
<MudPaper Elevation="3" Class="pa-4 rounded-lg text-center" Style="background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);">
<MudPaper Elevation="2" Class="pa-4 rounded-lg text-center bg-info-soft stat-card-ds">
<MudStack Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Color="Color.Info" Size="Size.Large"/>
<MudText Typo="Typo.subtitle2" Color="Color.Info">تیم اول</MudText>
<MudText Typo="Typo.h5" Color="Color.Info" Style="font-weight: bold;">@_weeklyBalance.LeftBalanceFormatted</MudText>
<MudText Typo="Typo.h5" Color="Color.Info" Class="dash-stat-value">@_weeklyBalance.LeftBalanceFormatted</MudText>
<MudProgressLinear Color="Color.Info"
Value="@GetLeftPercentage()"
Size="Size.Medium"
@@ -114,7 +111,7 @@
<!-- محاسبات پاداش - فشرده‌تر -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.subtitle1" Class="mb-2" Style="font-weight: bold;">محاسبات پاداش</MudText>
<MudText Typo="Typo.subtitle1" Class="mb-2 fw-bold">محاسبات پاداش</MudText>
<MudSimpleTable Dense="true" Hover="true" Style="background: transparent;">
<tbody>
<tr>
+23 -23
View File
@@ -1,4 +1,5 @@
@attribute [Route(RouteConstants.Contact.Index)]
@inject SitePageService SitePageService
<PageTitle>ارتباط با ما | کارا بازار سلامت</PageTitle>
@@ -8,10 +9,10 @@
<MudStack AlignItems="AlignItems.Center" Spacing="4">
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled" Class="mb-2">ارتباط با ما</MudChip>
<MudText Typo="Typo.h2" Align="Align.Center" Class="mb-3">
آماده شنیدن صدای شما هستیم
@(_page?.HeroTitle ?? "آماده شنیدن صدای شما هستیم")
</MudText>
<MudText Typo="Typo.body1" Align="Align.Center" Class="mud-text-secondary mb-6" Style="max-width:600px">
سوالات، پیشنهادات یا انتقادات خود را با ما در میان بگذارید. تیم ما آماده پاسخگویی به شماست.
@(_page?.HeroSubtitle ?? "سوالات، پیشنهادات یا انتقادات خود را با ما در میان بگذارید. تیم ما آماده پاسخگویی به شماست.")
</MudText>
</MudStack>
</MudContainer>
@@ -25,7 +26,7 @@
<MudGrid Spacing="6">
<!-- Contact Form -->
<MudItem xs="12" lg="8">
<MudPaper Elevation="3" Class="pa-6">
<MudPaper Elevation="2" Class="pa-6">
<MudText Typo="Typo.h5" Class="mb-6">فرم تماس</MudText>
<MudForm @ref="_form" Model="_contactForm">
@@ -72,12 +73,12 @@
Variant="Variant.Outlined"
Required="true"
RequiredError="انتخاب موضوع الزامی است.">
<MudSelectItem Value="@("general")">عمومی</MudSelectItem>
<MudSelectItem Value="@("support")">پشتیبانی فنی</MudSelectItem>
<MudSelectItem Value="@("sales")">فروش و قیمت‌گذاری</MudSelectItem>
<MudSelectItem Value="@("partnership")">شریک تجاری</MudSelectItem>
<MudSelectItem Value="@("complaint")">شکایت</MudSelectItem>
<MudSelectItem Value="@("suggestion")">پیشنهاد</MudSelectItem>
<MudSelectItem T="string" Value="@("general")">عمومی</MudSelectItem>
<MudSelectItem T="string" Value="@("support")">پشتیبانی فنی</MudSelectItem>
<MudSelectItem T="string" Value="@("sales")">فروش و قیمت‌گذاری</MudSelectItem>
<MudSelectItem T="string" Value="@("partnership")">شریک تجاری</MudSelectItem>
<MudSelectItem T="string" Value="@("complaint")">شکایت</MudSelectItem>
<MudSelectItem T="string" Value="@("suggestion")">پیشنهاد</MudSelectItem>
</MudSelect>
</MudItem>
@@ -123,7 +124,7 @@
<MudItem xs="12" lg="4">
<MudStack Spacing="4">
<!-- Contact Details -->
<MudPaper Elevation="3" Class="pa-6">
<MudPaper Elevation="2" Class="pa-6">
<MudText Typo="Typo.h6" Class="mb-4">اطلاعات تماس</MudText>
<MudStack Spacing="4">
@@ -132,7 +133,7 @@
<div>
<MudText Typo="Typo.body2" >آدرس</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶
@(_contactAddress ?? "کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶")
</MudText>
</div>
</MudStack>
@@ -141,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">026-34233563</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@(_contactPhone ?? "026-34233563")</MudText>
</div>
</MudStack>
@@ -149,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">info@kbs1.co</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@(_contactEmail ?? "info@kbs1.co")</MudText>
</div>
</MudStack>
@@ -158,8 +159,7 @@
<div>
<MudText Typo="Typo.body2" >ساعات کاری</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
شنبه تا پنج‌شنبه<br />
۹ صبح تا ۶ عصر
@((MarkupString)(_contactHours ?? "شنبه تا پنج‌شنبه<br />۹ صبح تا ۶ عصر"))
</MudText>
</div>
</MudStack>
@@ -167,14 +167,14 @@
</MudPaper>
<!-- Social Media -->
<MudPaper Elevation="3" Class="pa-6">
<MudPaper Elevation="2" Class="pa-6">
<MudText Typo="Typo.h6" Class="mb-4">شبکه‌های اجتماعی</MudText>
<MudStack Spacing="3">
<MudButton Variant="Variant.Outlined"
Color="Color.Inherit"
StartIcon="@Icons.Custom.Brands.Telegram"
Href="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="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="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="https://wa.me/989123456789"
Href="@(_socialWhatsapp ?? "https://wa.me/989123456789")"
Target="_blank"
FullWidth="true">
واتس‌اپ
@@ -219,14 +219,14 @@
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h4" Align="Align.Center" Class="mb-8">موقعیت مکانی</MudText>
<MudPaper Elevation="3" Class="pa-4 overflow-hidden">
<MudPaper Elevation="2" Class="pa-4 overflow-hidden">
<!-- Placeholder for Map -->
<div style="height: 400px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center;">
<div class="contact-map-placeholder" style="height: 400px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; border-radius: 12px;">
<MudStack AlignItems="AlignItems.Center" Spacing="3">
<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">
کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶
@(_contactAddress ?? "کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶")
</MudText>
</MudStack>
</div>
+75 -4
View File
@@ -1,8 +1,10 @@
using FluentValidation;
using FrontOffice.Main.Utilities;
using MudBlazor;
using Severity = MudBlazor.Severity;
namespace FrontOffice.Main.Pages;
public partial class Contact
{
private ContactForm _contactForm = new();
@@ -10,6 +12,60 @@ public partial class Contact
private bool _isSubmitting;
private readonly ContactFormValidator _contactFormValidator = new();
// 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 SitePageService.GetByKeyAsync("contact");
if (_page != null)
{
// 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
{
// Fallback: all values remain null → hardcoded defaults will render
}
}
private async Task SubmitContactForm()
{
if (_form is null) return;
@@ -55,14 +111,12 @@ public partial class Contact
private void CallSupport()
{
// TODO: Initiate phone call or show phone number
Snackbar.Add("شماره تماس: ۰۲۱-۱۲۳۴۵۶۷۸", Severity.Info);
Snackbar.Add($"شماره تماس: {_contactPhone ?? "026-34233563"}", Severity.Info);
}
private void SendEmail()
{
// TODO: Open email client or redirect to email page
Snackbar.Add("ایمیل: info@kbs1.co", Severity.Info);
Snackbar.Add($"ایمیل: {_contactEmail ?? "info@kbs1.co"}", Severity.Info);
}
public class ContactForm
@@ -87,4 +141,21 @@ public partial class Contact
RuleFor(x => x.Message).NotEmpty().MinimumLength(10).WithMessage("پیام باید حداقل ۱۰ کاراکتر باشد");
}
}
// 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? 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; }
}
}
@@ -0,0 +1,190 @@
@attribute [Route(RouteConstants.DiscountStore.Cart)]
<PageTitle>سبد خرید تخفیفی</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="سبد خرید تخفیفی" BackHref="@RouteConstants.DiscountStore.Products" />
@if (DiscountCart.Items.Count == 0)
{
<MudAlert Severity="Severity.Info">سبد خرید تخفیفی شما خالی است.</MudAlert>
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.ArrowBack"
OnClick="() => Navigation.NavigateTo(RouteConstants.DiscountStore.Products)">
بازگشت به فروشگاه تخفیفی
</MudButton>
}
else
{
<!-- Desktop Table -->
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudPaper Elevation="1" Class="pa-4 rounded-lg">
<MudTable Items="DiscountCart.Items">
<HeaderContent>
<MudTh>محصول</MudTh>
<MudTh>قیمت واحد</MudTh>
<MudTh>سقف تخفیف</MudTh>
<MudTh>تعداد</MudTh>
<MudTh>تخفیف</MudTh>
<MudTh>قیمت نهایی</MudTh>
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<AppImage Path="@GetImageUrl(context.ImageUrl)" Alt="@context.Title"
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% تخفیف
</MudChip>
</MudTd>
<MudTd>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIconButton Icon="@Icons.Material.Filled.Remove" Color="Color.Default"
OnClick="@(() => DecrementQty(context.ProductId))" />
<MudText>@context.Quantity</MudText>
<MudIconButton Icon="@Icons.Material.Filled.Add" Color="Color.Default"
OnClick="@(() => IncrementQty(context.ProductId))" />
</MudStack>
</MudTd>
<MudTd>
@if (context.DiscountAmount > 0)
{
<MudText Color="Color.Success">@FormatPrice(context.DiscountAmount)-</MudText>
}
else
{
<MudText Class="mud-text-secondary">—</MudText>
}
</MudTd>
<MudTd>@FormatPrice(context.FinalPrice)</MudTd>
<MudTd>
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error"
OnClick="() => Remove(context.ProductId)" />
</MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
</MudHidden>
<!-- Mobile Cards -->
<MudHidden Breakpoint="Breakpoint.MdAndUp">
<MudStack Spacing="2">
@foreach (var item in DiscountCart.Items)
{
<MudPaper Class="pa-3 rounded-lg w-100-mobile" Outlined="true">
<MudStack Spacing="1">
<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-circle" />
<MudText Typo="Typo.subtitle2">@item.Title</MudText>
</MudStack>
<MudChip T="string" Color="Color.Error" Variant="Variant.Outlined" Size="Size.Small">
@item.MaxDiscountPercent% تخفیف
</MudChip>
</MudStack>
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Class="mud-text-secondary">@FormatPrice(item.UnitPrice) واحد</MudText>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIconButton
Icon="@(item.Quantity <= 1 ? Icons.Material.Filled.Delete : Icons.Material.Filled.Remove)"
Color="Color.Default"
OnClick="@(() => DecrementQty(item.ProductId))" />
<MudText>@item.Quantity</MudText>
<MudIconButton Icon="@Icons.Material.Filled.Add"
Color="Color.Default"
OnClick="@(() => IncrementQty(item.ProductId))" />
</MudStack>
</MudStack>
<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>جمع: @FormatPrice(item.FinalPrice)</MudText>
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error"
OnClick="() => Remove(item.ProductId)" />
</MudStack>
</MudStack>
</MudStack>
</MudPaper>
}
</MudStack>
</MudHidden>
<!-- Summary -->
@if (DeviceDetector.IsMobile())
{
<MudStack Spacing="1" Class="mobile-actions-stack px-5">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2">جمع کل:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(DiscountCart.TotalPrice) تومان</MudText>
</MudStack>
@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">@FormatPrice(DiscountCart.TotalDiscount)- تومان</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(DiscountCart.FinalPrice) تومان</MudText>
</MudStack>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween" Spacing="2" Class="mobile-actions-stack">
<MudButton Variant="Variant.Filled" Color="Color.Primary"
OnClick="() => Navigation.NavigateTo(RouteConstants.DiscountStore.Products)">
افزودن محصول
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Success" OnClick="ProceedCheckout"
StartIcon="@Icons.Material.Filled.CreditCard">ادامه خرید</MudButton>
</MudStack>
}
else
{
<MudPaper Class="pa-4 rounded-lg" Style="background-color: var(--mud-palette-background-grey);">
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">جمع کل:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(DiscountCart.TotalPrice) تومان</MudText>
</MudStack>
@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">@FormatPrice(DiscountCart.TotalDiscount)- تومان</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(DiscountCart.FinalPrice) تومان</MudText>
</MudStack>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
مبلغ دقیق پرداخت در مرحله تسویه مشخص خواهد شد.
</MudText>
</MudStack>
</MudPaper>
<MudStack Row="true" Justify="Justify.FlexEnd" Spacing="2" Class="mt-3">
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
OnClick="() => Navigation.NavigateTo(RouteConstants.DiscountStore.Products)">
افزودن محصول
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Success" OnClick="ProceedCheckout"
StartIcon="@Icons.Material.Filled.CreditCard">ادامه فرایند خرید</MudButton>
</MudStack>
}
}
</MudStack>
</MudContainer>
@@ -0,0 +1,56 @@
using Microsoft.AspNetCore.Components;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.DiscountStore;
public partial class Cart : IDisposable
{
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
await DiscountCart.EnsureInitializedAsync();
DiscountCart.OnChange += StateHasChanged;
}
private async Task IncrementQty(long productId)
{
var item = DiscountCart.Items.FirstOrDefault(i => i.ProductId == productId);
if (item is null) return;
await DiscountCart.UpdateQuantityAsync(productId, item.Quantity + 1);
}
private async Task DecrementQty(long productId)
{
var item = DiscountCart.Items.FirstOrDefault(i => i.ProductId == productId);
if (item is null) return;
if (item.Quantity <= 1)
{
await DiscountCart.RemoveAsync(productId);
}
else
{
await DiscountCart.UpdateQuantityAsync(productId, item.Quantity - 1);
}
}
private async Task Remove(long productId)
{
await DiscountCart.RemoveAsync(productId);
}
private void ProceedCheckout()
{
Navigation.NavigateTo(RouteConstants.DiscountStore.Checkout);
}
private static string FormatPrice(long price) => $"{price:N0} ";
private static string GetImageUrl(string? imageUrl)
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/');
public void Dispose()
{
DiscountCart.OnChange -= StateHasChanged;
}
}
@@ -0,0 +1,145 @@
@using CMSMicroservice.Protobuf.Protos.UserAddress
@attribute [Route(RouteConstants.DiscountStore.Checkout)]
<PageTitle>تسویه حساب فروشگاه تخفیفی</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<PageHeader Title="تسویه حساب تخفیفی" BackHref="@RouteConstants.DiscountStore.Cart" />
<MudGrid Spacing="3">
<!-- Left Column: Address + Payment Settings -->
<MudItem xs="12" md="8">
<!-- Address Selection -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg mb-3">
<MudText Typo="Typo.h6" Class="mb-2">انتخاب آدرس</MudText>
@if (_loadingAddresses)
{
<MudStack AlignItems="AlignItems.Center" Class="py-4">
<MudProgressCircular Indeterminate="true" Color="Color.Primary" />
<MudText Class="mt-2 mud-text-secondary">در حال بارگذاری آدرس‌ها...</MudText>
</MudStack>
}
else if (_addresses.Count == 0)
{
<MudAlert Severity="Severity.Warning">
هیچ آدرسی ثبت نشده است. لطفاً از بخش پروفایل آدرس خود را اضافه کنید.
</MudAlert>
<MudButton Class="mt-2" Variant="Variant.Outlined" Href="/profile/addresses">افزودن آدرس</MudButton>
}
else
{
<MudStack Spacing="1">
@foreach (var address in _addresses)
{
<MudPaper Outlined="@(_selectedAddress?.Id != address.Id)"
Elevation="@(_selectedAddress?.Id == address.Id ? 2 : 0)"
Class="pa-3 rounded-xl cursor-pointer"
Style="@(_selectedAddress?.Id == address.Id ? "border: 2px solid var(--mud-palette-primary);" : "")"
@onclick="() => _selectedAddress = address">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudStack>
<MudText Typo="Typo.subtitle2">@address.Title</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@address.Address</MudText>
</MudStack>
@if (address.IsDefault)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Outlined">پیش‌فرض</MudChip>
}
</MudStack>
</MudPaper>
}
</MudStack>
}
</MudPaper>
<!-- Notes -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-2">توضیحات سفارش</MudText>
<MudTextField T="string" @bind-Value="_notes" Lines="3"
Placeholder="توضیحات اختیاری..."
Variant="Variant.Outlined" />
</MudPaper>
</MudItem>
<!-- Right Column: Order Summary -->
<MudItem xs="12" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-lg" Style="position:sticky; top:80px;">
<MudText Typo="Typo.h6" Class="mb-2">خلاصه سفارش</MudText>
@if (DiscountCart.Items.Count == 0)
{
<MudAlert Severity="Severity.Info">سبد خرید شما خالی است.</MudAlert>
}
else
{
<MudList T="string" Dense="true">
@foreach (var item in DiscountCart.Items)
{
<MudListItem T="string">
<MudListItemText>
<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-circle" />
<MudText Typo="Typo.subtitle2" Style="flex:1; margin:0 8px;">@item.Title</MudText>
<MudText Typo="Typo.subtitle2">@FormatPrice(item.FinalPrice)</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.caption" Class="mud-text-secondary">
@item.Quantity × @FormatPrice(item.UnitPrice)
</MudText>
@if (item.MaxDiscountPercent > 0)
{
<MudText Typo="Typo.caption" Color="Color.Error">@item.MaxDiscountPercent% تخفیف</MudText>
}
</MudStack>
</MudStack>
</MudListItemText>
</MudListItem>
}
</MudList>
<MudDivider Class="my-2" />
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">جمع کل:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(DiscountCart.TotalPrice) تومان</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<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">@FormatPrice(NetGatewayAmount) تومان</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">مالیات بر ارزش افزوده (۹٪):</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(FinalGatewayAmount) تومان
</MudText>
</MudStack>
</MudStack>
<MudButton Disabled="@(!CanPlaceOrder || _placing)" Class="mt-3 w-100-mobile"
Variant="Variant.Filled" Color="Color.Primary"
OnClick="PlaceOrder" FullWidth="true"
StartIcon="@Icons.Material.Filled.CheckCircle">
@(_placing ? "در حال ثبت سفارش..." : "ثبت و پرداخت سفارش")
</MudButton>
}
</MudPaper>
</MudItem>
</MudGrid>
</MudContainer>
@@ -0,0 +1,119 @@
using Microsoft.AspNetCore.Components;
using CMSMicroservice.Protobuf.Protos.UserAddress;
using FrontOffice.Main.Utilities;
using MudBlazor;
namespace FrontOffice.Main.Pages.DiscountStore;
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!;
private List<CustomerAddressModel> _addresses = new();
private CustomerAddressModel? _selectedAddress;
private bool _loadingAddresses;
private string? _notes;
private bool _placing;
private const decimal VAT_RATE = 0.09m;
/// <summary>مبلغ درگاه قبل از مالیات (جمع کل - تخفیف)</summary>
private long NetGatewayAmount => DiscountCart.TotalPrice - DiscountCart.TotalDiscount;
/// <summary>مالیات بر ارزش افزوده ۹٪</summary>
private long VatAmount => (long)(NetGatewayAmount * VAT_RATE);
/// <summary>مبلغ نهایی قابل پرداخت (شامل VAT)</summary>
private long FinalGatewayAmount => NetGatewayAmount + VatAmount;
private bool CanPlaceOrder => DiscountCart.Items.Count > 0 && _selectedAddress is not null;
protected override async Task OnInitializedAsync()
{
await DiscountCart.EnsureInitializedAsync();
await LoadAddresses();
}
private async Task LoadAddresses()
{
_loadingAddresses = true;
try
{
var response = await UserAddressContract.GetCustomerAddressesAsync(new());
if (response?.Models?.Any() == true)
{
_addresses = response.Models.ToList();
_selectedAddress = _addresses.FirstOrDefault(a => a.IsDefault) ?? _addresses.First();
}
else
{
_addresses = new();
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری آدرس‌ها: {ex.Message}", Severity.Error);
_addresses = new();
}
finally
{
_loadingAddresses = false;
await InvokeAsync(StateHasChanged);
}
}
private async Task PlaceOrder()
{
if (!CanPlaceOrder || _selectedAddress is null)
{
Snackbar.Add("لطفاً آدرس را انتخاب کنید.", Severity.Warning);
return;
}
_placing = true;
try
{
// Always use 100% discount — send full price, server will apply max from wallet
var result = await DiscountOrderService.PlaceOrderAsync(
_selectedAddress.Id,
DiscountCart.TotalPrice,
_notes);
if (!result.Success)
{
Snackbar.Add(result.Message, Severity.Error);
return;
}
// If there's a gateway payment URL, redirect to it
if (!string.IsNullOrWhiteSpace(result.PaymentUrl) && result.GatewayAmount > 0)
{
Snackbar.Add("در حال انتقال به درگاه پرداخت...", Severity.Info);
await DiscountCart.ClearAsync();
Navigation.NavigateTo(result.PaymentUrl, forceLoad: true);
return;
}
// If fully paid via discount balance (no gateway needed)
await DiscountCart.ClearAsync();
Snackbar.Add("سفارش با موفقیت ثبت شد!", Severity.Success);
Navigation.NavigateTo($"{RouteConstants.DiscountStore.OrderDetail}{result.OrderId}");
}
catch (Exception ex)
{
Snackbar.Add($"خطا در ثبت سفارش: {ex.Message}", Severity.Error);
}
finally
{
_placing = false;
}
}
private static string FormatPrice(long price) => $"{price:N0}";
private static string GetImageUrl(string? imageUrl)
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/');
}
@@ -0,0 +1,209 @@
@attribute [Route("/discount-store/order/{Id:long}")]
<PageTitle>جزئیات سفارش تخفیفی</PageTitle>
@if (!string.IsNullOrEmpty(_paymentMessage))
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="pt-4">
<MudAlert Severity="@_paymentSeverity" Variant="Variant.Filled" Class="mb-2"
CloseIconClicked="() => _paymentMessage = null" ShowCloseIcon="true">
@_paymentMessage
</MudAlert>
</MudContainer>
}
@if (_loading)
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<LoadingState />
</MudContainer>
}
else if (_order is null)
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<EmptyState Icon="@Icons.Material.Filled.SearchOff"
Title="سفارش مورد نظر یافت نشد."
ActionText="بازگشت به لیست سفارش‌ها"
ActionHref="@RouteConstants.DiscountStore.Orders" />
</MudContainer>
}
else
{
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<!-- Header -->
<PageHeader Title="@($"سفارش #{_order.OrderNumber}")" BackHref="@RouteConstants.DiscountStore.Orders" />
<MudStack Row="true" Spacing="2" Class="flex-wrap mb-4">
<MudChip T="string" Color="@DiscountOrderService.GetPaymentStatusColor(_order.PaymentStatus)"
Variant="Variant.Filled">
@DiscountOrderService.GetPaymentStatusText(_order.PaymentStatus)
</MudChip>
<MudChip T="string" Color="@DiscountOrderService.GetDeliveryStatusColor(_order.DeliveryStatus)"
Variant="Variant.Outlined">
@DiscountOrderService.GetDeliveryStatusText(_order.DeliveryStatus)
</MudChip>
</MudStack>
@if (_order.PaymentStatus == 2)
{
<MudAlert Severity="Severity.Error" Variant="Variant.Outlined" Class="mb-4">
<MudText>پرداخت این سفارش ناموفق بود و سفارش منقضی شده است. موجودی رزرو شده آزاد شد.</MudText>
<MudButton Variant="Variant.Text" Color="Color.Primary" Class="mt-2"
Href="@RouteConstants.DiscountStore.Products">
بازگشت به فروشگاه و ثبت سفارش جدید
</MudButton>
</MudAlert>
}
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="2">
<MudText Typo="Typo.caption" Class="mud-text-secondary">
تاریخ ثبت: @FormatDate(_order.Created)
</MudText>
@* ── Address ── *@
@if (_order.Address is not null)
{
<MudText Typo="Typo.body2">
<b>آدرس:</b> @_order.Address.Title — @_order.Address.Address
</MudText>
@if (!string.IsNullOrWhiteSpace(_order.Address.PostalCode))
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">کد پستی: @_order.Address.PostalCode</MudText>
}
}
<MudDivider Class="my-2" />
@* ── Desktop: Table ── *@
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudText Typo="Typo.h6" Class="mb-2">اقلام سفارش</MudText>
<MudTable Items="_order.Items" Dense="true">
<HeaderContent>
<MudTh>محصول</MudTh>
<MudTh>قیمت واحد</MudTh>
<MudTh>تعداد</MudTh>
<MudTh>سقف تخفیف</MudTh>
<MudTh>تخفیف</MudTh>
<MudTh>قیمت نهایی</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Title</MudTd>
<MudTd>@FormatPrice(context.UnitPrice)</MudTd>
<MudTd>@context.Count</MudTd>
<MudTd>
@if (context.MaxDiscountPercent > 0)
{
<MudChip T="string" Size="Size.Small" Color="Color.Error" Variant="Variant.Outlined">@context.MaxDiscountPercent%</MudChip>
}
</MudTd>
<MudTd>
@if (context.DiscountAmount > 0)
{
<MudText Color="Color.Success">@FormatPrice(context.DiscountAmount)-</MudText>
}
else
{
<MudText Class="mud-text-secondary">—</MudText>
}
</MudTd>
<MudTd>@FormatPrice(context.FinalPrice)</MudTd>
</RowTemplate>
</MudTable>
</MudHidden>
@* ── Mobile: Cards ── *@
<MudHidden Breakpoint="Breakpoint.MdAndUp">
<MudText Typo="Typo.h6" Class="mb-2">اقلام سفارش</MudText>
<MudStack Spacing="2">
@foreach (var item in _order.Items)
{
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
<MudStack Spacing="1">
<MudText Typo="Typo.subtitle2">@item.Title</MudText>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">قیمت واحد:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(item.UnitPrice)</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">تعداد:</MudText>
<MudText Typo="Typo.body2">@item.Count</MudText>
</MudStack>
@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">@FormatPrice(item.DiscountAmount)-</MudText>
</MudStack>
}
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle2">قیمت نهایی:</MudText>
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatPrice(item.FinalPrice)</MudText>
</MudStack>
</MudStack>
</MudPaper>
}
</MudStack>
</MudHidden>
<MudDivider Class="my-2" />
@* ── Financial Summary ── *@
<MudStack Spacing="1" Class="pa-3" Style="background-color: var(--mud-palette-background-grey); border-radius: 8px;">
<MudText Typo="Typo.h6" Class="mb-1">خلاصه مالی</MudText>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">جمع کل:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(_order.TotalPrice)</MudText>
</MudStack>
@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">@FormatPrice(_order.DiscountBalanceUsed)-</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(_order.GatewayAmount)
</MudText>
</MudStack>
</MudStack>
@* ── Tracking / Transaction ── *@
@if (!string.IsNullOrWhiteSpace(_order.TrackingCode))
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">کد رهگیری:</MudText>
<MudText Typo="Typo.body2" Class="fw-semibold">@_order.TrackingCode</MudText>
</MudStack>
}
@if (!string.IsNullOrWhiteSpace(_order.TransactionId))
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.caption" Class="mud-text-secondary">شناسه تراکنش:</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@_order.TransactionId</MudText>
</MudStack>
}
@* ── Notes ── *@
@if (!string.IsNullOrWhiteSpace(_order.Notes))
{
<MudDivider Class="my-1" />
<MudText Typo="Typo.body2" Class="mud-text-secondary">
<b>توضیحات:</b> @_order.Notes
</MudText>
}
@if (!string.IsNullOrWhiteSpace(_order.AdminNotes))
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true">
<b>پیام مدیر:</b> @_order.AdminNotes
</MudAlert>
}
</MudStack>
</MudPaper>
</MudContainer>
}
@@ -0,0 +1,76 @@
using Microsoft.AspNetCore.Components;
using DateTimeConverterCL;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.DiscountStore;
public partial class OrderDetail
{
[Parameter] public long Id { get; set; }
[SupplyParameterFromQuery(Name = "payment")]
public string? PaymentResult { get; set; }
[Inject] private DiscountOrderService DiscountOrderService { get; set; } = default!;
private DiscountOrderDetail? _order;
private bool _loading = true;
private string? _paymentMessage;
private MudBlazor.Severity _paymentSeverity;
protected override async Task OnInitializedAsync()
{
// نمایش پیام نتیجه پرداخت (بعد از بازگشت از درگاه)
if (!string.IsNullOrEmpty(PaymentResult))
{
switch (PaymentResult.ToLower())
{
case "success":
_paymentMessage = "پرداخت با موفقیت انجام شد! سفارش شما ثبت نهایی شد.";
_paymentSeverity = MudBlazor.Severity.Success;
break;
case "failed":
_paymentMessage = "پرداخت ناموفق بود. مبلغ رزرو شده آزاد خواهد شد.";
_paymentSeverity = MudBlazor.Severity.Error;
break;
case "error":
_paymentMessage = "خطایی در پردازش پرداخت رخ داد. لطفاً با پشتیبانی تماس بگیرید.";
_paymentSeverity = MudBlazor.Severity.Warning;
break;
}
}
await LoadOrderAsync();
}
private async Task LoadOrderAsync()
{
_loading = true;
try
{
_order = await DiscountOrderService.GetOrderByIdAsync(Id);
}
catch
{
Snackbar.Add("خطا در بارگذاری سفارش", MudBlazor.Severity.Error);
}
finally
{
_loading = false;
}
}
private static string FormatPrice(long price) => $"{price:N0} تومان";
private static string FormatDate(DateTime date)
{
try
{
return date.ToLocalTime().MiladiToJalaliWithTime();
}
catch
{
return date.ToString("yyyy/MM/dd");
}
}
}
@@ -0,0 +1,130 @@
@attribute [Route(RouteConstants.DiscountStore.Orders)]
<PageTitle>سفارش‌های فروشگاه تخفیفی</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="سفارش‌های تخفیفی" BackHref="@RouteConstants.DiscountStore.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.DiscountStore.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>
<MudTh>تاریخ</MudTh>
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.OrderNumber</MudTd>
<MudTd>@context.ItemsCount</MudTd>
<MudTd>@FormatPrice(context.TotalPrice)</MudTd>
<MudTd>
@if (context.DiscountBalanceUsed > 0)
{
<MudText Color="Color.Success">@FormatPrice(context.DiscountBalanceUsed)</MudText>
}
else
{
<MudText Class="mud-text-secondary">—</MudText>
}
</MudTd>
<MudTd>@FormatPrice(context.GatewayAmount)</MudTd>
<MudTd>
<MudChip T="string" Size="Size.Small"
Color="@DiscountOrderService.GetPaymentStatusColor(context.PaymentStatus)"
Variant="Variant.Filled">
@DiscountOrderService.GetPaymentStatusText(context.PaymentStatus)
</MudChip>
</MudTd>
<MudTd>
<MudChip T="string" Size="Size.Small"
Color="@DiscountOrderService.GetDeliveryStatusColor(context.DeliveryStatus)"
Variant="Variant.Outlined">
@DiscountOrderService.GetDeliveryStatusText(context.DeliveryStatus)
</MudChip>
</MudTd>
<MudTd>@FormatDate(context.Created)</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)
{
<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.OrderNumber</MudText>
<MudChip T="string" Size="Size.Small"
Color="@DiscountOrderService.GetPaymentStatusColor(order.PaymentStatus)"
Variant="Variant.Filled">
@DiscountOrderService.GetPaymentStatusText(order.PaymentStatus)
</MudChip>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.caption" Class="mud-text-secondary">@FormatDate(order.Created)</MudText>
<MudChip T="string" Size="Size.Small"
Color="@DiscountOrderService.GetDeliveryStatusColor(order.DeliveryStatus)"
Variant="Variant.Outlined">
@DiscountOrderService.GetDeliveryStatusText(order.DeliveryStatus)
</MudChip>
</MudStack>
<MudDivider />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">@order.ItemsCount قلم</MudText>
<MudText Typo="Typo.body2" Class="fw-semibold">@FormatPrice(order.TotalPrice) تومان</MudText>
</MudStack>
@if (order.DiscountBalanceUsed > 0)
{
<MudText Typo="Typo.caption" Color="Color.Success">
از کیف تخفیفی: @FormatPrice(order.DiscountBalanceUsed) تومان
</MudText>
}
</MudStack>
</MudPaper>
}
</MudStack>
</MudHidden>
@if (_totalPages > 1)
{
<MudStack AlignItems="AlignItems.Center" Class="mt-4">
<MudPagination Count="@_totalPages" Selected="@_currentPage"
SelectedChanged="OnPageChanged" Color="Color.Primary"
Variant="Variant.Outlined" />
</MudStack>
}
}
</MudStack>
</MudContainer>
@@ -0,0 +1,65 @@
using Microsoft.AspNetCore.Components;
using DateTimeConverterCL;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.DiscountStore;
public partial class Orders
{
[Inject] private DiscountOrderService DiscountOrderService { get; set; } = default!;
private List<DiscountOrderSummary> _orders = new();
private int _currentPage = 1;
private int _totalPages;
private bool _loading = true;
private const int PageSize = 10;
protected override async Task OnInitializedAsync()
{
await LoadOrdersAsync();
}
private async Task LoadOrdersAsync()
{
_loading = true;
try
{
var result = await DiscountOrderService.GetUserOrdersAsync(_currentPage, PageSize);
_orders = result.Orders;
_totalPages = result.TotalPages;
}
catch
{
Snackbar.Add("خطا در بارگذاری سفارش‌ها", MudBlazor.Severity.Error);
}
finally
{
_loading = false;
}
}
private async Task OnPageChanged(int page)
{
_currentPage = page;
await LoadOrdersAsync();
}
private void ViewOrder(long orderId)
{
Navigation.NavigateTo($"{RouteConstants.DiscountStore.OrderDetail}{orderId}");
}
private static string FormatPrice(long price) => $"{price:N0}";
private static string FormatDate(DateTime date)
{
try
{
return date.ToLocalTime().MiladiToJalaliWithTime();
}
catch
{
return date.ToString("yyyy/MM/dd");
}
}
}
@@ -0,0 +1,140 @@
@attribute [Route("/discount-store/product/{Id:long}")]
<PageTitle>@(_product?.Title ?? "محصول تخفیفی")</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-4 py-md-6">
@if (_loading)
{
<LoadingState />
}
else if (_product is null)
{
<EmptyState Icon="@Icons.Material.Filled.SearchOff"
Title="محصول مورد نظر یافت نشد."
ActionText="بازگشت به فروشگاه تخفیفی"
ActionHref="@RouteConstants.DiscountStore.Products" />
}
else
{
<!-- Breadcrumb -->
<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.Contain"
Style="width:100%; max-height:400px; border-radius:12px;" />
@if (_product.Images.Count > 1)
{
<MudStack Row="true" Spacing="1" Class="mt-2 flex-wrap" Justify="Justify.Center">
@foreach (var img in _product.Images)
{
<span @onclick="() => _selectedImage = img.ImageUrl" style="cursor:pointer">
<AppImage Path="@img.ThumbnailUrl" Alt="@(img.Title ?? "")"
ImgWidth="60" ImgHeight="60"
ObjectFit="ObjectFit.Cover"
Class="rounded-lg"
Style="@($"border: 2px solid {(img.ImageUrl == _selectedImage ? "var(--mud-palette-primary)" : "transparent")};")"
/>
</span>
}
</MudStack>
}
</MudPaper>
</MudItem>
<!-- Product Info -->
<MudItem xs="12" md="7">
<MudStack Spacing="3">
<MudText Typo="Typo.h5" Class="fw-bold">@_product.Title</MudText>
<!-- Categories -->
@if (_product.Categories.Any())
{
<MudStack Row="true" Spacing="1" Class="flex-wrap">
@foreach (var cat in _product.Categories)
{
<MudChip T="string" Color="Color.Info" Variant="Variant.Outlined" Size="Size.Small">@cat.Title</MudChip>
}
</MudStack>
}
<!-- Price & Discount -->
<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>
@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">
<MudChip T="string" Variant="Variant.Outlined" Size="Size.Small"
Color="@(_product.RemainingCount > 0 ? Color.Success : Color.Error)"
Icon="@Icons.Material.Filled.Inventory">
@(_product.RemainingCount > 0 ? $"موجود ({_product.RemainingCount} عدد)" : "ناموجود")
</MudChip>
<MudChip T="string" Variant="Variant.Outlined" Size="Size.Small" Color="Color.Default"
Icon="@Icons.Material.Filled.Visibility">
@_product.ViewCount بازدید
</MudChip>
</MudStack>
<!-- Short Info -->
@if (!string.IsNullOrWhiteSpace(_product.ShortInfo))
{
<MudText Typo="Typo.body1" Class="mud-text-secondary">@_product.ShortInfo</MudText>
}
<!-- Add to Cart -->
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center" Class="flex-wrap">
<MudNumericField T="int" @bind-Value="_quantity" Min="1"
Max="@(_product.RemainingCount > 0 ? _product.RemainingCount : 1)"
Label="تعداد" Variant="Variant.Outlined" Margin="Margin.Dense"
Style="width:100px;" />
<MudButton Variant="Variant.Filled" Color="Color.Success"
StartIcon="@Icons.Material.Filled.AddShoppingCart"
OnClick="AddToCart"
Disabled="@(_product.RemainingCount <= 0 || _addingToCart)"
Size="Size.Large">
@(_addingToCart ? "در حال افزودن..." : "افزودن به سبد")
</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.ShoppingCart"
Href="@RouteConstants.DiscountStore.Cart">
مشاهده سبد
</MudButton>
</MudStack>
</MudStack>
</MudItem>
</MudGrid>
<!-- Full Information -->
@if (!string.IsNullOrWhiteSpace(_product.FullInfo))
{
<MudPaper Elevation="1" Class="pa-4 pa-md-6 mt-6 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-3">توضیحات محصول</MudText>
<MudDivider Class="mb-4" />
<div class="blog-content">
@((MarkupString)_product.FullInfo)
</div>
</MudPaper>
}
}
</MudContainer>
@@ -0,0 +1,64 @@
using Microsoft.AspNetCore.Components;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.DiscountStore;
public partial class ProductDetail
{
[Parameter] public long Id { get; set; }
[Inject] private DiscountProductService DiscountProductService { get; set; } = default!;
[Inject] private DiscountCartService DiscountCartService { get; set; } = default!;
private DiscountProductDetail? _product;
private string _selectedImage = string.Empty;
private int _quantity = 1;
private bool _loading = true;
private bool _addingToCart;
protected override async Task OnInitializedAsync()
{
await LoadProductAsync();
}
private async Task LoadProductAsync()
{
_loading = true;
try
{
_product = await DiscountProductService.GetByIdAsync(Id);
if (_product is not null)
{
_selectedImage = _product.Images.FirstOrDefault()?.ImageUrl
?? _product.ThumbnailUrl;
}
}
catch
{
Snackbar.Add("خطا در بارگذاری محصول", MudBlazor.Severity.Error);
}
finally
{
_loading = false;
}
}
private async Task AddToCart()
{
if (_product is null) return;
_addingToCart = true;
try
{
await DiscountCartService.AddAsync(_product.Id, _quantity);
Snackbar.Add($"{_product.Title} به سبد خرید اضافه شد", MudBlazor.Severity.Success);
}
catch
{
Snackbar.Add("خطا در افزودن به سبد خرید", MudBlazor.Severity.Error);
}
finally
{
_addingToCart = false;
}
}
}
@@ -0,0 +1,133 @@
@attribute [Route(RouteConstants.DiscountStore.Products)]
<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.body2" Align="Align.Center" Class="dash-hero-sub">
خرید با استفاده از موجودی کیف پول تخفیفی
</MudText>
</MudStack>
</MudPaper>
<!-- Filters -->
<MudPaper Elevation="1" Class="pa-3 pa-md-4 mb-4 rounded-lg">
<MudGrid Spacing="2" AlignItems="AlignItems.Center">
<MudItem xs="12" sm="5">
<MudTextField @bind-Value="_search"
Placeholder="جستجو در محصولات..."
AdornmentIcon="@Icons.Material.Filled.Search"
Adornment="Adornment.Start"
Immediate="true"
OnKeyUp="OnSearchKeyUp"
Variant="Variant.Outlined"
Margin="Margin.Dense" />
</MudItem>
<MudItem xs="8" sm="5">
<MudSelect T="long?" Value="_selectedCategoryId" ValueChanged="OnCategoryChanged"
Label="دسته‌بندی" Variant="Variant.Outlined" Margin="Margin.Dense"
Clearable="true" AnchorOrigin="Origin.BottomCenter">
@foreach (var cat in _categories)
{
<MudSelectItem T="long?" Value="@((long?)cat.Id)">@cat.Title (@cat.ProductCount)</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="4" sm="2" Class="d-flex gap-2">
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
StartIcon="@Icons.Material.Filled.Search" OnClick="SearchProducts"
Style="height:40px;">جستجو</MudButton>
</MudItem>
<MudItem xs="12" Class="d-flex justify-start justify-md-end gap-2">
<MudButton Size="Size.Small" Variant="Variant.Filled" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.ShoppingCart"
Href="@RouteConstants.DiscountStore.Cart">
سبد خرید
@if (DiscountCart.Count > 0)
{
<MudBadge Content="@DiscountCart.Count" Color="Color.Error" Overlap="true" Class="ms-2" />
}
</MudButton>
</MudItem>
</MudGrid>
</MudPaper>
<!-- Loading -->
@if (_loading)
{
<MudStack AlignItems="AlignItems.Center" Class="py-8">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
<MudText Class="mt-2 mud-text-secondary">در حال بارگذاری...</MudText>
</MudStack>
}
else if (_result.Products.Count == 0)
{
<MudAlert Severity="Severity.Info" Class="my-4">محصولی یافت نشد.</MudAlert>
}
else
{
<!-- Products Grid -->
<MudGrid Spacing="1">
@foreach (var p in _result.Products)
{
<MudItem xs="6" sm="6" md="3"
onclick="@(() => NavigateToProduct(p.Id))">
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden"
Style="cursor:pointer;height: 300px">
<MudCardContent Class="d-flex flex-column pa-1 h-100">
<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;">
<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.Error" 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">@p.Title</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">
<MudButton Variant="Variant.Filled" Color="Color.Primary"
OnClick="@(() => AddToCart(p))"
StartIcon="@Icons.Material.Filled.AddShoppingCart"
Disabled="@(p.RemainingCount <= 0)">
@(p.RemainingCount <= 0 ? "ناموجود" : "افزودن")
</MudButton>
</MudCardActions>
</MudCard>
</MudItem>
}
</MudGrid>
<!-- Pagination -->
@if (_result.TotalPages > 1)
{
<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>
}
}
</MudContainer>
@@ -0,0 +1,87 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.DiscountStore;
public partial class Products : ComponentBase, IDisposable
{
[Inject] private DiscountProductService ProductService { get; set; } = default!;
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
private string _search = string.Empty;
private long? _selectedCategoryId;
private int _currentPage = 1;
private bool _loading;
private const int PageSize = 12;
private DiscountProductListResult _result = new(new(), 0, 0, 1);
private List<DiscountCategoryNode> _categories = new();
protected override async Task OnInitializedAsync()
{
_loading = true;
await DiscountCart.EnsureInitializedAsync();
DiscountCart.OnChange += StateHasChanged;
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;
}
private async Task LoadProductsAsync()
{
_loading = true;
StateHasChanged();
_result = await ProductService.GetProductsAsync(
page: _currentPage,
pageSize: PageSize,
search: string.IsNullOrWhiteSpace(_search) ? null : _search,
categoryId: _selectedCategoryId);
_loading = false;
StateHasChanged();
}
private async Task SearchProducts() => await LoadProductsAsync();
private async Task OnSearchKeyUp(KeyboardEventArgs e)
{
if (e.Key == "Enter")
{
_currentPage = 1;
await LoadProductsAsync();
}
}
private async Task OnCategoryChanged(long? value)
{
_selectedCategoryId = value;
_currentPage = 1;
await LoadProductsAsync();
}
private async Task OnPageChanged(int page)
{
_currentPage = page;
await LoadProductsAsync();
}
private async Task AddToCart(DiscountProductCard p)
{
await DiscountCart.AddAsync(p.Id);
}
private void NavigateToProduct(long id)
{
Navigation.NavigateTo($"{RouteConstants.DiscountStore.ProductDetail}{id}");
}
private static string FormatPrice(long price) => $"{price:N0} تومان";
public void Dispose()
{
DiscountCart.OnChange -= StateHasChanged;
}
}
+1 -1
View File
@@ -116,7 +116,7 @@
<!-- Contact Support -->
<section class="py-12 bg-grey-50">
<MudContainer MaxWidth="MaxWidth.Large">
<MudPaper Elevation="3" Class="pa-8 text-center">
<MudPaper Elevation="2" Class="pa-8 text-center">
<MudIcon Icon="@Icons.Material.Filled.ContactSupport" 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">
@@ -0,0 +1,62 @@
@attribute [Route(RouteConstants.Gateway.CartChooser)]
<PageTitle>سبد خرید</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6 py-md-10">
<PageHeader Title="سبد خرید" BackHref="@RouteConstants.Profile.Index" />
<MudStack AlignItems="AlignItems.Center" Spacing="4">
<MudIcon Icon="@Icons.Material.Filled.ShoppingCart" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.body1" Align="Align.Center" Class="mud-text-secondary">
سبد خرید کدام فروشگاه را می‌خواهید مشاهده کنید؟
</MudText>
<MudGrid Spacing="4" Justify="Justify.Center" Class="mt-4">
<MudItem xs="12" sm="6">
<MudLink Href="@RouteConstants.Store.Cart" Underline="Underline.None" Class="store-chooser-link">
<MudPaper Elevation="2" Class="pa-6 rounded-xl text-center store-chooser-card store-chooser-regular">
<MudAvatar Size="Size.Large" Class="mx-auto mb-3 gateway-avatar-sm-store">
<MudIcon Icon="@Icons.Material.Filled.Storefront" Size="Size.Large" Class="text-brand-store" />
</MudAvatar>
<MudText Typo="Typo.h6" Class="fw-semibold">سبد خرید فروشگاه</MudText>
@if (_cartCount > 0)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Filled" Size="Size.Small" Class="mt-2">
@_cartCount محصول
</MudChip>
}
else
{
<MudText Typo="Typo.caption" Class="mud-text-secondary mt-1">سبد خالی</MudText>
}
</MudPaper>
</MudLink>
</MudItem>
<MudItem xs="12" sm="6">
<MudLink Href="@RouteConstants.DiscountStore.Cart" Underline="Underline.None" Class="store-chooser-link">
<MudPaper Elevation="2" Class="pa-6 rounded-xl text-center store-chooser-card store-chooser-discount">
<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>
@if (_discountCartCount > 0)
{
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small" Class="mt-2">
@_discountCartCount محصول
</MudChip>
}
else
{
<MudText Typo="Typo.caption" Class="mud-text-secondary mt-1">سبد خالی</MudText>
}
</MudPaper>
</MudLink>
</MudItem>
</MudGrid>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack"
OnClick="GoBack" Class="mt-4">
بازگشت به داشبورد
</MudButton>
</MudStack>
</MudContainer>
@@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.Gateway;
public partial class CartChooser : IDisposable
{
[Inject] private CartService CartService { get; set; } = default!;
[Inject] private DiscountCartService DiscountCartService { get; set; } = default!;
[Inject] private IJSRuntime JS { get; set; } = default!;
private int _cartCount;
private int _discountCartCount;
protected override async Task OnInitializedAsync()
{
await CartService.EnsureInitializedAsync();
await DiscountCartService.EnsureInitializedAsync();
_cartCount = CartService.Count;
_discountCartCount = DiscountCartService.Count;
CartService.OnChange += OnCartChanged;
DiscountCartService.OnChange += OnDiscountCartChanged;
}
private void OnCartChanged()
{
_cartCount = CartService.Count;
InvokeAsync(StateHasChanged);
}
private void OnDiscountCartChanged()
{
_discountCartCount = DiscountCartService.Count;
InvokeAsync(StateHasChanged);
}
public void Dispose()
{
CartService.OnChange -= OnCartChanged;
DiscountCartService.OnChange -= OnDiscountCartChanged;
}
private async Task GoBack() => await JS.InvokeVoidAsync("history.back");
}
@@ -0,0 +1,49 @@
@attribute [Route(RouteConstants.Gateway.OrdersChooser)]
@inject IJSRuntime JS
<PageTitle>سفارشات من</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6 py-md-10">
<PageHeader Title="سفارشات من" BackHref="@RouteConstants.Profile.Index" />
<MudStack AlignItems="AlignItems.Center" Spacing="4">
<MudIcon Icon="@Icons.Material.Filled.Receipt" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.body1" Align="Align.Center" Class="mud-text-secondary">
سفارشات کدام فروشگاه را می‌خواهید مشاهده کنید؟
</MudText>
<MudGrid Spacing="4" Justify="Justify.Center" Class="mt-4">
<MudItem xs="12" sm="6">
<MudLink Href="@RouteConstants.Store.Orders" Underline="Underline.None" Class="store-chooser-link">
<MudPaper Elevation="2" Class="pa-6 rounded-xl text-center store-chooser-card store-chooser-regular">
<MudAvatar Size="Size.Large" Class="mx-auto mb-3 gateway-avatar-sm-store">
<MudIcon Icon="@Icons.Material.Filled.Storefront" Size="Size.Large" Class="text-brand-store" />
</MudAvatar>
<MudText Typo="Typo.h6" Class="fw-semibold">سفارشات فروشگاه</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary mt-1">سفارشات خرید معمولی</MudText>
</MudPaper>
</MudLink>
</MudItem>
<MudItem xs="12" sm="6">
<MudLink Href="@RouteConstants.DiscountStore.Orders" Underline="Underline.None" Class="store-chooser-link">
<MudPaper Elevation="2" Class="pa-6 rounded-xl text-center store-chooser-card store-chooser-discount">
<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>
</MudPaper>
</MudLink>
</MudItem>
</MudGrid>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack"
OnClick="GoBack" Class="mt-4">
بازگشت به داشبورد
</MudButton>
</MudStack>
</MudContainer>
@code {
private async Task GoBack() => await JS.InvokeVoidAsync("history.back");
}
@@ -0,0 +1,56 @@
@attribute [Route(RouteConstants.Gateway.StoreChooser)]
<PageTitle>فروشگاه‌ها</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6 py-md-10">
<PageHeader Title="انتخاب فروشگاه" BackHref="@RouteConstants.Profile.Index" />
<MudStack AlignItems="AlignItems.Center" Spacing="4">
<MudText Typo="Typo.body1" Align="Align.Center" Class="mud-text-secondary" Style="max-width:500px;">
برای خرید، یکی از فروشگاه‌ها را انتخاب کنید. هر فروشگاه سبد خرید و سفارشات مجزای خود را دارد.
</MudText>
<MudGrid Spacing="4" Justify="Justify.Center" Class="mt-4">
<!-- Regular Store -->
<MudItem xs="12" sm="6">
<MudLink Href="@RouteConstants.Store.Products" Underline="Underline.None" Class="store-chooser-link">
<MudPaper Elevation="2" Class="pa-6 rounded-xl text-center store-chooser-card store-chooser-regular">
<MudAvatar Size="Size.Large" Class="mx-auto mb-3 gateway-avatar-store">
<MudIcon Icon="@Icons.Material.Filled.Storefront" Size="Size.Large" Class="text-brand-store" />
</MudAvatar>
<MudText Typo="Typo.h5" Class="fw-bold">فروشگاه</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-2">
خرید محصولات با پرداخت از کیف پول اعتباری
</MudText>
@if (_cartCount > 0)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Filled" Size="Size.Small" Class="mt-3">
@_cartCount محصول در سبد خرید
</MudChip>
}
</MudPaper>
</MudLink>
</MudItem>
<!-- Discount Store -->
<MudItem xs="12" sm="6">
<MudLink Href="@RouteConstants.DiscountStore.Products" Underline="Underline.None" Class="store-chooser-link">
<MudPaper Elevation="2" Class="pa-6 rounded-xl text-center store-chooser-card store-chooser-discount">
<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.body2" Class="mud-text-secondary mt-2">
خرید با استفاده از موجودی کیف تخفیفی + درگاه پرداخت
</MudText>
@if (_discountCartCount > 0)
{
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small" Class="mt-3">
@_discountCartCount محصول در سبد خرید
</MudChip>
}
</MudPaper>
</MudLink>
</MudItem>
</MudGrid>
</MudStack>
</MudContainer>
@@ -0,0 +1,43 @@
using Microsoft.AspNetCore.Components;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.Gateway;
public partial class StoreChooser : IDisposable
{
[Inject] private CartService CartService { get; set; } = default!;
[Inject] private DiscountCartService DiscountCartService { get; set; } = default!;
private int _cartCount;
private int _discountCartCount;
protected override async Task OnInitializedAsync()
{
await CartService.EnsureInitializedAsync();
await DiscountCartService.EnsureInitializedAsync();
_cartCount = CartService.Count;
_discountCartCount = DiscountCartService.Count;
CartService.OnChange += OnCartChanged;
DiscountCartService.OnChange += OnDiscountCartChanged;
}
private void OnCartChanged()
{
_cartCount = CartService.Count;
InvokeAsync(StateHasChanged);
}
private void OnDiscountCartChanged()
{
_discountCartCount = DiscountCartService.Count;
InvokeAsync(StateHasChanged);
}
public void Dispose()
{
CartService.OnChange -= OnCartChanged;
DiscountCartService.OnChange -= OnDiscountCartChanged;
}
}
+318 -329
View File
@@ -1,354 +1,343 @@
@attribute [Route(RouteConstants.Main.MainPage)]
@inject NavigationManager Navigation
@inject IJSRuntime JS
<PageTitle>صفحه اصلی</PageTitle>
<PageTitle>صفحه اصلی | کارا بازار سلامت</PageTitle>
<!-- HERO -->
<section id="hero" class="hero-section">
<MudContainer MaxWidth="MaxWidth.Large" Class="py-16">
<MudGrid Justify="Justify.Center" Spacing="3">
<MudItem xs="12" md="6">
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled" Class="mb-3">پیشنهاد ویژه برای شروع</MudChip>
<MudStack Spacing="2">
<MudText Typo="Typo.h1">
با دعوت از دوستان خود، از خریدهای واقعی پاداش بگیرید
</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mb-6">
ثبت‌نام آسان، تجربهٔ سریع و شفاف. همین امروز حساب بساز و با دعوت دوستانت، امتیاز و پاداش دریافت کن.
</MudText>
@* ═══════════════════════════════════════════════
1. HERO — Bold, minimal, centered
═══════════════════════════════════════════════ *@
<section class="hero-gradient py-12 py-md-16">
<MudContainer MaxWidth="MaxWidth.Medium">
<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">
🌿 پلتفرم سلامت‌محور فروش و تیم‌سازی
</MudChip>
<MudStack Row="true" Spacing="3" Class="mb-2">
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Large"
Class="mud-ripple rounded-pill"
OnClick="@(() => Navigation.NavigateTo("/pricing"))">مشاهده قیمت‌ها</MudButton>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
Class="mud-ripple rounded-pill"
OnClick="@(() => NavigateToRegistrationWizard())">شروع کن</MudButton>
</MudStack>
<MudDivider Class="my-2" Style="width: 90%;" />
<MudStack Row="true" Spacing="1">
<MudChip T="string"
Color="Color.Info"
Variant="Variant.Outlined"
Class="mb-3">ثبت‌نام زیر ۲ دقیقه</MudChip>
<MudChip T="string"
Color="Color.Info"
Variant="Variant.Outlined"
Class="mb-3">پشتیبانی ۷×۲۴</MudChip>
<MudChip T="string"
Color="Color.Info"
Variant="Variant.Outlined"
Class="mb-3">پرداخت ایمن</MudChip>
<MudText Typo="Typo.h1" Class="hero-title" Style="max-width:680px;">
رشد تیم، فروش واقعی، پاداش شفاف
</MudText>
<MudText Typo="Typo.body1" Class="hero-subtitle" Style="max-width:520px;">
با دعوت از دوستان، از خریدهای واقعی محصولات سلامت پاداش بگیرید. ثبت‌نام سریع، داشبورد لحظه‌ای.
</MudText>
<MudStack Row="true" Spacing="2" Class="mt-2 flex-wrap" Justify="Justify.Center">
<MudButton Variant="Variant.Filled"
Class="rounded-pill hero-cta-primary"
Size="Size.Large"
OnClick="NavigateToRegistrationWizard">
شروع رایگان
</MudButton>
<MudButton Variant="Variant.Outlined"
Class="rounded-pill hero-cta-outline"
Size="Size.Large"
OnClick="@(() => Navigation.NavigateTo("/blog"))">
آخرین اخبار
</MudButton>
</MudStack>
@* ── Trust badges ── *@
<MudStack Row="true" Spacing="4" Class="mt-4 flex-wrap" Justify="Justify.Center" AlignItems="AlignItems.Center">
@foreach (var badge in _trustBadges)
{
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@badge.Icon" Size="Size.Small" Style="color:rgba(255,255,255,.7);" />
<MudText Typo="Typo.caption" Style="color:rgba(255,255,255,.8);">@badge.Text</MudText>
</MudStack>
}
</MudStack>
</MudStack>
</MudContainer>
</section>
@* ═══════════════════════════════════════════════
1b. FEATURED POST BANNER — below hero
═══════════════════════════════════════════════ *@
@if (_latestPosts.Any())
{
var featured = _latestPosts.First();
<section class="py-4" style="background:var(--mud-palette-surface);">
<MudContainer MaxWidth="MaxWidth.Medium">
<MudPaper Elevation="1" Class="landing-blog-banner rounded-xl cursor-pointer pa-3"
@onclick="() => NavigateToPost(featured.Slug)">
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center">
@if (!string.IsNullOrWhiteSpace(featured.ThumbnailUrl))
{
<AppImage Path="@featured.ThumbnailUrl"
Alt="@featured.Title"
ObjectFit="ObjectFit.Cover"
Class="landing-blog-thumb"
Fallback="@Icons.Material.Filled.Article" />
}
else
{
<MudAvatar Variant="Variant.Filled" Color="Color.Primary" Size="Size.Large" Class="rounded-lg" Style="min-width:64px;">
<MudIcon Icon="@Icons.Material.Filled.Article" />
</MudAvatar>
}
<MudStack Spacing="1" Style="flex:1; min-width:0;">
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary">
🔥 جدیدترین مطلب
</MudChip>
<MudText Typo="Typo.subtitle1" Class="landing-blog-title">@featured.Title</MudText>
@if (!string.IsNullOrWhiteSpace(featured.Summary))
{
<MudText Typo="Typo.body2" Class="landing-blog-summary mud-text-secondary">@featured.Summary</MudText>
}
</MudStack>
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Class="mud-text-secondary d-none d-sm-flex" />
</MudStack>
</MudItem>
</MudPaper>
</MudContainer>
</section>
}
<MudItem xs="12" md="6">
<MudPaper Class="pa-8 rounded-xl" Style="background: radial-gradient(600px 280px at 120% 0, #daccff 0, transparent 60%), radial-gradient(600px 280px at -10% 100%, #ffe2f2 0, transparent 60%), linear-gradient(180deg, #fff, #fbfaff);">
<MudImage Src="images/team-meeting.jpg"
Alt="جلسه تیم فروش"
ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Style="width:100%"
Class="rounded-xl" />
</MudPaper>
</MudItem>
@* ═══════════════════════════════════════════════
2. HOW IT WORKS — 3 numbered steps
═══════════════════════════════════════════════ *@
<section class="section-landing">
<MudContainer MaxWidth="MaxWidth.Large">
<div class="text-center mb-8 fade-in-up">
<MudText Typo="Typo.h3">سه گام تا شروع</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
بدون پیچیدگی، سریع و آسان
</MudText>
</div>
<MudGrid Spacing="4" Justify="Justify.Center">
@foreach (var (step, i) in _steps.Select((s, i) => (s, i)))
{
<MudItem xs="12" sm="4">
<MudPaper Elevation="0" Class="pa-5 rounded-xl feature-card-v2 text-center fade-in-up" data-delay="@(i * 150)">
<MudAvatar Size="Size.Large" Color="Color.Primary" Variant="Variant.Filled" Class="mx-auto mb-3 landing-step-num">
<MudText Typo="Typo.h5" Style="color:#fff;font-weight:800;">@(i + 1)</MudText>
</MudAvatar>
<MudText Typo="Typo.h6" Class="mb-1">@step.Title</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@step.Desc</MudText>
</MudPaper>
</MudItem>
}
</MudGrid>
</MudContainer>
</section>
<MudStack Spacing="4">
<!-- WHYUS -->
<section class="whyus-section">
<MudContainer MaxWidth="MaxWidth.Large" Class="whyus-section">
<MudText Typo="Typo.h4" GutterBottom="true">
چرا کاربران ما را انتخاب می‌کنند؟
</MudText>
<MudText Typo="Typo.subtitle1" Class="mud-text-secondary mb-8">
سه دلیل روشن برای شروع امروز.
@* ═══════════════════════════════════════════════
3. FEATURES — 6 cards with icons
═══════════════════════════════════════════════ *@
<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">چرا کارا بازار سلامت؟</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2" Style="max-width:540px;margin:0 auto;">
ابزارهایی ساده و قدرتمند برای رشد کسب‌وکار شما
</MudText>
</div>
<MudGrid Spacing="3" Justify="Justify.Center">
<MudItem xs="12" sm="6" md="4">
<MudCard Class="feature-card rounded-xl pa-4" Outlined="true">
<MudStack>
<MudStack Row="true">
<MudIconButton Icon="@Icons.Material.Outlined.VerifiedUser"
Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Small" />
<MudText Typo="Typo.h6" Align="Align.Center" Class="mb-2">ثبت‌نام سریع و ساده</MudText>
</MudStack>
<MudText Class="ps-10 mud-text-secondary">
در چند گام کوتاه، حسابت را بساز و شروع کن.
</MudText>
</MudStack>
</MudCard>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudCard Class="feature-card rounded-xl pa-4" Outlined="true">
<MudStack>
<MudStack Row="true">
<MudIconButton Icon="@Icons.Material.Outlined.StarBorder"
Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Small" />
<MudText Typo="Typo.h6" Align="Align.Center" Class="mb-2">مزایا و پاداش‌های شفاف</MudText>
</MudStack>
<MudText Class="ps-10 mud-text-secondary">
قوانین روشن، دسترسی آسان به سوابق و گزارش‌ها.
</MudText>
</MudStack>
</MudCard>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudCard Class="feature-card rounded-xl pa-4" Outlined="true">
<MudStack>
<MudStack Row="true">
<MudIconButton Icon="@Icons.Material.Outlined.Devices"
Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Small" />
<MudText Typo="Typo.h6" Align="Align.Center" Class="mb-2">اپلیکیشن واکنش‌گرا</MudText>
</MudStack>
<MudText Class="ps-10 mud-text-secondary">
تجربه‌ای روان در موبایل و دسکتاپ.
</MudText>
</MudStack>
</MudCard>
</MudItem>
</MudGrid>
</MudContainer>
</section>
@* <!-- FEATURES: معرفی پکیج‌ها -->
<section id="features" class="py-20">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h4" GutterBottom="true">
پکیج‌های سرمایه گذاری
</MudText>
<MudText Typo="Typo.subtitle1" Class="mud-text-secondary mb-8">
بر اساس اندازهٔ تیم خود انتخاب کنید.
</MudText>
@if (_isLoadingPackages)
<MudGrid Spacing="3" Justify="Justify.Center">
@foreach (var (icon, title, desc, delay) in _features)
{
<MudStack AlignItems="AlignItems.Center" Class="py-8">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large" />
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">در حال بارگذاری پکیج‌ها...</MudText>
</MudStack>
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="0" Class="pa-5 rounded-xl feature-card-v2 fade-in-up" data-delay="@delay">
<MudStack Spacing="2">
<MudAvatar Color="Color.Primary" Variant="Variant.Filled" Size="Size.Medium" Class="mb-1">
<MudIcon Icon="@icon" />
</MudAvatar>
<MudText Typo="Typo.h6">@title</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@desc</MudText>
</MudStack>
</MudPaper>
</MudItem>
}
else if (_packs.Any())
</MudGrid>
</MudContainer>
</section>
@* ═══════════════════════════════════════════════
4. STATS — Counter cards
═══════════════════════════════════════════════ *@
<section class="section-landing">
<MudContainer MaxWidth="MaxWidth.Medium">
<MudGrid Spacing="3" Justify="Justify.Center">
@foreach (var stat in _stats)
{
<MudGrid Spacing="4" Justify="Justify.Center" Class="stretch-grid">
@foreach (var p in _packs)
{
<MudItem xs="12" md="4" Class="d-flex">
<MudCard>
<MudCardMedia Image="@p.Image"
Title="@p.Title"
Height="300" />
<MudCardContent>
<MudText Typo="Typo.h5" Class="mb-3">@(p.Title)</MudText>
@((MarkupString)p.Body)
</MudCardContent>
<MudCardActions Class="ma-2">
<MudText Typo="Typo.h5"
Color="Color.Success"
Align="Align.End">@(p.Price.ToThousands().ToCurrencyUnitIRT())</MudText>
<MudItem xs="6" sm="3">
<MudPaper Elevation="2" Class="pa-4 rounded-xl stat-card fade-in-up text-center">
<MudText Class="stat-number" Color="@stat.Color" id="@stat.ElementId">@stat.Display</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary mt-1">@stat.Label</MudText>
</MudPaper>
</MudItem>
}
</MudGrid>
</MudContainer>
</section>
@* ═══════════════════════════════════════════════
5. LATEST BLOG POSTS
═══════════════════════════════════════════════ *@
@if (_latestPosts.Any())
{
<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">آخرین مقالات</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
جدیدترین مطالب و اخبار کارا بازار سلامت
</MudText>
</div>
<MudGrid Spacing="4" Justify="Justify.Center">
@foreach (var post in _latestPosts.Take(2))
{
<MudItem xs="12" sm="6" md="6">
<MudPaper Elevation="1" Class="rounded-xl blog-card-v2 cursor-pointer fade-in-up" Style="overflow:hidden;"
@onclick="() => NavigateToPost(post.Slug)">
<div class="blog-thumb" style="height:180px; background:#f0f0f0;">
@if (!string.IsNullOrWhiteSpace(post.ThumbnailUrl))
{
<AppImage Path="@post.ThumbnailUrl"
Alt="@post.Title"
ObjectFit="ObjectFit.Cover"
Style="width:100%; height:180px;" />
}
else
{
<div style="height:180px; display:flex; align-items:center; justify-content:center; background:linear-gradient(135deg,#6366f1,#a78bfa);">
<MudIcon Icon="@Icons.Material.Filled.Article" Size="Size.Large" Style="color:rgba(255,255,255,.5);" />
</div>
}
</div>
<div class="pa-4">
@if (post.Categories.Any())
{
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary" Class="mb-2">
@post.Categories.First().Title
</MudChip>
}
<MudText Typo="Typo.h6" Class="mb-1" Style="display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;">
@post.Title
</MudText>
@if (!string.IsNullOrWhiteSpace(post.Summary))
{
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-2" Style="display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;">
@post.Summary
</MudText>
}
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center" Class="mt-2">
@if (post.PublishedAt.HasValue)
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">
<MudIcon Icon="@Icons.Material.Filled.CalendarToday" Size="Size.Small" Class="ml-1" />
@post.PublishedAt.Value.ToString("yyyy/MM/dd")
</MudText>
}
<MudSpacer />
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="@(() => NavigateToPackage(p.Id))">مشاهده جزئیات</MudButton>
</MudCardActions>
</MudCard>
</MudItem>
}
</MudGrid>
}
else
{
<MudStack AlignItems="AlignItems.Center" Class="py-8">
<MudText Typo="Typo.body1" Class="mud-text-secondary">هیچ پکیجی یافت نشد.</MudText>
</MudStack>
}
</MudContainer>
</section>
*@
<!-- STATS -->
<section class="stats-strip">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h4" Align="Align.Center" GutterBottom="true">
کارا بازار سلامت چطور کار می‌کند؟
</MudText>
<MudText Typo="Typo.subtitle1" Align="Align.Center" Class="mud-text-secondary mb-8">
سه گام روشن تا شروع یک ماجراجویی جدید
</MudText>
<MudGrid>
<MudItem xs="12" lg="6">
<MudStack AlignItems="AlignItems.Start">
<MudPaper Outlined="true" Class="pa-6 rounded-xl">
<MudTimeline>
<MudTimelineItem Color="Color.Info" Size="Size.Small">
<ItemOpposite>
<MudText Color="Color.Info" Typo="Typo.h5">ثبت‌نام</MudText>
</ItemOpposite>
<ItemContent>
<MudText Color="Color.Info" Typo="Typo.h6" GutterBottom="true">مرحله اول</MudText>
<MudText>یک حساب بساز و وارد شو.</MudText>
</ItemContent>
</MudTimelineItem>
<MudTimelineItem Color="Color.Success" Size="Size.Small">
<ItemOpposite>
<MudText Color="Color.Success" Typo="Typo.h5">دعوت دوستان</MudText>
</ItemOpposite>
<ItemContent>
<MudText Align="Align.End" Color="Color.Success" Typo="Typo.h6" GutterBottom="true">مرحله دوم</MudText>
<MudText Align="Align.End">لینک دعوتت را به اشتراک بگذار.</MudText>
</ItemContent>
</MudTimelineItem>
<MudTimelineItem Color="Color.Error" Size="Size.Small">
<ItemOpposite>
<MudText Color="Color.Error" Typo="Typo.h5">دریافت پاداش</MudText>
</ItemOpposite>
<ItemContent>
<MudText Color="Color.Error" Typo="Typo.h6" GutterBottom="true">مرحله سوم</MudText>
<MudText>از خریدهای واقعی دوستانت پاداش بگیر.</MudText>
</ItemContent>
</MudTimelineItem>
</MudTimeline>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
<MudIcon Icon="@Icons.Material.Filled.Visibility" Size="Size.Small" Class="ml-1" />
@post.ViewCount
</MudText>
</MudStack>
</div>
</MudPaper>
</MudStack>
</MudItem>
<MudItem xs="1">
<MudDivider Vertical="true" />
</MudItem>
<MudItem xs="12" lg="5" Class="d-flex align-center">
<MudStack>
<MudGrid Spacing="4" Justify="Justify.SpaceAround">
<MudItem xs="6">
<MudPaper Elevation="2" Class="pa-4 rounded-xl ">
<MudText Typo="Typo.h4" Align="Align.Center" Color="Color.Success">+۵۰٪</MudText>
<MudText Typo="Typo.caption" HtmlTag="div" Align="Align.Center" Color="Color.Info">رشد میانگین تیم</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudText Typo="Typo.h4" Align="Align.Center" Color="Color.Primary">۹۹٫۹٪</MudText>
<MudText Typo="Typo.caption" HtmlTag="div" Align="Align.Center" Color="Color.Info">آپ‌تایم سرویس</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudText Typo="Typo.h4" Align="Align.Center" Color="Color.Warning">۳ روز</MudText>
<MudText Typo="Typo.caption" HtmlTag="div" Align="Align.Center" Color="Color.Info">میانگین زمان استقرار</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
<MudText Typo="Typo.h4" Align="Align.Center" Color="Color.Secondary">+۲۰ کشور</MudText>
<MudText Typo="Typo.caption" HtmlTag="div" Align="Align.Center" Color="Color.Info">پوشش ارسال کد</MudText>
</MudPaper>
</MudItem>
</MudGrid>
</MudStack>
</MudItem>
</MudItem>
}
</MudGrid>
</MudContainer>
</section>
<!-- TESTIMONIALS -->
<section id="testimonials" class="py-20 testimonials">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h4" Align="Align.Center" GutterBottom="true">
اعتماد مشتریان
</MudText>
<MudText Typo="Typo.subtitle1" Align="Align.Center" Class="mud-text-secondary mb-8">
بخشی از تجربهٔ استفاده از «کارا بازار سلامت».
</MudText>
<MudGrid Spacing="3" Justify="Justify.Center">
<MudItem xs="12" md="6">
<MudPaper Elevation="4" Class=" pa-5 ">
<MudStack Spacing="2">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudAvatar>
<MudImage ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Src="images/avatar1.jpg"></MudImage>
</MudAvatar>
<div>
<MudText Typo="Typo.subtitle2">شرکت سینا نت</MudText>
<MudText Typo="Typo.overline" Class="mud-text-secondary">مدیر عملیات</MudText>
</div>
</MudStack>
<MudText Typo="Typo.body2">
«با کارا بازار سلامت، محاسبهٔ کارمزدها و پایش تیم‌ها بدون اکسل و دردسر انجام می‌شود.»
</MudText>
</MudStack>
</MudPaper>
</MudItem>
<MudItem xs="12" md="6">
<MudPaper Elevation="4" Class=" pa-5 ">
<MudStack Spacing="2">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudAvatar>
<MudImage ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Src="images/avatar2.jpg"></MudImage>
</MudAvatar>
<div>
<MudText Typo="Typo.subtitle2">هولدینگ آریانا</MudText>
<MudText Typo="Typo.overline" Class="mud-text-secondary">مدیر فروش</MudText>
</div>
</MudStack>
<MudText Typo="Typo.body2">
«شجره‌نامهٔ بصری و گزارش‌های دقیق باعث شد رشد تیم را لحظه‌ای ببینیم.»
</MudText>
</MudStack>
</MudPaper>
</MudItem>
</MudGrid>
</MudContainer>
</section>
<!-- FAQ -->
<section id="faq" class="mb-6">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h4" Align="Align.Center" GutterBottom="true">
سوالات متداول
</MudText>
<MudText Typo="Typo.subtitle1" Align="Align.Center" Class="mud-text-secondary mb-8">
پاسخ به سوالات رایج شما.
</MudText>
<div class="mx-auto max-w-800">
<MudExpansionPanels Square="false" Class=" ">
@foreach (var q in _faqs)
{
<MudExpansionPanel Text="@q.Q">
<MudText Typo="Typo.body2">@q.A</MudText>
</MudExpansionPanel>
}
</MudExpansionPanels>
<div class="text-center mt-6">
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Class="rounded-pill"
Href="/blog">
مشاهده همه مقالات
</MudButton>
</div>
</MudContainer>
</section>
}
</MudStack>
@* ═══════════════════════════════════════════════
6. TESTIMONIALS
═══════════════════════════════════════════════ *@
<section class="section-landing">
<MudContainer MaxWidth="MaxWidth.Large">
<div class="text-center mb-8 fade-in-up">
<MudText Typo="Typo.h3">اعتماد مشتریان</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
بخشی از تجربه استفاده از «کارا بازار سلامت»
</MudText>
</div>
<MudGrid Spacing="4" Justify="Justify.Center">
@foreach (var t in _testimonials)
{
<MudItem xs="12" md="4">
<MudPaper Elevation="2" Class="pa-5 rounded-xl testimonial-card fade-in-up" data-delay="@t.Delay">
<MudStack Spacing="2">
<MudRating ReadOnly="true" SelectedValue="5" Size="Size.Small" Color="Color.Warning" />
<MudText Typo="Typo.body2" Class="mud-text-secondary">
«@t.Quote»
</MudText>
<MudDivider Class="my-1" />
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudAvatar Size="Size.Small" Color="Color.Primary" Variant="Variant.Filled">
<MudIcon Icon="@Icons.Material.Filled.Person" Size="Size.Small" />
</MudAvatar>
<div>
<MudText Typo="Typo.subtitle2">@t.Name</MudText>
<MudText Typo="Typo.overline" Class="mud-text-secondary">@t.Role</MudText>
</div>
</MudStack>
</MudStack>
</MudPaper>
</MudItem>
}
</MudGrid>
</MudContainer>
</section>
@* ═══════════════════════════════════════════════
7. FAQ
═══════════════════════════════════════════════ *@
<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">سوالات متداول</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
پاسخ به سوالات رایج شما
</MudText>
</div>
<div class="fade-in-up">
<MudExpansionPanels Elevation="1" Class="rounded-xl">
@foreach (var q in _faqs)
{
<MudExpansionPanel Text="@q.Q">
<MudText Typo="Typo.body2" Class="mud-text-secondary">@q.A</MudText>
</MudExpansionPanel>
}
</MudExpansionPanels>
</div>
</MudContainer>
</section>
@* ═══════════════════════════════════════════════
8. CTA BANNER
═══════════════════════════════════════════════ *@
<section class="section-landing-lg">
<MudContainer MaxWidth="MaxWidth.Medium">
<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">
آماده شروع هستید؟
</MudText>
<MudText Typo="Typo.body1" Align="Align.Center" Class="hero-subtitle">
همین الان ثبت‌نام کنید و از مزایای کارا بازار سلامت بهره‌مند شوید.
</MudText>
<MudButton Variant="Variant.Filled"
Size="Size.Large"
Class="rounded-pill mt-2 hero-cta-primary"
OnClick="NavigateToRegistrationWizard">
شروع رایگان
</MudButton>
</MudStack>
</div>
</MudContainer>
</section>
+107 -81
View File
@@ -1,44 +1,66 @@
using FrontOffice.BFF.Package.Protobuf.Protos.Package;
using FrontOffice.Main.Utilities;
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor;
using System.Security.Cryptography;
namespace FrontOffice.Main.Pages;
public partial class Index:IDisposable
public partial class Index : IDisposable
{
[Inject] private PackageContract.PackageContractClient PackageClient { get; set; } = default!;
[Inject] private MobileNumberEncryptor Encryptor { get; set; }
private string? _email;
private bool _isLoadingPackages;
private List<Pack> _packs = new();
[Inject] private BlogPostService BlogPostService { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!;
// ── Latest blog posts (loaded from CMS) ──
private List<BlogPostCardDto> _latestPosts = new();
protected override async Task OnInitializedAsync()
{
MainService.OnChangeHandler +=async () =>
MainService.OnChangeHandler += OnStateChanged;
// Load latest published blog posts
// اول پین‌شده‌ها، اگر کافی نبود آخرین پست‌ها اضافه شود
try
{
await InvokeAsync(StateHasChanged);
};
// await LoadPackagesAsync();
//string mobileNumber = "09387342688";
//// انکریپت کردن
//string encrypted = Encryptor.EncryptMobileNumber(mobileNumber);
//Console.WriteLine($"Encrypted: {encrypted}");
//// دیکریپت کردن برای تست
//string decrypted = Encryptor.DecryptMobileNumber(encrypted);
//Console.WriteLine($"Decrypted: {decrypted}");
_latestPosts = await BlogPostService.GetFeaturedPostsAsync(2);
// اگر فقط ۱ پین‌شده داریم، ۱ پست آخر هم اضافه کن
if (_latestPosts.Count < 2)
{
var result = await BlogPostService.GetPublishedPostsAsync(page: 1, pageSize: 2);
var existing = _latestPosts.Select(p => p.Id).ToHashSet();
foreach (var post in result.Posts)
{
if (!existing.Contains(post.Id))
{
_latestPosts.Add(post);
if (_latestPosts.Count >= 2) break;
}
}
}
}
catch
{
_latestPosts = new();
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Init scroll-triggered fade-in animations
await JS.InvokeVoidAsync("initScrollAnimations");
// Animate stat counters
foreach (var stat in _stats)
{
await JS.InvokeVoidAsync("animateCounter", stat.ElementId, stat.Target, 2200, stat.Suffix);
}
}
if (await AuthService.IsAuthenticatedAsync())
{
if ((await AuthService.IsCompleteRegisterAsync()))
if (await AuthService.IsCompleteRegisterAsync())
{
Navigation.NavigateTo(RouteConstants.Profile.Index);
}
@@ -47,56 +69,8 @@ public partial class Index:IDisposable
Navigation.NavigateTo(RouteConstants.Registration.Wizard);
}
}
await base.OnAfterRenderAsync(firstRender);
}
// private async Task LoadPackagesAsync()
// {
// _isLoadingPackages = true;
// try
// {
// var response = await PackageClient.GetAllPackageByFilterAsync(request: new());
// if (response?.Models?.Any() == true)
// {
// _packs = response.Models.Select(p => new Pack(
// Id: p.Id,
// Title: p.Title,
// Body: p.Description,
// Image: UrlUtility.DownloadUrl + p.ImagePath,
// Price: p.Price
// )).ToList();
// }
// else
// _packs = new List<Pack>();
// }
// catch (Exception ex)
// {
// Snackbar.Add($"خطا در بارگذاری پکیج‌ها: {ex.Message}", Severity.Error);
// // Fallback to empty list
// _packs = new List<Pack>();
// }
// finally
// {
// _isLoadingPackages = false;
// await InvokeAsync(StateHasChanged);
// }
// }
private void JoinWaitlist()
{
if (string.IsNullOrWhiteSpace(_email))
{
Snackbar.Add("لطفاً ایمیل معتبر وارد کنید.", Severity.Warning);
return;
}
Snackbar.Add("به لیست انتظار «کارا بازار سلامت» اضافه شدید.", Severity.Success);
_email = string.Empty;
}
private void NavigateToPackage(long packageId)
{
Navigation.NavigateTo($"{RouteConstants.Package.Detail}{packageId}");
await base.OnAfterRenderAsync(firstRender);
}
private void NavigateToRegistrationWizard()
@@ -104,25 +78,77 @@ public partial class Index:IDisposable
Navigation.NavigateTo(RouteConstants.Registration.Wizard);
}
private record Pack(long Id, string Title, string Body, string Image, long Price);
private void NavigateToPost(string slug)
{
Navigation.NavigateTo($"/blog/{slug}");
}
private record Plan(string Name, string Price, bool Highlight, IEnumerable<string> Features);
private async void OnStateChanged()
{
await InvokeAsync(StateHasChanged);
}
private record QA(string Q, string A);
// ── 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("دامنه اختصاصی دارم؛ قابل اتصال است؟", "بله، پشت دامنه و گواهی 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);
public void Dispose()
{
MainService.OnChangeHandler -=async () =>
{
await InvokeAsync(StateHasChanged);
};
MainService.OnChangeHandler -= OnStateChanged;
}
}
@@ -6,14 +6,11 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">آمار باشگاه توسعه دهندگان</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<PageHeader Title="آمار باشگاه توسعه دهندگان" BackHref="@RouteConstants.Profile.Index" />
@if (_isLoading)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
<LoadingState Message="در حال دریافت آمار..." />
}
else if (_statistics is not null)
{
@@ -3,16 +3,7 @@
<PageTitle>پکیج‌های من</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-8">
<!-- Breadcrumb -->
<MudBreadcrumbs Items="_breadcrumbItems" Class="mb-4" />
<!-- Header -->
<MudStack Class="mb-6">
<MudText Typo="Typo.h4">پکیج‌های من</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
وضعیت پکیج‌های خریداری شده و عضویت باشگاه مشتریان
</MudText>
</MudStack>
<PageHeader Title="پکیج‌های من" BackHref="@RouteConstants.Profile.Index" />
@if (_isLoading)
{
@@ -3,13 +3,7 @@
<PageTitle>پکیج‌ها</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-8">
<!-- Header -->
<MudStack Class="mb-6">
<MudText Typo="Typo.h4">پکیج‌های سرمایه‌گذاری</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
با خرید پکیج طلایی، به باشگاه مشتریان بپیوندید و از مزایای ویژه بهره‌مند شوید.
</MudText>
</MudStack>
<PageHeader Title="پکیج‌های سرمایه‌گذاری" BackHref="@RouteConstants.Profile.Index" />
@if (_isLoading)
{
+15 -16
View File
@@ -4,26 +4,25 @@
@if (_isLoading)
{
<MudStack AlignItems="AlignItems.Center" Class="py-16">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large" />
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">در حال بارگذاری...</MudText>
</MudStack>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<LoadingState Message="در حال بارگذاری..." />
</MudContainer>
}
else if (_package == null)
{
<MudStack AlignItems="AlignItems.Center" Class="py-16">
<MudIcon Icon="@Icons.Material.Filled.Error" Size="Size.Large" Color="Color.Error" />
<MudText Typo="Typo.h5" Class="mt-2">پکیج یافت نشد</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">پکیج مورد نظر وجود ندارد یا حذف شده است.</MudText>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="mt-4" OnClick="() => Navigation.NavigateTo(RouteConstants.Main.MainPage)">
بازگشت به صفحه اصلی
</MudButton>
</MudStack>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<EmptyState Icon="@Icons.Material.Filled.Error"
Title="پکیج یافت نشد"
Description="پکیج مورد نظر وجود ندارد یا حذف شده است."
ActionText="بازگشت به صفحه اصلی"
ActionHref="@RouteConstants.Main.MainPage" />
</MudContainer>
}
else
{
<!-- Breadcrumb -->
<MudContainer MaxWidth="MaxWidth.Large" Class="py-4">
<PageHeader Title="جزئیات پکیج" BackHref="@RouteConstants.Package.List" />
<MudBreadcrumbs Items="_breadcrumbItems" />
</MudContainer>
@@ -36,9 +35,9 @@ else
<MudStack Class="mb-4">
<!-- Package Image -->
<MudPaper Class="pa-2 rounded-xl" Style="background: radial-gradient(600px 280px at 120% 0, #daccff 0, transparent 60%), radial-gradient(600px 280px at -10% 100%, #ffe2f2 0, transparent 60%), linear-gradient(180deg, #fff, #fbfaff);">
<MudImage Src="@_package.Image"
<AppImage Path="@_package.Image"
Alt="@_package.Title"
Height="250"
ImgHeight="250"
ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Style="width:100%"
@@ -228,9 +227,9 @@ else
<MudItem xs="12" md="4">
<MudPaper Elevation="2" Class="pa-4 d-flex flex-column h-100 cursor-pointer"
OnClick="() => NavigateToPackage(relatedPackage.Id)">
<MudImage Src="@relatedPackage.Image"
<AppImage Path="@relatedPackage.Image"
Alt="@relatedPackage.Title"
Height="200"
ImgHeight="200"
ObjectFit="ObjectFit.Cover"
Class="rounded-xl mb-3" />
<MudText Typo="Typo.h6" Class="mb-2">@relatedPackage.Title</MudText>
@@ -1,5 +1,5 @@
using Blazored.LocalStorage;
using FrontOffice.BFF.Package.Protobuf.Protos.Package;
using CMSMicroservice.Protobuf.Protos.Package;
using FrontOffice.Main.Shared;
using FrontOffice.Main.Utilities;
using Grpc.Core;
@@ -74,7 +74,7 @@ public partial class PackageDetail : IDisposable
Id = packageResponse.Id,
Title = packageResponse.Title,
Body = packageResponse.Description,
Image = UrlUtility.DownloadUrl + packageResponse.ImagePath,
Image = packageResponse.ImagePath ?? string.Empty,
Specifications = new List<Specification>
{
new() { Name = "ظرفیت", Value = "تا ۲۰۰ عضو", Icon = Icons.Material.Filled.Group },
@@ -4,10 +4,7 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">مدیریت آدرس‌ها</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<PageHeader Title="مدیریت آدرس‌ها" BackHref="@RouteConstants.Profile.Index" />
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="4">
@@ -1,4 +1,5 @@
using FrontOffice.BFF.UserAddress.Protobuf.Protos.UserAddress;
using CMSMicroservice.Protobuf.Protos.UserAddress;
using CMSMicroservice.Protobuf.Protos.UserAddress;
using FrontOffice.Main.Pages.Profile.Components;
using Microsoft.AspNetCore.Components;
using MudBlazor;
@@ -9,7 +10,7 @@ public partial class Addresses : ComponentBase
{
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
private List<GetAllUserAddressByFilterResponseModel> _addresses = new();
private List<CustomerAddressModel> _addresses = new();
private bool _isLoadingAddresses;
protected override async Task OnInitializedAsync()
@@ -22,7 +23,7 @@ public partial class Addresses : ComponentBase
_isLoadingAddresses = true;
try
{
var response = await UserAddressContract.GetAllUserAddressByFilterAsync(new());
var response = await UserAddressContract.GetCustomerAddressesAsync(new());
_addresses = response?.Models?.ToList() ?? new();
}
catch (Exception ex)
@@ -44,7 +45,7 @@ public partial class Addresses : ComponentBase
await LoadAddresses();
}
private async Task OpenEditAddressDialog(GetAllUserAddressByFilterResponseModel address)
private async Task OpenEditAddressDialog(CustomerAddressModel address)
{
var dialog = await DialogService.ShowAsync<EditAddressDialog>("ویرایش آدرس", new DialogParameters<EditAddressDialog>
{
@@ -59,7 +60,7 @@ public partial class Addresses : ComponentBase
{
try
{
await UserAddressContract.SetAddressAsDefaultAsync(new() { Id = id });
await UserAddressContract.SetCustomerDefaultAddressAsync(new() { Id = id });
Snackbar.Add("آدرس پیش‌فرض تغییر کرد.", Severity.Success);
await LoadAddresses();
}
@@ -76,7 +77,7 @@ public partial class Addresses : ComponentBase
{
try
{
await UserAddressContract.DeleteUserAddressAsync(new() { Id = id });
await UserAddressContract.DeleteCustomerAddressAsync(new() { Id = id });
Snackbar.Add("آدرس حذف شد.", Severity.Success);
await LoadAddresses();
}
@@ -4,10 +4,7 @@
<MudContainer MaxWidth="MaxWidth.Small" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">تغییر رمز عبور</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Settings">بازگشت</MudButton>
</MudStack>
<PageHeader Title="تغییر رمز عبور" BackHref="@RouteConstants.Profile.Settings" />
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="3">
@@ -1,10 +1,11 @@
<MudDialog >
@using CMSMicroservice.Protobuf.Protos.City
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h4" Align="Align.Center">افزودن آدرس جدید</MudText>
</TitleContent>
<DialogContent>
<MudForm @ref="_form" Model="_request" Validation="@(_validator.ValidateValue)">
<MudForm @ref="_form" Model="_request">
<MudStack Spacing="3">
<MudTextField @bind-Value="_request.Title"
For="@(() => _request.Title)"
@@ -29,12 +30,12 @@
Required="true"
RequiredError="کد پستی الزامی است." />
<MudAutocomplete T="CityProto.GetAllCitiesByFilterResponseModel"
<MudAutocomplete T="object"
@bind-Value="_selectedCity"
Label="شهر"
Variant="Variant.Outlined"
SearchFunc="SearchCities"
ToStringFunc="@(city => city != null ? $"{city.Native} ({city.StateName})" : string.Empty)"
ToStringFunc="@(city => city != null ? $"{((CMSMicroservice.Protobuf.Protos.City.CityDto)city).Native} ({((CMSMicroservice.Protobuf.Protos.City.CityDto)city).StateName})" : string.Empty)"
Required="true"
RequiredError="شهر الزامی است."
Clearable="true"
@@ -49,8 +50,8 @@
<ItemTemplate Context="city">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.LocationCity" Size="Size.Small" />
<MudText>@city.Native</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">(@city.StateName)</MudText>
<MudText>@(((CMSMicroservice.Protobuf.Protos.City.CityDto)city).Native)</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">(@(((CMSMicroservice.Protobuf.Protos.City.CityDto)city).StateName))</MudText>
</MudStack>
</ItemTemplate>
<NoItemsTemplate>
@@ -1,43 +1,42 @@
using FrontOffice.BFF.UserAddress.Protobuf.Protos.UserAddress;
using FrontOffice.BFF.UserAddress.Protobuf.Validator;
using CMSMicroservice.Protobuf.Protos.UserAddress;
using CMSMicroservice.Protobuf.Protos.City;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using Severity = MudBlazor.Severity;
using CityProto = FrontOffice.BFF.City.Protobuf;
namespace FrontOffice.Main.Pages.Profile.Components;
public record CityModel(long Id, string Name);
public partial class AddAddressDialog : ComponentBase
{
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = default!;
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
[Inject] private CityProto.CityContract.CityContractClient CityContract { get; set; } = default!;
[Inject] private CityContract.CityContractClient CityContract { get; set; } = default!;
private MudForm? _form;
private readonly CreateNewUserAddressRequestValidator _validator = new();
private bool _isSaving;
private CreateNewUserAddressRequest _request = new();
private CityProto.GetAllCitiesByFilterResponseModel? _selectedCity;
private CreateCustomerAddressRequest _request = new();
private object? _selectedCity;
private async Task<IEnumerable<CityProto.GetAllCitiesByFilterResponseModel>> SearchCities(string value,
CancellationToken ct)
private async Task<IEnumerable<object>> SearchCities(string value, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(value) || value.Length < 2)
return Enumerable.Empty<CityProto.GetAllCitiesByFilterResponseModel>();
return Enumerable.Empty<object>();
try
{
var response = await CityContract.GetAllCitiesByFilterAsync(new CityProto.GetAllCitiesByFilterRequest
var response = await CityContract.GetAllCitiesByFilterAsync(new GetAllCitiesByFilterRequest
{
PaginationState = new CityProto.PaginationState { PageNumber = 1, PageSize = 20 },
Filter = new CityProto.GetAllCitiesByFilterFilter { Name = value }
}, cancellationToken: ct);
return response?.Models?.ToList() ?? new List<CityProto.GetAllCitiesByFilterResponseModel>();
PaginationState = new CMSMicroservice.Protobuf.Protos.City.PaginationState { PageNumber = 1, PageSize = 20 },
Filter = new GetAllCitiesByFilterFilter { Name = value }
});
return response?.Cities?.Cast<object>() ?? Enumerable.Empty<object>();
}
catch
{
return Enumerable.Empty<CityProto.GetAllCitiesByFilterResponseModel>();
return Enumerable.Empty<object>();
}
}
@@ -53,11 +52,11 @@ public partial class AddAddressDialog : ComponentBase
return;
}
_request.CityId = _selectedCity.Id;
_request.CityId = (long)((dynamic)_selectedCity).Id;
_isSaving = true;
try
{
var response = await UserAddressContract.CreateNewUserAddressAsync(_request);
var response = await UserAddressContract.CreateCustomerAddressAsync(_request);
Snackbar.Add("آدرس با موفقیت اضافه شد.", Severity.Success);
MudDialog.Close(DialogResult.Ok(true));
}
@@ -1,9 +1,10 @@
@using CMSMicroservice.Protobuf.Protos.City
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h4" Align="Align.Center">ویرایش آدرس</MudText>
</TitleContent>
<DialogContent>
<MudForm @ref="_form" Model="_request" Validation="@(_validator.ValidateValue)">
<MudForm @ref="_form" Model="_request">
<MudStack Spacing="3">
<MudTextField @bind-Value="_request.Title"
For="@(() => _request.Title)"
@@ -28,12 +29,12 @@
Required="true"
RequiredError="کد پستی الزامی است." />
<MudAutocomplete T="CityProto.GetAllCitiesByFilterResponseModel"
<MudAutocomplete T="object"
@bind-Value="_selectedCity"
Label="شهر"
Variant="Variant.Outlined"
SearchFunc="SearchCities"
ToStringFunc="@(city => city != null ? $"{city.Native} ({city.StateName})" : string.Empty)"
ToStringFunc="@(city => city != null ? $"{((CMSMicroservice.Protobuf.Protos.City.CityDto)city).Native} ({((CMSMicroservice.Protobuf.Protos.City.CityDto)city).StateName})" : string.Empty)"
Required="true"
RequiredError="شهر الزامی است."
Clearable="true"
@@ -48,8 +49,8 @@
<ItemTemplate Context="city">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.LocationCity" Size="Size.Small" />
<MudText>@city.Native</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">(@city.StateName)</MudText>
<MudText>@(((CMSMicroservice.Protobuf.Protos.City.CityDto)city).Native)</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">(@(((CMSMicroservice.Protobuf.Protos.City.CityDto)city).StateName))</MudText>
</MudStack>
</ItemTemplate>
<NoItemsTemplate>
@@ -1,10 +1,9 @@
using FrontOffice.BFF.UserAddress.Protobuf.Protos.UserAddress;
using FrontOffice.BFF.UserAddress.Protobuf.Validator;
using CMSMicroservice.Protobuf.Protos.UserAddress;
using CMSMicroservice.Protobuf.Protos.City;
using Mapster;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using Severity = MudBlazor.Severity;
using CityProto = FrontOffice.BFF.City.Protobuf;
namespace FrontOffice.Main.Pages.Profile.Components;
@@ -12,22 +11,21 @@ public partial class EditAddressDialog : ComponentBase
{
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = default!;
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
[Inject] private CityProto.CityContract.CityContractClient CityContract { get; set; } = default!;
[Inject] private CityContract.CityContractClient CityContract { get; set; } = default!;
[Parameter] public GetAllUserAddressByFilterResponseModel? Model { get; set; }
[Parameter] public CustomerAddressModel? Model { get; set; }
private MudForm? _form;
private readonly UpdateUserAddressRequestValidator _validator = new();
private bool _isSaving;
private UpdateUserAddressRequest _request = new();
private CityProto.GetAllCitiesByFilterResponseModel? _selectedCity;
private UpdateCustomerAddressRequest _request = new();
private object? _selectedCity;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
if (Model != null)
{
_request = Model.Adapt<UpdateUserAddressRequest>();
_request = Model.Adapt<UpdateCustomerAddressRequest>();
// Load selected city if CityId exists
if (Model.CityId > 0)
@@ -41,13 +39,13 @@ public partial class EditAddressDialog : ComponentBase
{
try
{
var response = await CityContract.GetAllCitiesByFilterAsync(new CityProto.GetAllCitiesByFilterRequest
var response = await CityContract.GetAllCitiesByFilterAsync(new GetAllCitiesByFilterRequest
{
PaginationState = new CityProto.PaginationState { PageNumber = 1, PageSize = 1 },
Filter = new CityProto.GetAllCitiesByFilterFilter { Id = cityId }
PaginationState = new CMSMicroservice.Protobuf.Protos.City.PaginationState { PageNumber = 1, PageSize = 1 },
Filter = new GetAllCitiesByFilterFilter { Id = cityId }
});
_selectedCity = response?.Models?.FirstOrDefault();
_selectedCity = response?.Cities?.FirstOrDefault();
}
catch
{
@@ -55,24 +53,24 @@ public partial class EditAddressDialog : ComponentBase
}
}
private async Task<IEnumerable<CityProto.GetAllCitiesByFilterResponseModel>> SearchCities(string value, CancellationToken ct)
private async Task<IEnumerable<object>> SearchCities(string value, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(value) || value.Length < 2)
return Enumerable.Empty<CityProto.GetAllCitiesByFilterResponseModel>();
return Enumerable.Empty<object>();
try
{
var response = await CityContract.GetAllCitiesByFilterAsync(new CityProto.GetAllCitiesByFilterRequest
var response = await CityContract.GetAllCitiesByFilterAsync(new GetAllCitiesByFilterRequest
{
PaginationState = new CityProto.PaginationState { PageNumber = 1, PageSize = 20 },
Filter = new CityProto.GetAllCitiesByFilterFilter { Native = value }
}, cancellationToken: ct);
return response?.Models?.ToList() ?? new List<CityProto.GetAllCitiesByFilterResponseModel>();
PaginationState = new CMSMicroservice.Protobuf.Protos.City.PaginationState { PageNumber = 1, PageSize = 20 },
Filter = new GetAllCitiesByFilterFilter { Name = value }
});
return response?.Cities?.Cast<object>() ?? Enumerable.Empty<object>();
}
catch
{
return Enumerable.Empty<CityProto.GetAllCitiesByFilterResponseModel>();
return Enumerable.Empty<object>();
}
}
@@ -88,11 +86,11 @@ public partial class EditAddressDialog : ComponentBase
return;
}
_request.CityId = _selectedCity.Id;
_request.CityId = (long)((dynamic)_selectedCity).Id;
_isSaving = true;
try
{
await UserAddressContract.UpdateUserAddressAsync(_request);
await UserAddressContract.UpdateCustomerAddressAsync(_request);
Snackbar.Add("آدرس با موفقیت ویرایش شد.", Severity.Success);
MudDialog.Close(DialogResult.Ok(true));
}
@@ -7,12 +7,12 @@
<MudSelect T="int" @bind-Value="_selectedDepth" Label="عمق" Variant="Variant.Outlined"
Dense="true" Margin="Margin.Dense" Style="width: 90px; min-width: 90px;">
<MudSelectItem Value="2">2 سطح</MudSelectItem>
<MudSelectItem Value="3">3 سطح</MudSelectItem>
<MudSelectItem Value="4">4 سطح</MudSelectItem>
<MudSelectItem Value="5">5 سطح</MudSelectItem>
<MudSelectItem Value="6">6 سطح</MudSelectItem>
<MudSelectItem Value="15">همه</MudSelectItem>
<MudSelectItem T="int" Value="2">2 سطح</MudSelectItem>
<MudSelectItem T="int" Value="3">3 سطح</MudSelectItem>
<MudSelectItem T="int" Value="4">4 سطح</MudSelectItem>
<MudSelectItem T="int" Value="5">5 سطح</MudSelectItem>
<MudSelectItem T="int" Value="6">6 سطح</MudSelectItem>
<MudSelectItem T="int" Value="15">همه</MudSelectItem>
</MudSelect>
<MudButtonGroup Variant="Variant.Outlined" Size="Size.Small" OverrideStyles="false">
@@ -9,9 +9,9 @@
@if (!string.IsNullOrWhiteSpace(node.Avatar))
{
<MudAvatar Size="@GetAvatarSize(Level)">
<MudImage ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Src="@node.Avatar" />
<AppImage Path="@node.Avatar"
ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center" />
</MudAvatar>
}
else
@@ -0,0 +1,64 @@
@attribute [Route(RouteConstants.Profile.Hub)]
<PageTitle>پروفایل</PageTitle>
<MudContainer MaxWidth="MaxWidth.Small" Class="py-6">
<MudStack AlignItems="AlignItems.Center" Spacing="3" Class="mb-6">
<MudAvatar Size="Size.Large" Color="Color.Primary" Variant="Variant.Filled" Class="profile-hub-avatar">
<MudIcon Icon="@Icons.Material.Outlined.Person" Size="Size.Large" />
</MudAvatar>
<MudStack Spacing="0" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h6" Class="fw-bold">
@($"{_userProfile.FirstName} {_userProfile.LastName}")
</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@(_userProfile.Mobile)</MudText>
</MudStack>
</MudStack>
<MudStack Spacing="2">
<MudLink Href="@RouteConstants.Profile.Personal" Underline="Underline.None">
<MudPaper Elevation="0" Class="pa-4 rounded-xl profile-hub-item">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
<MudIcon Icon="@Icons.Material.Outlined.Person" Color="Color.Primary" />
<MudStack Spacing="0">
<MudText Typo="Typo.subtitle2">اطلاعات شخصی</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">نام، کد ملی و تاریخ تولد</MudText>
</MudStack>
</MudStack>
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Class="mud-text-secondary" />
</MudStack>
</MudPaper>
</MudLink>
<MudLink Href="@RouteConstants.Profile.Addresses" Underline="Underline.None">
<MudPaper Elevation="0" Class="pa-4 rounded-xl profile-hub-item">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
<MudIcon Icon="@Icons.Material.Outlined.LocationOn" Color="Color.Success" />
<MudStack Spacing="0">
<MudText Typo="Typo.subtitle2">آدرس‌ها</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">مدیریت آدرس‌های تحویل</MudText>
</MudStack>
</MudStack>
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Class="mud-text-secondary" />
</MudStack>
</MudPaper>
</MudLink>
<MudLink Href="@RouteConstants.Profile.Settings" Underline="Underline.None">
<MudPaper Elevation="0" Class="pa-4 rounded-xl profile-hub-item">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
<MudIcon Icon="@Icons.Material.Outlined.Settings" Color="Color.Warning" />
<MudStack Spacing="0">
<MudText Typo="Typo.subtitle2">تنظیمات</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">تغییر رمز و تنظیمات حساب</MudText>
</MudStack>
</MudStack>
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Class="mud-text-secondary" />
</MudStack>
</MudPaper>
</MudLink>
</MudStack>
</MudContainer>
@@ -0,0 +1,34 @@
using CMSMicroservice.Protobuf.Protos.User;
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
namespace FrontOffice.Main.Pages.Profile;
public partial class Hub
{
[Inject] private UserContract.UserContractClient UserContract { get; set; } = default!;
private GetUserResponse _userProfile = new();
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
await LoadUserProfile();
}
}
private async Task LoadUserProfile()
{
try
{
_userProfile = await UserContract.GetUserAsync(request: new());
}
catch
{
_userProfile = new GetUserResponse();
}
StateHasChanged();
}
}
+183 -245
View File
@@ -5,299 +5,237 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudGrid Spacing="4">
<!-- Profile Header -->
@* ═══════════════════════════════════════════════
1. PROFILE HEADER — Modern gradient card
═══════════════════════════════════════════════ *@
<MudItem xs="12">
<MudPaper Elevation="4" Class="pa-6">
<MudStack Spacing="4">
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center">
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center">
<MudAvatar Size="Size.Large" Color="Color.Primary">
<MudIcon Icon="@Icons.Material.Filled.Person" Size="Size.Large" />
</MudAvatar>
<div>
<MudText Typo="Typo.h5">@($"{_userProfile.FirstName} {_userProfile.LastName}")</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@(_userProfile.Mobile)</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">عضو از @(_userProfile.MobileVerifiedAt?.ToDateTime().MiladiToJalali())</MudText>
</div>
</MudStack>
<MudSpacer/>
<MudButton Variant="Variant.Outlined" Href="https://dayadiamond.ir/" >سامانه دایا</MudButton>
<MudPaper Elevation="0" Class="dash-header-card pa-5 pa-md-6 rounded-xl">
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center" Class="flex-wrap">
<MudAvatar Size="Size.Large" Class="dash-avatar">
<MudIcon Icon="@Icons.Material.Filled.Person" Size="Size.Large" />
</MudAvatar>
<MudStack Spacing="0" Class="flex-grow-1">
<MudText Typo="Typo.h5" Class="dash-hero-name">
@($"{_userProfile.FirstName} {_userProfile.LastName}")
</MudText>
<MudText Typo="Typo.body2" Class="dash-hero-sub">
@(_userProfile.Mobile)
</MudText>
<MudText Typo="Typo.caption" Class="dash-hero-hint">
عضو از @(_userProfile.MobileVerifiedAt?.ToDateTime().MiladiToJalali())
</MudText>
</MudStack>
<!-- Referral Code Section -->
<MudDivider />
@if (CanShowReferralLink)
{
<MudStack Spacing="3">
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudText Typo="Typo.subtitle1">کد دعوت شما</MudText>
<MudSpacer />
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Share"
OnClick="ShareReferralCode">
اشتراک‌گذاری
</MudButton>
</MudStack>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
این کد را با دوستان خود به اشتراک بگذارید تا از خریدهای آنها پاداش دریافت کنید.
</MudText>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudTextField @bind-Value="_userProfile.ReferralCode"
Label="کد دعوت"
Variant="Variant.Outlined"
ReadOnly="true"
Class="flex-grow-1"
Adornment="Adornment.End"
AdornmentIcon="@Icons.Material.Filled.ContentCopy"
OnAdornmentClick="CopyReferralCode" />
</MudStack>
@if (!string.IsNullOrWhiteSpace(_copyMessage))
{
<MudText Typo="Typo.caption" Color="Color.Success">@(_copyMessage)</MudText>
}
</MudStack>
}
else
{
<MudPaper Class="my-3 pa-3 border border-secondary">
<MudStack Spacing="3" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Large" Color="Color.Info" />
<MudText Typo="Typo.h6" Align="Align.Center" Color="Color.Info">
لینک دعوت شما هنوز فعال نشده است
</MudText>
<MudText Typo="Typo.body1" Align="Align.Center" Class="mud-text-secondary">
برای فعال‌سازی لینک دعوت و دعوت دوستان خود به جمع مشتریان ویژه کارابازار، کافی است ابتدا <strong>پکیج پایه ۵۶ میلیونی</strong> را تهیه کنید و سپس در <strong>باشگاه مشتریان</strong> عضو شوید.
</MudText>
<MudText Typo="Typo.body2" Align="Align.Center" Class="mud-text-secondary">
از مزایای ویژه عضویت در باشگاه مشتریان کارابازار سلامت بهره‌مند شده و با فعالیت در زمینه توسعه فروشگاه‌ها پاداش دریافت کنید.
</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
StartIcon="@Icons.Material.Filled.ShoppingCart"
OnClick="OpenPurchaseOptions">
شروع فرآیند تامین اعتبار
</MudButton>
</MudStack>
</MudPaper>
}
<MudButton Variant="Variant.Filled"
Class="rounded-pill dash-hero-btn"
Href="https://dayadiamond.ir/"
StartIcon="@Icons.Material.Filled.Diamond">
سامانه دایا
</MudButton>
</MudStack>
</MudPaper>
</MudItem>
<!-- Wallet Highlight Row -->
@* ═══════════════════════════════════════════════
2. WALLET STRIP — Compact inline stats
═══════════════════════════════════════════════ *@
<MudItem xs="12">
<MudPaper Elevation="3" Class="pa-4 rounded-lg gradient-border">
<MudGrid Spacing="2" AlignItems="AlignItems.Center">
<MudItem xs="12" sm="6" md="4">
<MudStack>
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">موجودی اعتباری</MudText>
<MudText Typo="Typo.h5" Color="Color.Primary">@_walletCredit</MudText>
</MudStack>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudStack>
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">موجودی تخفیف باشگاه</MudText>
<MudText Typo="Typo.h5" Color="Color.Primary">@_walletDiscount</MudText>
</MudStack>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudStack>
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">موجودی پاداش تیمی</MudText>
<MudText Typo="Typo.h5" Color="Color.Primary">@_walletNetwork</MudText>
</MudStack>
</MudItem>
<MudItem xs="12" md="4" Class="d-flex justify-end">
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.AccountBalanceWallet" Href="@RouteConstants.Profile.Wallet">جزئیات کیف پول</MudButton>
</MudItem>
</MudGrid>
<MudPaper Elevation="1" Class="pa-2 pa-md-4 rounded-xl">
<MudStack Row="true" Justify="Justify.SpaceAround" AlignItems="AlignItems.Center" Class="flex-wrap wallet-strip">
<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>
</div>
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@_walletCredit</MudText>
</div>
<MudDivider Vertical="true" FlexItem="true" Class="d-none d-sm-flex" />
<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>
</div>
<MudText Typo="Typo.subtitle2" Color="Color.Success">@_walletDiscount</MudText>
</div>
<MudDivider Vertical="true" FlexItem="true" Class="d-none d-sm-flex" />
<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>
</div>
<MudText Typo="Typo.subtitle2" Color="Color.Warning">@_walletNetwork</MudText>
</div>
<MudDivider Vertical="true" FlexItem="true" Class="d-none d-sm-flex" />
<MudButton Variant="Variant.Text" Color="Color.Primary" Size="Size.Small"
Href="@RouteConstants.Profile.Wallet" EndIcon="@Icons.Material.Filled.ArrowBack">
کیف پول
</MudButton>
</MudStack>
</MudPaper>
</MudItem>
<!-- Profile Content as tiles -->
@* ═══════════════════════════════════════════════
3. REFERRAL SECTION
═══════════════════════════════════════════════ *@
<MudItem xs="12">
<MudPaper Elevation="4" Class="pa-4">
<MudGrid Spacing="3">
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Profile.Personal" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.Person" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.subtitle1" Class="mt-2">اطلاعات شخصی</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">نمایش و ویرایش اطلاعات</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Profile.Addresses" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.LocationOn" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.subtitle1" Class="mt-2">آدرس‌ها</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">مدیریت آدرس‌های شما</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Profile.Tree" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.AccountTree" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.subtitle1" Class="mt-2">شجره‌نامه</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">ساختار تیم</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Profile.Settings" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.Settings" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.subtitle1" Class="mt-2">تنظیمات حساب</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">اعلان‌ها و تنظیمات</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Store.Products" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.Storefront" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.subtitle1" Class="mt-2">فروشگاه</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">مشاهده و خرید محصولات</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Profile.Wallet" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.subtitle1" Class="mt-2">کیف پول</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">مدیریت و تاریخچه</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Profile.WithdrawalRequests" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.RequestPage" Size="Size.Large" Color="Color.Warning" />
<MudText Typo="Typo.subtitle1" Class="mt-2">درخواست برداشت</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">ثبت و پیگیری برداشت</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Club.Membership" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.CardMembership" Size="Size.Large" Color="Color.Secondary" />
<MudText Typo="Typo.subtitle1" Class="mt-2">باشگاه مشتریان</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">عضویت و مزایا</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Network.Statistics" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.Groups" Size="Size.Large" Color="Color.Info" />
<MudText Typo="Typo.subtitle1" Class="mt-2">آمار باشگاه</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">آمار جزئیات باشگاه</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Commission.Dashboard" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.Payments" Size="Size.Large" Color="Color.Success" />
<MudText Typo="Typo.subtitle1" Class="mt-2">پاداش</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">درآمد و تاریخچه</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
</MudGrid>
</MudPaper>
@if (CanShowReferralLink)
{
<MudPaper Elevation="1" Class="pa-5 rounded-xl">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mb-3">
<MudIcon Icon="@Icons.Material.Filled.Share" Color="Color.Primary" />
<MudText Typo="Typo.h6">کد دعوت شما</MudText>
<MudSpacer />
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Small"
Class="rounded-pill"
StartIcon="@Icons.Material.Filled.Share"
OnClick="ShareReferralCode">
اشتراک‌گذاری
</MudButton>
</MudStack>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-3">
این کد را با دوستان خود به اشتراک بگذارید تا از خریدهای آنها پاداش دریافت کنید.
</MudText>
<MudTextField @bind-Value="_userProfile.ReferralCode"
Label="کد دعوت"
Variant="Variant.Outlined"
ReadOnly="true"
Adornment="Adornment.End"
AdornmentIcon="@Icons.Material.Filled.ContentCopy"
OnAdornmentClick="CopyReferralCode"
Class="rounded-lg" />
@if (!string.IsNullOrWhiteSpace(_copyMessage))
{
<MudText Typo="Typo.caption" Color="Color.Success" Class="mt-2">@(_copyMessage)</MudText>
}
</MudPaper>
}
else
{
<MudPaper Elevation="0" Class="pa-6 rounded-xl text-center" Style="border:2px dashed var(--mud-palette-primary); background:rgba(99,102,241,.04);">
<MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Large" Color="Color.Primary" Class="mb-2" Style="font-size:2.5rem;" />
<MudText Typo="Typo.h6" Color="Color.Primary" Class="mb-2">
لینک دعوت شما هنوز فعال نشده است
</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-1" Style="max-width:560px; margin:0 auto;">
برای فعال‌سازی لینک دعوت، ابتدا <strong>پکیج پایه</strong> را تهیه کنید و سپس در <strong>باشگاه مشتریان</strong> عضو شوید.
</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-4" Style="max-width:560px; margin:0 auto;">
از مزایای ویژه عضویت بهره‌مند شده و با فعالیت در زمینه توسعه فروشگاه‌ها پاداش دریافت کنید.
</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
Class="rounded-pill"
StartIcon="@Icons.Material.Filled.ShoppingCart"
OnClick="OpenPurchaseOptions">
شروع فرآیند تامین اعتبار
</MudButton>
</MudPaper>
}
</MudItem>
@* ═══════════════════════════════════════════════
3.5 LATEST BLOG BANNER
═══════════════════════════════════════════════ *@
@if (_latestPost is not null)
{
<MudItem xs="12">
<MudLink Href="@($"/blog/{_latestPost.Slug}")" Underline="Underline.None">
<MudPaper Elevation="1" Class="rounded-xl dash-blog-banner overflow-hidden">
<MudStack Row="true" AlignItems="AlignItems.Center" Class="pa-2" Spacing="2">
@if (!string.IsNullOrWhiteSpace(_latestPost.ThumbnailUrl))
{
<div style="width:72px;height:72px;min-width:72px;border-radius:10px;overflow:hidden;">
<AppImage Path="@_latestPost.ThumbnailUrl" Alt="@_latestPost.Title"
ObjectFit="ObjectFit.Cover" Style="width:100%;height:100%;" />
</div>
}
else
{
<MudAvatar Rounded="true" Size="Size.Large" Color="Color.Primary" Variant="Variant.Outlined">
<MudIcon Icon="@Icons.Material.Outlined.Article" />
</MudAvatar>
}
<MudStack Spacing="0" Class="flex-grow-1" Style="min-width:0;">
<MudText Typo="Typo.caption" Color="Color.Primary" Style="font-size:0.7rem;">جدیدترین مطلب</MudText>
<MudText Typo="Typo.body2" Class="dash-blog-title" Style="font-size:0.85rem;">@_latestPost.Title</MudText>
</MudStack>
<MudIcon Icon="@Icons.Material.Filled.ArrowBack" Color="Color.Primary" Size="Size.Small" />
</MudStack>
</MudPaper>
</MudLink>
</MudItem>
}
@* ═══════════════════════════════════════════════
4. QUICK ACCESS TILES — Modern grid
═══════════════════════════════════════════════ *@
<MudItem xs="12">
<MudText Typo="Typo.h6" Class="mb-3">دسترسی سریع</MudText>
<MudGrid Spacing="3" Justify="Justify.Center">
@foreach (var tile in _dashTiles)
{
<MudItem xs="6" sm="4" md="3">
<MudLink Href="@tile.Href" Underline="Underline.None" Class="tile-link">
<MudPaper Elevation="0" Class="pa-4 rounded-xl dash-tile text-center">
<MudAvatar Size="Size.Medium" Class="mx-auto mb-2" Style="@tile.AvatarStyle">
<MudIcon Icon="@tile.Icon" Size="Size.Medium" />
</MudAvatar>
<MudText Typo="Typo.subtitle2">@tile.Title</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@tile.Subtitle</MudText>
</MudPaper>
</MudLink>
</MudItem>
}
</MudGrid>
</MudItem>
</MudGrid>
</MudContainer>
<!-- Purchase Options Bottom Sheet -->
@* ── 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>
<!-- Direct Payment Option -->
<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">
<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>
<!-- Daya Loan Option -->
<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">
<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 Variant="Variant.Text" Color="Color.Default" FullWidth="true" OnClick="ClosePurchaseOptions">
بستن
</MudButton>
</MudStack>
@@ -1,9 +1,5 @@
using FluentValidation;
using FrontOffice.BFF.User.Protobuf.Protos.User;
using FrontOffice.BFF.User.Protobuf.Validator;
using FrontOffice.BFF.UserAddress.Protobuf.Protos.UserAddress;
using FrontOffice.BFF.UserAddress.Protobuf.Validator;
using FrontOffice.BFF.Package.Protobuf.Protos.Package;
using FrontOffice.Main.Pages.Profile.Components;
using FrontOffice.Main.Utilities;
using Mapster;
@@ -11,6 +7,11 @@ using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor;
using System.ComponentModel.DataAnnotations;
using CMSMicroservice.Protobuf.Protos.Package;
using CMSMicroservice.Protobuf.Protos.User;
using CMSMicroservice.Protobuf.Protos.UserAddress;
using CMSMicroservice.Protobuf.Validator.User;
using CMSMicroservice.Protobuf.Validator.UserAddress;
using Severity = MudBlazor.Severity;
namespace FrontOffice.Main.Pages.Profile;
@@ -21,10 +22,11 @@ public partial class Index
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
[Inject] private PackageContract.PackageContractClient PackageContract { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!;
[Inject] private BlogPostService BlogPostService { get; set; } = default!;
private GetUserResponse _userProfile = new();
private UpdateUserRequest _updateUserRequest = new();
private readonly UpdateUserRequestValidator _personalValidator = new();
private GetUserForCustomerResponse _userProfile = new();
private BlogPostCardDto? _latestPost;
private UpdateCustomerProfileRequest _updateUserRequest = new();
private MudForm? _personalForm;
@@ -44,8 +46,24 @@ public partial class Index
private bool _showPurchaseBottomSheet;
private bool _isProcessingPayment;
// 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.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.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;"),
new(RouteConstants.Commission.Dashboard, Icons.Material.Filled.Payments, "پاداش", "درآمد و تاریخچه", "background:rgba(16,185,129,.12); color:#10b981;"),
};
private record DashTile(string Href, string Icon, string Title, string Subtitle, string AvatarStyle);
// Address management
private List<GetAllUserAddressByFilterResponseModel> _addresses = new();
private List<CustomerAddressModel> _addresses = new();
private bool _isLoadingAddresses;
private CreateNewUserAddressRequest _newAddressRequest = new();
private UpdateUserAddressRequest _editAddressRequest = new();
@@ -63,6 +81,7 @@ public partial class Index
await LoadUserProfile();
await LoadAddresses();
await LoadWallet();
await LoadLatestPost();
// نمایش Modal قرارداد باشگاه اگر پکیج خریده ولی باشگاه فعال نشده
await CheckAndShowClubContractModal();
@@ -133,19 +152,47 @@ public partial class Index
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
private async Task LoadLatestPost()
{
try
{
var posts = await BlogPostService.GetFeaturedPostsAsync(1);
if (!posts.Any())
{
var result = await BlogPostService.GetPublishedPostsAsync(page: 1, pageSize: 1);
posts = result.Posts;
}
_latestPost = posts.FirstOrDefault();
}
catch
{
_latestPost = null;
}
StateHasChanged();
}
private async Task LoadUserProfile()
{
try
{
_userProfile = await UserContract.GetUserAsync(request: new());
_updateUserRequest = _userProfile.Adapt<UpdateUserRequest>();
_userProfile = await UserContract.GetUserForCustomerAsync(new());
_updateUserRequest = new UpdateCustomerProfileRequest
{
FirstName = _userProfile.FirstName,
LastName = _userProfile.LastName,
Email = _userProfile.Email,
NationalCode = _userProfile.NationalCode,
};
if (_userProfile.BirthDate != null)
{
_date = _userProfile.BirthDate.ToDateTime();
_updateUserRequest.BirthDate = _userProfile.BirthDate;
}
}
catch (Exception ex)
{
// Handle the case when user is not authenticated or API fails
_userProfile = new GetUserResponse();
_userProfile = new GetUserForCustomerResponse();
}
StateHasChanged();
}
@@ -167,7 +214,7 @@ public partial class Index
if (_date != null)
_updateUserRequest.BirthDate = _date.Value.DateTimeToTimestamp();
await UserContract.UpdateUserAsync(request: _updateUserRequest);
await UserContract.UpdateCustomerProfileAsync(request: _updateUserRequest);
await LoadUserProfile();
Snackbar.Add("اطلاعات شخصی با موفقیت ذخیره شد.", Severity.Success);
}
@@ -250,20 +297,20 @@ public partial class Index
_isLoadingAddresses = true;
try
{
var response = await UserAddressContract.GetAllUserAddressByFilterAsync(request: new());
var response = await UserAddressContract.GetCustomerAddressesAsync(request: new());
if (response?.Models?.Any() == true)
{
_addresses = response.Models.ToList();
}
else
{
_addresses = new List<GetAllUserAddressByFilterResponseModel>();
_addresses = new List<CustomerAddressModel>();
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری آدرس‌ها: {ex.Message}", Severity.Error);
_addresses = new List<GetAllUserAddressByFilterResponseModel>();
_addresses = new List<CustomerAddressModel>();
}
finally
{
@@ -283,7 +330,7 @@ public partial class Index
}
}
private async Task OpenEditAddressDialog(GetAllUserAddressByFilterResponseModel address)
private async Task OpenEditAddressDialog(CustomerAddressModel address)
{
var dialog = await DialogService.ShowAsync<EditAddressDialog>("ویرایش آدرس", new DialogParameters<EditAddressDialog>
{
@@ -1,8 +1,10 @@
@page "/profile/payment-callback"
@attribute [Authorize]
@using Blazored.LocalStorage
@using FrontOffice.BFF.Package.Protobuf.Protos.Package
@using FrontOffice.BFF.User.Protobuf.Protos.User
@using CMSMicroservice.Protobuf.Protos.Package
@using CMSMicroservice.Protobuf.Protos.User
@inject IJSRuntime JS
<PageTitle>نتیجه پرداخت | کارا بازار سلامت</PageTitle>
@@ -36,7 +38,7 @@
</MudText>
<MudButton Color="Color.Primary" Variant="Variant.Filled" Class="mt-6"
Href="/profile">
OnClick="GoBack">
بازگشت به پروفایل
</MudButton>
}
@@ -47,7 +49,7 @@
<MudText Typo="Typo.body1" Class="mt-2">@_message</MudText>
<MudButton Color="Color.Primary" Variant="Variant.Filled" Class="mt-6"
Href="/profile">
OnClick="GoBack">
بازگشت به پروفایل
</MudButton>
@@ -112,13 +114,14 @@
{
OrderId = OrderId,
TransactionId = TransactionId,
Authority = Authority,
Status = Status ?? "NOK"
PaymentSuccess = Status == "OK",
RefId = !string.IsNullOrEmpty(Authority) ? Authority : null,
Message = !string.IsNullOrEmpty(Status) ? Status : null
});
_isSuccess = response.Success;
_message = response.Message;
_refId = response.RefId;
_refId = response.ReferenceCode ?? "";
_walletBalance = response.WalletBalance;
_discountBalance = response.DiscountBalance;
@@ -153,7 +156,7 @@
{
try
{
var userResponse = await UserContractClient.GetUserAsync(new Google.Protobuf.WellKnownTypes.Empty());
var userResponse = await UserContractClient.GetUserForCustomerAsync(new CMSMicroservice.Protobuf.Protos.User.GetUserForCustomerRequest());
if (!string.IsNullOrWhiteSpace(userResponse.Token))
{
// ذخیره توکن جدید در localStorage
@@ -171,4 +174,6 @@
}
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
private async Task GoBack() => await JS.InvokeVoidAsync("history.back");
}
@@ -4,13 +4,10 @@
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">اطلاعات شخصی</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<PageHeader Title="اطلاعات شخصی" BackHref="@RouteConstants.Profile.Index" />
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudForm @ref="_personalForm" Model="_updateUserRequest" Validation="@(_personalValidator.ValidateValue)">
<MudForm @ref="_personalForm" Model="_updateUserRequest">
<MudGrid Spacing="3">
<MudItem xs="12" md="6">
<MudTextField @bind-Value="_updateUserRequest.FirstName"
@@ -1,8 +1,6 @@
using FluentValidation;
using FrontOffice.BFF.User.Protobuf.Protos.User;
using FrontOffice.BFF.User.Protobuf.Validator;
using CMSMicroservice.Protobuf.Protos.User;
using FrontOffice.Main.Utilities;
using Mapster;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using Severity = MudBlazor.Severity;
@@ -13,9 +11,8 @@ public partial class Personal : ComponentBase
{
[Inject] private UserContract.UserContractClient UserContract { get; set; } = default!;
private GetUserResponse _userProfile = new();
private UpdateUserRequest _updateUserRequest = new();
private readonly UpdateUserRequestValidator _personalValidator = new();
private GetUserForCustomerResponse _userProfile = new();
private UpdateCustomerProfileRequest _updateUserRequest = new();
private MudForm? _personalForm;
private DateTime? _date;
@@ -30,8 +27,14 @@ public partial class Personal : ComponentBase
{
try
{
_userProfile = await UserContract.GetUserAsync(new());
_updateUserRequest = _userProfile.Adapt<UpdateUserRequest>();
_userProfile = await UserContract.GetUserForCustomerAsync(new());
_updateUserRequest = new UpdateCustomerProfileRequest
{
FirstName = _userProfile.FirstName,
LastName = _userProfile.LastName,
Email = _userProfile.Email,
NationalCode = _userProfile.NationalCode,
};
if (_userProfile.BirthDate != null)
_date = _userProfile.BirthDate.ToDateTime();
}
@@ -56,7 +59,7 @@ public partial class Personal : ComponentBase
if (_date != null)
_updateUserRequest.BirthDate = _date.Value.DateTimeToTimestamp();
await UserContract.UpdateUserAsync(_updateUserRequest);
await UserContract.UpdateCustomerProfileAsync(_updateUserRequest);
Snackbar.Add("اطلاعات شخصی با موفقیت ذخیره شد.", Severity.Success);
}
catch (Exception ex)
@@ -4,10 +4,7 @@
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">تنظیمات حساب</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<PageHeader Title="تنظیمات حساب" BackHref="@RouteConstants.Profile.Index" />
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="4">
@@ -1,5 +1,4 @@
using FrontOffice.BFF.User.Protobuf.Protos.User;
using Mapster;
using CMSMicroservice.Protobuf.Protos.User;
using Microsoft.AspNetCore.Components;
using MudBlazor;
@@ -16,8 +15,18 @@ public partial class Settings : ComponentBase
{
try
{
var user = await UserContract.GetUserAsync(new());
_request = user.Adapt<UpdateUserRequest>();
var user = await UserContract.GetUserForCustomerAsync(new());
_request = new UpdateUserRequest
{
Id = user.Id,
FirstName = user.FirstName,
LastName = user.LastName,
Email = user.Email,
NationalCode = user.NationalCode,
EmailNotifications = user.EmailNotifications,
SmsNotifications = user.SmsNotifications,
PushNotifications = user.PushNotifications,
};
}
catch
{
@@ -5,10 +5,7 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">شجره‌نامه</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<PageHeader Title="شجره‌نامه" BackHref="@RouteConstants.Profile.Index" />
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<OrganizationChart />
+82 -21
View File
@@ -4,28 +4,25 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">کیف پول</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<PageHeader Title="کیف پول" BackHref="@RouteConstants.Profile.Index" />
<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.h4" Color="Color.Warning">@_balances.Discount</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.h4" Color="Color.Success">@_balances.Network</MudText>
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">موجودی تخفیفی</MudText>
<MudText Typo="Typo.h4" Color="Color.Warning">@_balances.Discount</MudText>
</MudPaper>
</MudItem>
</MudGrid>
@@ -71,18 +68,45 @@
</MudGrid>
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudTable Items="_txs" Dense="true">
<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>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Date</MudTd>
<MudTd Color="@(context.Amount > 0 ? Color.Success : Color.Error)">@FormatPrice(context.Amount)</MudTd>
<MudTd>@context.Channel</MudTd>
<MudTd>@context.Description</MudTd>
<MudTd DataLabel="تاریخ">
<MudText Typo="Typo.caption">@context.Date</MudText>
</MudTd>
<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) : "-")
</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@FormatPrice(context.CreditBalance)</MudText>
</MudStack>
</MudTd>
<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) : "-")
</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@FormatPrice(context.NetworkBalance)</MudText>
</MudStack>
</MudTd>
<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) : "-")
</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@FormatPrice(context.DiscountBalance)</MudText>
</MudStack>
</MudTd>
<MudTd DataLabel="شناسه">@context.Channel</MudTd>
<MudTd DataLabel="توضیحات">@context.Description</MudTd>
</RowTemplate>
</MudTable>
</MudHidden>
@@ -91,13 +115,50 @@
<MudStack Spacing="2">
@foreach (var tx in _txs)
{
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
<MudStack Spacing="1">
<MudPaper Class="pa-3 rounded-lg" Outlined="true" Elevation="1">
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText>@tx.Date</MudText>
<MudText Color="@(tx.Amount > 0 ? Color.Success : Color.Error)">@FormatPrice(tx.Amount)</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@tx.Date</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">شناسه: @tx.Channel</MudText>
</MudStack>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@tx.Channel</MudText>
<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 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>
<MudText Typo="Typo.caption" Class="mud-text-secondary" Style="font-size: 0.7rem;">@FormatPrice(tx.CreditBalance)</MudText>
</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 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>
<MudText Typo="Typo.caption" Class="mud-text-secondary" Style="font-size: 0.7rem;">@FormatPrice(tx.NetworkBalance)</MudText>
</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 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>
<MudText Typo="Typo.caption" Class="mud-text-secondary" Style="font-size: 0.7rem;">@FormatPrice(tx.DiscountBalance)</MudText>
</MudStack>
</MudPaper>
</MudStack>
<MudText Typo="Typo.body2">@tx.Description</MudText>
</MudStack>
</MudPaper>
@@ -4,10 +4,7 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">درخواست‌های برداشت</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Wallet">بازگشت به کیف پول</MudButton>
</MudStack>
<PageHeader Title="درخواست‌های برداشت" BackHref="@RouteConstants.Profile.Wallet" />
<!-- فرم درخواست برداشت جدید -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
@@ -100,7 +97,7 @@
@if (_isLoading)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
<LoadingState />
}
else if (!_withdrawals.Any())
{
+68 -55
View File
@@ -6,10 +6,11 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="px-1">
<MudGrid Spacing="4" Justify="Justify.Center">
<MudItem xs="12" md="5" Class="d-none d-sm-block">
<MudStack Spacing="2" Class="mb-6">
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled" Class="mb-2">ثبت‌نام سه
مرحله‌ای</MudChip>
<MudText Typo="Typo.h3" Class="mb-2">
<MudStack Spacing="3" Class="mb-6">
<MudChip T="string" Color="Color.Primary" Variant="Variant.Filled" Size="Size.Small" Class="rounded-pill">
ثبت‌نام سه مرحله‌ای
</MudChip>
<MudText Typo="Typo.h3" Class="mb-1">
فقط در چند دقیقه حساب خود را فعال کنید
</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
@@ -18,34 +19,47 @@
</MudText>
</MudStack>
<MudPaper Elevation="1" Class="pa-4 rounded-xl gradient-border">
<MudStack Spacing="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.VerifiedUser" Color="Color.Primary" />
<MudPaper Elevation="0" Class="pa-5 rounded-xl" Style="border:1px solid var(--mud-palette-divider);">
<MudStack Spacing="3">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudAvatar Size="Size.Small" Color="Color.Primary" Variant="Variant.Filled">
<MudIcon Icon="@Icons.Material.Outlined.Verified" Size="Size.Small" />
</MudAvatar>
<MudText Typo="Typo.subtitle1">مزایای ثبت‌نام آنلاین</MudText>
</MudStack>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
• تایید سریع و آنلاین شماره موبایل<br />
• تکمیل اطلاعات هویتی بدون مراجعه حضوری<br />
• دریافت نسخه‌ی دیجیتال قرارداد برای بررسی دقیق
</MudText>
<MudStack Spacing="2">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Outlined.PhoneAndroid" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.body2" Class="mud-text-secondary">تایید سریع و آنلاین شماره موبایل</MudText>
</MudStack>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Outlined.Badge" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.body2" Class="mud-text-secondary">تکمیل اطلاعات هویتی بدون مراجعه حضوری</MudText>
</MudStack>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Outlined.Description" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.body2" Class="mud-text-secondary">دریافت نسخه‌ی دیجیتال قرارداد برای بررسی دقیق</MudText>
</MudStack>
</MudStack>
</MudStack>
</MudPaper>
</MudItem>
<MudItem xs="12" md="7">
<MudPaper Elevation="3" Class="pa-6 wizard-card">
<MudPaper Elevation="1" Class="pa-5 pa-md-6 wizard-card">
@if (_completed)
{
<MudStack Spacing="3" AlignItems="AlignItems.Center" Class="text-center">
<MudAvatar Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Large" />
<MudStack Spacing="3" AlignItems="AlignItems.Center" Class="text-center py-6">
<MudAvatar Size="Size.Large" Color="Color.Success" Variant="Variant.Filled">
<MudIcon Icon="@Icons.Material.Outlined.CheckCircle" Size="Size.Large" />
</MudAvatar>
<MudText Typo="Typo.h4">درخواست شما ثبت شد</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
<MudText Typo="Typo.body1" Class="mud-text-secondary" Style="max-width:400px;">
تیم ما پس از بررسی اطلاعات با شما تماس خواهد گرفت. می‌توانید از طریق داشبورد وضعیت ثبت‌نام
را دنبال کنید.
</MudText>
<MudStack Row="true" Spacing="2" Justify="Justify.Center">
<MudButton Variant="Variant.Filled" Color="Color.Primary"
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="rounded-lg"
OnClick="@(() => Navigation.NavigateTo(RouteConstants.Main.MainPage))">بازگشت به صفحه
اصلی</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
@@ -56,43 +70,45 @@
}
else
{
<MudStack Spacing="3" >
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h4">مراحل ثبت‌نام</MudText>
<MudChip T="string" Color="Color.Info" Variant="Variant.Outlined" Size="Size.Small">۳ مرحله
<MudText Typo="Typo.h5">مراحل ثبت‌نام</MudText>
<MudChip T="string" Color="Color.Primary" Variant="Variant.Outlined" Size="Size.Small" Class="rounded-pill">
مرحله @(_activeStep + 1) از ۳
</MudChip>
</MudStack>
@if (_isSubmitting)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="rounded" />
}
<MudStepper @bind-ActiveIndex="_activeStep" @ref="_mudStepper" Elevation="0" DisableClick="true" Class="mb-4 " id="registration-stepper">
<MudStepper @bind-ActiveIndex="_activeStep" @ref="_mudStepper" Elevation="0" DisableClick="true" Class="mb-4" id="registration-stepper">
<ChildContent>
<MudStep Label="تأیید موبایل" Icon="@Icons.Material.Filled.Smartphone">
<MudStep Label="تأیید موبایل" Icon="@Icons.Material.Outlined.PhoneAndroid">
@* Inline AuthDialog with captcha enabled *@
<AuthDialog @ref="_authDialog" InlineMode="true" HideCancelButton="true" OnLoginSuccess="@(async () => { OnPhoneVerified(); })" />
</MudStep>
<MudStep Label="اطلاعات هویتی" Icon="@Icons.Material.Filled.Badge">
<MudStep Label="اطلاعات هویتی" Icon="@Icons.Material.Outlined.Badge">
<MudForm @ref="_stepTwoForm">
<MudTextField Label="نام" Variant="Variant.Outlined" Immediate="true"
@bind-Value="_model.FirstName" For="@(() => _model.FirstName)" />
<MudTextField Label="نام خانوادگی" Variant="Variant.Outlined" Immediate="true"
@bind-Value="_model.LastName" For="@(() => _model.LastName)" />
<MudTextField Label="کد ملی" Variant="Variant.Outlined" Immediate="true"
MaxLength="10" @bind-Value="_model.NationalCode"
For="@(() => _model.NationalCode)" InputType="InputType.Number" />
<MudStack Spacing="3">
<MudTextField Label="نام" Variant="Variant.Outlined" Immediate="true"
@bind-Value="_model.FirstName" For="@(() => _model.FirstName)" />
<MudTextField Label="نام خانوادگی" Variant="Variant.Outlined" Immediate="true"
@bind-Value="_model.LastName" For="@(() => _model.LastName)" />
<MudTextField Label="کد ملی" Variant="Variant.Outlined" Immediate="true"
MaxLength="10" @bind-Value="_model.NationalCode"
For="@(() => _model.NationalCode)" InputType="InputType.Number" />
</MudStack>
</MudForm>
</MudStep>
<MudStep Label="قوانین و قرارداد" Icon="@Icons.Material.Filled.Rule">
<MudStep Label="قوانین و قرارداد" Icon="@Icons.Material.Outlined.Rule">
<MudForm @ref="_stepThreeForm">
<MudAlert Variant="Variant.Outlined" Severity="Severity.Info" Class="mb-3">
<MudAlert Variant="Variant.Outlined" Severity="Severity.Info" Class="mb-3 rounded-lg">
لطفاً قوانین و شرایط همکاری را با دقت مطالعه کنید و در صورت موافقت، تیک
تایید را فعال نمایید. همچنین می‌توانید نسخه‌ی قرارداد را دانلود و ذخیره
کنید.
تایید را فعال نمایید.
</MudAlert>
<MudPaper Elevation="0" Class="terms-box pa-4 mb-3">
<MudText Typo="Typo.subtitle2" Class="mb-2">بخشی از قوانین:</MudText>
@@ -106,47 +122,44 @@
</MudList>
</MudPaper>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mb-2">
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Download" Disabled="_isSubmitting"
OnClick="DownloadContract">
دانلود/پرینت قرارداد نمونه
</MudButton>
<MudText Typo="Typo.caption" Class="mud-text-secondary">فرمت: PDF</MudText>
</MudStack>
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Class="rounded-lg mb-3"
StartIcon="@Icons.Material.Outlined.Download" Disabled="_isSubmitting"
OnClick="DownloadContract">
دانلود قرارداد نمونه (PDF)
</MudButton>
<MudCheckBox @bind-Checked="_model.AcceptTerms" Color="Color.Success"
<MudCheckBox @bind-Checked="_model.AcceptTerms" Color="Color.Primary"
For="@(() => _model.AcceptTerms)"
Label="قوانین و مقررات را مطالعه کرده‌ام و می‌پذیرم" />
</MudForm>
</MudStep>
</ChildContent>
<ActionContent Context="stepper">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mt-2">
@if (_isAuthenticated && _activeStep == 1)
{
<MudButton Variant="Variant.Text" Color="Color.Secondary"
<MudButton Variant="Variant.Text" Color="Color.Error" Size="Size.Small"
OnClick="@AuthService.LogoutAsync">خروج از حساب</MudButton>
}
else
{
<MudButton Variant="Variant.Text" Color="Color.Secondary"
Disabled="_activeStep == 0 || _isSubmitting"
OnClick="@(async () => { await GoBack(); })">مرحله قبل</MudButton>
<MudButton Variant="Variant.Text" Color="Color.Secondary" Size="Size.Small"
Disabled="_activeStep == 0 || _isSubmitting"
OnClick="@(async () => { await GoBack(); })">مرحله قبل</MudButton>
}
<MudSpacer/>
<MudButton Variant="Variant.Filled" Color="Color.Primary"
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="rounded-lg"
Disabled="_isSubmitting"
OnClick="@(async () => { await GoNextAsync(); })">
@if (_isSubmitting)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="me-2" />
}
@_nextButtonText
</MudButton>
</MudStack>
</ActionContent>
</MudStepper>
</MudStack>
}
</MudPaper>
@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
using DateTimeConverterCL;
using FrontOffice.BFF.User.Protobuf.Protos.User;
using CMSMicroservice.Protobuf.Protos.User;
using FrontOffice.Main.Shared;
using FrontOffice.Main.Utilities;
using Google.Protobuf.WellKnownTypes;
@@ -23,13 +23,14 @@ public partial class RegisterWizard
private bool _completed;
private AuthDialog? _authDialog;
private MudStepper? _mudStepper;
private UpdateUserRequest _updateUserRequest = new();
private UpdateCustomerProfileRequest _updateUserRequest = new();
private bool _isAuthenticated;
private CancellationTokenSource? _operationCts;
private UserAuthInfo _userAuthInfo;
private Guid signGuid = Guid.NewGuid();
private const string TokenStorageKey = "auth:token";
private readonly DialogOptions _normalWidth = new() { MaxWidth = MaxWidth.ExtraSmall, FullWidth = true };
private bool _initialDataLoaded;
[Inject] private AuthService AuthService { get; set; } = default!;
[Inject] private IDeviceDetector _deviceDetector { get; set; } = default!;
@@ -55,6 +56,7 @@ public partial class RegisterWizard
if (_userAuthInfo.IsSignMainContract)
{
Navigation.NavigateTo(RouteConstants.Profile.Index);
return;
}
if (_activeStep == 0)
@@ -63,11 +65,12 @@ public partial class RegisterWizard
_activeStep = 1;
await InvokeAsync(StateHasChanged);
}
else if (_activeStep == 1)
else if (_activeStep == 1 && !_initialDataLoaded)
{
_initialDataLoaded = true;
try
{
var existUser = await UserContract.GetUserAsync(new Empty());
var existUser = await UserContract.GetUserForCustomerAsync(new GetUserForCustomerRequest());
if (existUser != null && !string.IsNullOrEmpty(existUser.FirstName) &&
string.IsNullOrWhiteSpace(_model.FirstName))
{
@@ -95,7 +98,6 @@ public partial class RegisterWizard
}
}
await base.OnAfterRenderAsync(firstRender);
}
@@ -355,7 +357,7 @@ public partial class RegisterWizard
_updateUserRequest.NationalCode = _model.NationalCode.PersianToEnglish();
await UserContract.UpdateUserAsync(request: _updateUserRequest);
await UserContract.UpdateCustomerProfileAsync(request: _updateUserRequest);
Snackbar.Add("اطلاعات شخصی با موفقیت ذخیره شد.", Severity.Success);
return true;
}
+9 -9
View File
@@ -5,7 +5,7 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<MudText Typo="Typo.h4">سبد خرید</MudText>
<PageHeader Title="سبد خرید" BackHref="@RouteConstants.Store.Products" />
@if (CartData.Items.Count == 0)
{
@@ -30,8 +30,8 @@
<MudTd>
<MudStack Spacing="1">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudImage Src="@GetProductImageUrl(context.ImageUrl)" Alt="@context.Title"
Width="64" Height="64" Class="product-thumb" />
<AppImage Path="@GetProductImageUrl(context.ImageUrl)" Alt="@context.Title"
ImgWidth="64" ImgHeight="64" Class="product-thumb" />
<MudText>@context.Title</MudText>
</MudStack>
@if (context.Discount > 0 || !string.IsNullOrWhiteSpace(context.Created) || !string.IsNullOrWhiteSpace(context.Description))
@@ -86,8 +86,8 @@
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudImage Src="@GetProductImageUrl(item.ImageUrl)" Alt="@item.Title"
Width="50" Height="50" Class="rounded-circle" />
<AppImage Path="@GetProductImageUrl(item.ImageUrl)" Alt="@item.Title"
ImgWidth="50" ImgHeight="50" Class="rounded-circle" />
<MudText Typo="Typo.subtitle2">@item.Title</MudText>
</MudStack>
@if (item.Discount > 0)
@@ -154,8 +154,8 @@
</MudStack>
}
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h6" Style="font-weight:bold;">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.h6" Color="Color.Primary" Style="font-weight:bold;">@FormatPrice(TotalWithVAT) تومان</MudText>
<MudText Typo="Typo.h6" Class="fw-bold">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.h6" Color="Color.Primary" Class="fw-bold">@FormatPrice(TotalWithVAT) تومان</MudText>
</MudStack>
</MudStack>
@@ -185,8 +185,8 @@
}
<MudDivider Class="my-1" />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1" Style="font-weight:bold;">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Style="font-weight:bold;">@FormatPrice(TotalWithVAT) تومان</MudText>
<MudText Typo="Typo.subtitle1" Class="fw-bold">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="fw-bold">@FormatPrice(TotalWithVAT) تومان</MudText>
</MudStack>
</MudStack>
</MudPaper>
@@ -66,7 +66,7 @@ public partial class Cart : ComponentBase, IDisposable
private long VATAmount => VAT.CalculateVAT(CartData.Total);
private static string GetProductImageUrl(string? imageUrl)
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl;
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/');
public void Dispose()
{
@@ -5,9 +5,9 @@
<PageTitle>دسته‌بندی‌ها</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<PageHeader Title="دسته‌بندی محصولات" BackHref="@RouteConstants.Store.Products" />
<MudPaper Elevation="1" Class="pa-4 mb-4 rounded-lg">
<MudStack Spacing="1">
<MudText Typo="Typo.h5">دسته‌بندی محصولات</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
از بین درخت دسته‌بندی‌ها انتخاب کنید تا محصولات آن دسته را مشاهده نمایید.
</MudText>
@@ -1,9 +1,11 @@
@using FrontOffice.BFF.UserOrder.Protobuf.Protos.UserOrder
@using CMSMicroservice.Protobuf.Protos.UserOrder
@using CMSMicroservice.Protobuf.Protos
@attribute [Route(RouteConstants.Store.CheckoutSummary)]
<PageTitle>خلاصه خرید</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<PageHeader Title="خلاصه خرید" BackHref="@RouteConstants.Store.Cart" />
<MudGrid Spacing="3">
<MudItem xs="12" md="8">
<MudPaper Elevation="2" Class="pa-4 rounded-lg mb-3">
@@ -29,7 +31,7 @@
@foreach (var address in _addresses)
{
<MudPaper Outlined="@(_selectedAddress?.Id != address.Id)"
Elevation="@(_selectedAddress?.Id == address.Id ? 4 : 0)"
Elevation="@(_selectedAddress?.Id == address.Id ? 2 : 0)"
Class="pa-3 rounded-xl cursor-pointer"
Style="@(_selectedAddress?.Id == address.Id ? "border: 2px solid var(--mud-palette-primary);" : "")"
@onclick="() => _selectedAddress = address">
@@ -83,8 +85,8 @@
<MudListItemText>
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudImage Src="@GetProductImageUrl(item.ImageUrl)" Alt="@item.Title"
Width="48" Height="48" Class="rounded-circle" />
<AppImage Path="@GetProductImageUrl(item.ImageUrl)" Alt="@item.Title"
ImgWidth="48" ImgHeight="48" Class="rounded-circle" />
<MudText Typo="Typo.subtitle2">@item.Title</MudText>
<MudText Typo="Typo.subtitle2">@FormatPrice(item.LineTotal)</MudText>
</MudStack>
@@ -117,8 +119,8 @@
}
<MudDivider Class="my-1" />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1" Style="font-weight: bold;">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Style="font-weight: bold;">@FormatPrice(VAT.AddVAT(Cart.Total))</MudText>
<MudText Typo="Typo.subtitle1" Class="fw-bold">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="fw-bold">@FormatPrice(VAT.AddVAT(Cart.Total))</MudText>
</MudStack>
</MudStack>
@@ -1,8 +1,9 @@
using FrontOffice.BFF.UserAddress.Protobuf.Protos.UserAddress;
using FrontOffice.BFF.UserOrder.Protobuf.Protos.UserOrder;
using CMSMicroservice.Protobuf.Protos.UserAddress;
using CMSMicroservice.Protobuf.Protos.UserOrder;
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using Messages = CMSMicroservice.Protobuf.Protos;
namespace FrontOffice.Main.Pages.Store;
@@ -15,12 +16,12 @@ public partial class CheckoutSummary : ComponentBase
[Inject] private UserOrderContract.UserOrderContractClient UserOrderContract { get; set; } = default!;
// Snackbar and Navigation are injected via _Imports.razor
private List<GetAllUserAddressByFilterResponseModel> _addresses = new();
private GetAllUserAddressByFilterResponseModel? _selectedAddress;
private List<CustomerAddressModel> _addresses = new();
private CustomerAddressModel? _selectedAddress;
private bool _loadingAddresses;
private long walletBalance;
private PaymentMethod _payment = PaymentMethod.Wallet;
private Messages.PaymentMethod _payment = Messages.PaymentMethod.Wallet;
private bool CanPlaceOrder => Cart.Items.Count > 0 && _selectedAddress != null;
@@ -55,7 +56,7 @@ public partial class CheckoutSummary : ComponentBase
_loadingAddresses = true;
try
{
var response = await UserAddressContract.GetAllUserAddressByFilterAsync(new());
var response = await UserAddressContract.GetCustomerAddressesAsync(new());
if (response?.Models?.Any() == true)
{
_addresses = response.Models.ToList();
@@ -112,5 +113,5 @@ public partial class CheckoutSummary : ComponentBase
private long CalculateVAT() => VAT.CalculateVAT(Cart.Total);
private static string GetProductImageUrl(string? imageUrl)
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl;
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/');
}
@@ -6,27 +6,33 @@
@if (_loading)
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudStack AlignItems="AlignItems.Center">
<MudProgressCircular Indeterminate="true" Color="Color.Primary" />
</MudStack>
<LoadingState />
</MudContainer>
}
else if (_order is null)
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudAlert Severity="Severity.Warning">سفارش یافت نشد.</MudAlert>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" OnClick="() => Navigation.NavigateTo(RouteConstants.Store.Orders)">بازگشت</MudButton>
<EmptyState Icon="@Icons.Material.Filled.SearchOff"
Title="سفارش یافت نشد."
ActionText="بازگشت"
ActionHref="@RouteConstants.Store.Orders" />
</MudContainer>
}
else
{
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<PageHeader Title="@($"جزئیات سفارش #{_order.Id}")" BackHref="@RouteConstants.Store.Orders" />
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="2">
<MudText Typo="Typo.h5">سفارش #@_order.Id</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">تاریخ: @_order.PaymentDate.ToDateTime().MiladiToJalaliWithTime()</MudText>
@if (_order.PaymentDate != null)
{
<MudText Typo="Typo.body2" Class="mud-text-secondary">تاریخ پرداخت: @_order.PaymentDate.ToDateTime().MiladiToJalaliWithTime()</MudText>
}
else
{
<MudText Typo="Typo.body2" Class="mud-text-secondary">تاریخ ثبت: @DateTime.Now.MiladiToJalaliWithTime()</MudText>
}
<MudText Typo="Typo.body2">وضعیت: @GetStatusText(_order.PaymentStatus)</MudText>
<MudText Typo="Typo.body2">روش پرداخت: @GetPaymentMethodText(_order.PaymentMethod)</MudText>
<MudText Typo="Typo.body2">آدرس: @_order.UserAddressText</MudText>
<MudDivider Class="my-2" />
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
@@ -40,14 +46,14 @@ else
<RowTemplate>
<MudTd>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudImage Src="@GetProductImageUrl(context.ProductThumbnailPath)" Alt="@context.ProductTitle"
Width="60" Height="60" Class="rounded-circle" />
<MudText>@context.ProductThumbnailPath</MudText>
<AppImage Path="@GetProductImageUrl(context.ProductThumbnailPath)" Alt="@context.ProductTitle"
ImgWidth="60" ImgHeight="60" Class="rounded-circle" />
<MudText>@context.ProductTitle</MudText>
</MudStack>
</MudTd>
<MudTd>@FormatPrice(context.UnitPrice.Value)</MudTd>
<MudTd>@FormatPrice(context.UnitPrice ?? 0)</MudTd>
<MudTd>@context.Count</MudTd>
<MudTd>@FormatPrice(context.UnitPrice.Value*context.Count.Value)</MudTd>
<MudTd>@FormatPrice((context.UnitPrice ?? 0) * (context.Count ?? 0))</MudTd>
</RowTemplate>
</MudTable>
</MudHidden>
@@ -59,15 +65,15 @@ else
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
<MudStack Spacing="1">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudImage Src="@GetProductImageUrl(it.ProductThumbnailPath)" Alt="@it.ProductTitle"
Width="60" Height="60" Class="rounded-circle" />
<AppImage Path="@GetProductImageUrl(it.ProductThumbnailPath)" Alt="@it.ProductTitle"
ImgWidth="60" ImgHeight="60" Class="rounded-circle" />
<MudText>@it.ProductTitle</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Class="mud-text-secondary">@FormatPrice(it.UnitPrice.Value) واحد</MudText>
<MudText Class="mud-text-secondary">@FormatPrice(it.UnitPrice ?? 0) واحد</MudText>
<MudText>تعداد: @it.Count</MudText>
</MudStack>
<MudText>جمع: @FormatPrice(it.UnitPrice.Value*it.Count.Value)</MudText>
<MudText>جمع: @FormatPrice((it.UnitPrice ?? 0) * (it.Count ?? 0))</MudText>
</MudStack>
</MudPaper>
}
@@ -77,7 +83,7 @@ else
@* نمایش جزئیات مالی *@
@{
var subtotal = _order.FactorDetails.Sum(s => s.UnitPrice.Value * s.Count.Value);
var subtotal = _order.FactorDetails.Sum(s => (s.UnitPrice ?? 0) * (s.Count ?? 0));
}
<MudStack Spacing="1" Class="pa-2" Style="background-color: var(--mud-palette-background-grey); border-radius: 8px;">
<MudStack Row="true" Justify="Justify.SpaceBetween">
@@ -94,15 +100,15 @@ else
</MudStack>
<MudDivider Class="my-1" />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1" Style="font-weight: bold;">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Style="font-weight: bold;">@FormatPrice(_order.VatInfo.TotalAmount)</MudText>
<MudText Typo="Typo.subtitle1" Class="fw-bold">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="fw-bold">@FormatPrice(_order.VatInfo.TotalAmount)</MudText>
</MudStack>
}
else
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1" Style="font-weight: bold;">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Style="font-weight: bold;">@FormatPrice(subtotal)</MudText>
<MudText Typo="Typo.subtitle1" Class="fw-bold">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="fw-bold">@FormatPrice(subtotal)</MudText>
</MudStack>
}
</MudStack>
@@ -1,6 +1,7 @@
using FrontOffice.BFF.UserOrder.Protobuf.Protos.UserOrder;
using CMSMicroservice.Protobuf.Protos.UserOrder;
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using Messages = CMSMicroservice.Protobuf.Protos;
namespace FrontOffice.Main.Pages.Store;
@@ -22,28 +23,28 @@ public partial class OrderDetail : ComponentBase
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
private string GetStatusText(PaymentStatus orderPaymentStatus)
private string GetStatusText(Messages.PaymentStatus orderPaymentStatus)
{
return orderPaymentStatus switch
{
PaymentStatus.Pending => "در انتظار پرداخت",
PaymentStatus.Success => "پرداخت شده",
PaymentStatus.Reject => "پرداخت ناموفق",
Messages.PaymentStatus.Pending => "در انتظار پرداخت",
Messages.PaymentStatus.Success => "پرداخت شده",
Messages.PaymentStatus.Reject => "پرداخت ناموفق",
_ => "نامشخص",
};
}
private string GetPaymentMethodText(PaymentMethod orderPaymentMethod)
private string GetPaymentMethodText(Messages.PaymentMethod orderPaymentMethod)
{
return orderPaymentMethod switch
{
PaymentMethod.Wallet => "کیف پول",
PaymentMethod.Ipg => "درگاه پرداخت اینترنتی",
Messages.PaymentMethod.Wallet => "کیف پول",
Messages.PaymentMethod.Ipg => "درگاه پرداخت اینترنتی",
_ => "نامشخص",
};
}
private static string GetProductImageUrl(string? imageUrl)
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : UrlUtility.DownloadUrl+imageUrl;
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/');
}
@@ -1,9 +1,10 @@
@using FrontOffice.BFF.UserOrder.Protobuf.Protos.UserOrder
@using CMSMicroservice.Protobuf.Protos.UserOrder
@attribute [Route(RouteConstants.Store.OrderTracking + "{id:long}")]
<PageTitle>پیگیری سفارش</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<PageHeader Title="پیگیری سفارش" BackHref="@RouteConstants.Store.Orders" />
@if (_loading)
{
<MudStack AlignItems="AlignItems.Center">
@@ -15,7 +16,7 @@
{
<MudAlert Severity="Severity.Warning">سفارش یافت نشد.</MudAlert>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack"
Href="@RouteConstants.Store.Orders">
OnClick="GoBack">
بازگشت به سفارش‌ها
</MudButton>
}
@@ -1,12 +1,15 @@
using FrontOffice.BFF.UserOrder.Protobuf.Protos.UserOrder;
using CMSMicroservice.Protobuf.Protos.UserOrder;
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using Messages = CMSMicroservice.Protobuf.Protos;
namespace FrontOffice.Main.Pages.Store;
public partial class OrderTracking : ComponentBase
{
[Inject] private OrderService OrderService { get; set; } = default!;
[Inject] private IJSRuntime JS { get; set; } = default!;
[Parameter] public long id { get; set; }
@@ -45,7 +48,7 @@ public partial class OrderTracking : ComponentBase
if (_order is null) return;
// Build tracking steps based on PaymentStatus
var isPaid = _order.PaymentStatus == PaymentStatus.Success;
var isPaid = _order.PaymentStatus == Messages.PaymentStatus.Success;
_trackingSteps = new List<TrackingStep>
{
@@ -107,10 +110,10 @@ public partial class OrderTracking : ComponentBase
return $"{persianCalendar.GetYear(date)}/{persianCalendar.GetMonth(date):00}/{persianCalendar.GetDayOfMonth(date):00}";
}
private static string GetPaymentMethodText(PaymentMethod method) => method switch
private static string GetPaymentMethodText(Messages.PaymentMethod method) => method switch
{
PaymentMethod.Ipg => "پرداخت آنلاین",
PaymentMethod.Wallet => "کیف پول",
Messages.PaymentMethod.Ipg => "پرداخت آنلاین",
Messages.PaymentMethod.Wallet => "کیف پول",
_ => "نامشخص"
};
@@ -122,4 +125,6 @@ public partial class OrderTracking : ComponentBase
public bool IsCurrent { get; set; }
public string Date { get; set; } = "";
}
private async Task GoBack() => await JS.InvokeVoidAsync("history.back");
}
+24 -8
View File
@@ -4,12 +4,10 @@
<PageTitle>سفارشات من</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudText Typo="Typo.h4" Class="mb-3">سفارشات من</MudText>
<PageHeader Title="سفارشات من" BackHref="@RouteConstants.Store.Products" />
@if (_loading)
{
<MudStack AlignItems="AlignItems.Center" Class="py-4">
<MudProgressCircular Indeterminate="true" Color="Color.Primary" />
</MudStack>
<LoadingState />
}
else if (_orders.Count == 0)
{
@@ -28,9 +26,18 @@
</HeaderContent>
<RowTemplate>
<MudTd>@context.Id</MudTd>
<MudTd>@context.PaymentDate.ToDateTime().MiladiToJalaliWithTime()</MudTd>
<MudTd>
@if (context.PaymentDate != null)
{
@context.PaymentDate.ToDateTime().MiladiToJalaliWithTime()
}
else
{
<text>در انتظار پرداخت</text>
}
</MudTd>
<MudTd>@GetStatusText(context.PaymentStatus)</MudTd>
<MudTd>@FormatPrice(context.FactorDetails.Sum(s=>s.UnitPrice.Value*s.Count.Value))</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>
@@ -46,11 +53,20 @@
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText>سفارش #@o.Id</MudText>
<MudText Class="mud-text-secondary">@o.PaymentDate.ToDateTime().MiladiToJalaliWithTime()</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.Value*s.Count.Value))</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>
@@ -1,7 +1,8 @@
using System;
using FrontOffice.BFF.UserOrder.Protobuf.Protos.UserOrder;
using CMSMicroservice.Protobuf.Protos.UserOrder;
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using Messages = CMSMicroservice.Protobuf.Protos;
using MudBlazor;
namespace FrontOffice.Main.Pages.Store;
@@ -45,13 +46,13 @@ public partial class Orders : ComponentBase
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
private string GetStatusText(PaymentStatus contextPaymentStatus)
private string GetStatusText(Messages.PaymentStatus contextPaymentStatus)
{
return contextPaymentStatus switch
{
PaymentStatus.Pending => "در انتظار پرداخت",
PaymentStatus.Success => "پرداخت شده",
PaymentStatus.Reject => "پرداخت ناموفق",
Messages.PaymentStatus.Pending => "در انتظار پرداخت",
Messages.PaymentStatus.Success => "پرداخت شده",
Messages.PaymentStatus.Reject => "پرداخت ناموفق",
_ => "نامشخص",
};
}
@@ -6,23 +6,22 @@
@if (_loading)
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudStack AlignItems="AlignItems.Center">
<MudProgressCircular Color="Color.Primary" Indeterminate="true"/>
</MudStack>
<LoadingState />
</MudContainer>
}
else if (_product is null)
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudAlert Severity="Severity.Warning">محصول یافت نشد.</MudAlert>
<MudButton Class="mt-2" Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack"
OnClick="() => Navigation.NavigateTo(RouteConstants.Store.Products)">بازگشت
</MudButton>
<EmptyState Icon="@Icons.Material.Filled.SearchOff"
Title="محصول یافت نشد."
ActionText="بازگشت"
ActionHref="@RouteConstants.Store.Products" />
</MudContainer>
}
else
{
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<PageHeader Title="جزئیات محصول" BackHref="@RouteConstants.Store.Products" />
<MudGrid Spacing="3">
<MudItem xs="12" md="6">
@if (_product is not null)
@@ -32,7 +31,9 @@ else
<MudPaper Class="rounded-lg pa-2">
@if (!string.IsNullOrWhiteSpace(MainImageUrl))
{
<div class="rounded-lg" style="width:100%;height:360px;background-image: url('@MainImageUrl');background-size: cover; background-position: center;border-radius: 0.5rem;color: transparent;" >@MainImageAlt</div>
<AppImage Path="@MainImageUrl" Alt="@MainImageAlt"
ObjectFit="ObjectFit.Cover"
Style="width:100%; max-height:400px; border-radius:12px;" />
}
else
{
@@ -45,7 +46,7 @@ else
@foreach (var item in _galleryItems)
{
<MudButton Variant="Variant.Text" Class="p-0" Style="min-width:0;" OnClick="() => SelectGalleryImage(item)">
<MudImage Src="@GetThumbnailUrl(item)" Alt="@item.Title" Class="rounded-sm" Style="@GetThumbnailStyle(item)" />
<AppImage Path="@GetThumbnailUrl(item)" Alt="@item.Title" Class="rounded-sm" Style="@GetThumbnailStyle(item)" />
</MudButton>
}
</MudStack>
@@ -3,7 +3,18 @@
<PageTitle>محصولات</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="pa-2 pa-md-6">
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<!-- Hero Banner -->
<MudPaper Class="store-hero pa-6 pa-md-8 mb-4 rounded-xl" Elevation="0">
<MudStack AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Storefront" Size="Size.Large" Class="dash-hero-name" />
<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>
<MudPaper Elevation="1" Class="pa-3 pa-md-4 mb-4 rounded-lg">
<MudGrid Spacing="2" AlignItems="AlignItems.Center">
@* ردیف اول: جستجو و دکمه‌ها *@
@@ -44,25 +55,25 @@
Margin="Margin.Dense"
AnchorOrigin="Origin.BottomCenter"
AdornmentIcon="@Icons.Material.Filled.Sort">
<MudSelectItem Value="ProductSortOption.PriceDesc">
<MudSelectItem T="ProductSortOption" Value="ProductSortOption.PriceDesc">
<div class="d-flex align-center gap-2">
<MudIcon Icon="@Icons.Material.Filled.ArrowDownward" Size="Size.Small"/>
گران‌ترین
</div>
</MudSelectItem>
<MudSelectItem Value="ProductSortOption.PriceAsc">
<MudSelectItem T="ProductSortOption" Value="ProductSortOption.PriceAsc">
<div class="d-flex align-center gap-2">
<MudIcon Icon="@Icons.Material.Filled.ArrowUpward" Size="Size.Small"/>
ارزان‌ترین
</div>
</MudSelectItem>
<MudSelectItem Value="ProductSortOption.Newest">
<MudSelectItem T="ProductSortOption" Value="ProductSortOption.Newest">
<div class="d-flex align-center gap-2">
<MudIcon Icon="@Icons.Material.Filled.NewReleases" Size="Size.Small"/>
جدیدترین
</div>
</MudSelectItem>
<MudSelectItem Value="ProductSortOption.Title">
<MudSelectItem T="ProductSortOption" Value="ProductSortOption.Title">
<div class="d-flex align-center gap-2">
<MudIcon Icon="@Icons.Material.Filled.SortByAlpha" Size="Size.Small"/>
الفبایی
+8 -3
View File
@@ -8,15 +8,19 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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" />
<base href="~/" />
<link href="css/site.css" rel="stylesheet" />
<link href="FrontOffice.Main.styles.css" rel="stylesheet" />
<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" />
<!-- Ensure latest MudBlazor CSS is used (cache-busting) -->
<!-- MudBlazor CSS (باید قبل از site.css باشه تا override بشه) -->
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" asp-append-version="true" />
<!-- Custom styles (بعد از MudBlazor برای override) -->
<link href="css/site.css" rel="stylesheet" asp-append-version="true" />
<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" />
</head>
@@ -42,6 +46,7 @@
<script src="js/d3-flextree.min.js"></script>
<script src="js/d3-org-chart3.js"></script>
<script src="js/org-chart.js" asp-append-version="true"></script>
<script src="js/landing.js" asp-append-version="true"></script>
<script src="_framework/blazor.server.js"></script>
<script>
+101
View File
@@ -0,0 +1,101 @@
@inject ImageCacheService ImageCache
@if (_loading)
{
<MudSkeleton SkeletonType="SkeletonType.Rectangle"
Width="@(ImgWidth > 0 ? $"{ImgWidth}px" : SkeletonWidth)"
Height="@(ImgHeight > 0 ? $"{ImgHeight}px" : SkeletonHeight)"
Class="@Class" Style="@Style" />
}
else if (!string.IsNullOrWhiteSpace(_resolvedSrc))
{
<MudImage Src="@_resolvedSrc"
Alt="@Alt"
ObjectFit="@ObjectFit"
ObjectPosition="@ObjectPosition"
Elevation="@Elevation"
Fluid="@Fluid"
Width="@ImgWidth"
Height="@ImgHeight"
Style="@Style"
Class="@Class" />
}
else if (!string.IsNullOrWhiteSpace(Fallback))
{
<div class="@($"d-flex align-center justify-center {Class}")" style="@Style">
<MudIcon Icon="@Fallback" Size="Size.Large" Style="color:rgba(0,0,0,.15);" />
</div>
}
@code {
/// <summary>مسیر تصویر (نسبی، data-URI، یا http)</summary>
[Parameter] public string? Path { get; set; }
/// <summary>متن جایگزین</summary>
[Parameter] public string? Alt { get; set; }
/// <summary>نحوه جایگیری تصویر</summary>
[Parameter] public ObjectFit ObjectFit { get; set; } = ObjectFit.Cover;
/// <summary>موقعیت تصویر</summary>
[Parameter] public ObjectPosition ObjectPosition { get; set; } = ObjectPosition.Center;
/// <summary>سایه تصویر</summary>
[Parameter] public int Elevation { get; set; }
/// <summary>تصویر سیال (100% عرض)</summary>
[Parameter] public bool Fluid { get; set; }
/// <summary>استایل inline</summary>
[Parameter] public string? Style { get; set; }
/// <summary>کلاس CSS</summary>
[Parameter] public string? Class { get; set; }
/// <summary>عرض تصویر (پیکسل) — مانند MudImage</summary>
[Parameter] public int? ImgWidth { get; set; }
/// <summary>ارتفاع تصویر (پیکسل) — مانند MudImage</summary>
[Parameter] public int? ImgHeight { get; set; }
/// <summary>عرض placeholder skeleton (مقدار CSS)</summary>
[Parameter] public string? SkeletonWidth { get; set; }
/// <summary>ارتفاع placeholder skeleton (مقدار CSS)</summary>
[Parameter] public string? SkeletonHeight { get; set; }
/// <summary>آیکون fallback هنگام عدم وجود تصویر</summary>
[Parameter] public string? Fallback { get; set; }
private string? _resolvedSrc;
private bool _loading;
private string? _lastPath;
protected override async Task OnParametersSetAsync()
{
// فقط وقتی Path تغییر کرده، resolve مجدد انجام شود
if (Path == _lastPath) return;
_lastPath = Path;
if (string.IsNullOrWhiteSpace(Path))
{
_resolvedSrc = null;
_loading = false;
return;
}
// اگر قبلاً base64 هست → نمایش فوری
if (Path.StartsWith("data:", StringComparison.Ordinal))
{
_resolvedSrc = Path;
_loading = false;
return;
}
_loading = true;
StateHasChanged();
_resolvedSrc = await ImageCache.ResolveAsync(Path);
_loading = false;
}
}
+91 -158
View File
@@ -1,168 +1,101 @@
@if (InlineMode)
{
@* Inline rendering without MudDialog wrapper *@
<MudStack Spacing="2">
<MudText Typo="Typo.h5" Align="Align.Center">@GetDialogTitle()</MudText>
@PhoneOrVerifyContent()
</MudStack>
<div class="auth-dialog-content">
<MudStack Spacing="3" AlignItems="AlignItems.Center" Class="mb-4">
<MudAvatar Size="Size.Large" Color="Color.Primary" Variant="Variant.Filled">
<MudIcon Icon="@(_currentStep == AuthStep.Phone ? Icons.Material.Outlined.PhoneAndroid : Icons.Material.Outlined.LockOpen)" Size="Size.Large" />
</MudAvatar>
<MudText Typo="Typo.h5">@GetDialogTitle()</MudText>
</MudStack>
<PhoneVerifyForm @ref="_phoneVerifyForm"
CurrentStep="_currentStep"
PhoneRequest="_phoneRequest"
VerifyRequest="_verifyRequest"
CaptchaCode="@_captchaCode"
CaptchaInput="@_captchaInput"
CaptchaInputChanged="@(v => _captchaInput = v)"
OnRefreshCaptcha="GenerateCaptcha"
IsBusy="_isBusy"
ErrorMessage="@_errorMessage"
InfoMessage="@_infoMessage"
PhoneNumber="@_phoneNumber"
ResendRemaining="_resendRemaining"
OnChangePhone="ChangePhoneAsync"
OnResendOtp="ResendOtpAsync" />
</div>
}
else
{
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h4" Align="Align.Center">@GetDialogTitle()</MudText>
</TitleContent>
<MudDialog Class="auth-dialog-wrapper">
<DialogContent>
@PhoneOrVerifyContent()
<div class="auth-dialog-content">
<MudStack Spacing="2" AlignItems="AlignItems.Center" Class="mb-4 pt-2">
<MudAvatar Size="Size.Large" Color="Color.Primary" Variant="Variant.Filled">
<MudIcon Icon="@(_currentStep == AuthStep.Phone ? Icons.Material.Outlined.PhoneAndroid : Icons.Material.Outlined.LockOpen)" Size="Size.Large" />
</MudAvatar>
<MudText Typo="Typo.h5" Align="Align.Center">@GetDialogTitle()</MudText>
</MudStack>
<PhoneVerifyForm @ref="_phoneVerifyForm"
CurrentStep="_currentStep"
PhoneRequest="_phoneRequest"
VerifyRequest="_verifyRequest"
CaptchaCode="@_captchaCode"
CaptchaInput="@_captchaInput"
CaptchaInputChanged="@(v => _captchaInput = v)"
OnRefreshCaptcha="GenerateCaptcha"
IsBusy="_isBusy"
ErrorMessage="@_errorMessage"
InfoMessage="@_infoMessage"
PhoneNumber="@_phoneNumber"
ResendRemaining="_resendRemaining"
OnChangePhone="ChangePhoneAsync"
OnResendOtp="ResendOtpAsync" />
<MudStack Class="mt-4" Spacing="2">
@if (_currentStep == AuthStep.Phone)
{
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="SendOtpAsync"
Disabled="_isBusy"
Class="rounded-lg"
FullWidth="true"
Size="Size.Large">
@if (_isBusy)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="me-2" />
}
ارسال رمز پویا
</MudButton>
}
else if (_currentStep == AuthStep.Verify)
{
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="VerifyOtpAsync"
Disabled="_isBusy || IsVerificationLocked"
Class="rounded-lg"
FullWidth="true"
Size="Size.Large">
@if (_isBusy)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="me-2" />
}
تأیید و ورود
</MudButton>
}
@if (!HideCancelButton)
{
<MudButton Variant="Variant.Text"
OnClick="Cancel"
Disabled="_isBusy"
FullWidth="true">لغو
</MudButton>
}
</MudStack>
</div>
</DialogContent>
<DialogActions>
@if (!HideCancelButton)
{
<MudButton Variant="Variant.Text"
OnClick="Cancel"
Disabled="_isBusy">لغو
</MudButton>
}
@if (_currentStep == AuthStep.Phone)
{
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="SendOtpAsync"
Disabled="_isBusy"
FullWidth="true">
ارسال رمز پویا
</MudButton>
}
else if (_currentStep == AuthStep.Verify)
{
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="VerifyOtpAsync"
Disabled="_isBusy || IsVerificationLocked"
FullWidth="true">
تأیید و ورود
</MudButton>
}
</DialogActions>
</MudDialog>
}
@code {
private RenderFragment PhoneOrVerifyContent() => __builder =>
{
if (_currentStep == AuthStep.Phone)
{
// Phone Step
__builder.OpenComponent(0, typeof(MudText));
__builder.AddAttribute(1, "Typo", Typo.body2);
__builder.AddAttribute(2, "Class", "mb-4");
__builder.AddAttribute(3, "Align", Align.Center);
__builder.AddContent(4, "لطفاً شماره موبایل خود را وارد کنید تا رمز پویا ارسال شود.");
__builder.CloseComponent();
<MudForm @ref="_phoneForm" Model="_phoneRequest" Validation="@(_phoneRequestValidator.ValidateValue)">
<MudTextField @bind-Value="_phoneRequest.Mobile"
For="@(() => _phoneRequest.Mobile)"
Label="شماره موبایل"
InputType="InputType.Telephone"
InputMode="InputMode.tel"
Variant="Variant.Outlined"
Immediate="true"
Required="true"
RequiredError="وارد کردن شماره موبایل الزامی است."
HelperText="مثال: 09121234567"
Class="mb-2"/>
@* @if (EnableCaptcha) *@
@* { *@
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1" Class="mt-2 mb-2">
<MudItem xs="7" md="12">
<MudTextField xs="4" Label="کد کپچا" Placeholder="کد نمایش داده شده" Immediate="true"
Variant="Variant.Outlined" @bind-Value="_captchaInput" Required="true"
RequiredError="لطفاً کد کپچا را وارد کنید."/>
</MudItem>
<MudItem xs="4" >
<MudPaper Elevation="1" Class="captcha-box d-flex align-center justify-center"
Style="min-width:100px;min-height:48px;">
<MudText Typo="Typo.h5">@_captchaCode</MudText>
</MudPaper>
</MudItem>
<MudItem xs="1" >
<MudButton Variant="Variant.Text" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Refresh" Disabled="_isBusy"
OnClick="GenerateCaptcha">
</MudButton>
</MudItem>
</MudStack>
@* } *@
@* *@
<MudCheckBox T="bool" Required="true"
Label="شرایط و قوانین را می‌پذیرم"
RequiredError="برای ادامه باید شرایط و قوانین را بپذیرید."
Class="mb-1"/>
@if (!string.IsNullOrWhiteSpace(_errorMessage))
{
<MudAlert Severity="Severity.Error" Dense="true" Elevation="0"
Class="mb-2">@_errorMessage</MudAlert>
}
</MudForm>
}
else if (_currentStep == AuthStep.Verify)
{
// Verify Step
<MudText Typo="Typo.body2" Class="mb-4" Align="Align.Center">رمز پویا شش رقمی ارسال ‌شده به @_phoneNumber را
وارد کنید.
</MudText>
<MudForm @ref="_verifyForm" Model="_verifyRequest" Validation="@(_verifyRequestValidator.ValidateValue)">
<MudTextField @bind-Value="_verifyRequest.Code"
For="@(() => _verifyRequest.Code)"
Label="رمز پویا"
InputType="InputType.Telephone"
InputMode="InputMode.tel"
Variant="Variant.Outlined"
Immediate="true"
Required="true"
RequiredError="وارد کردن رمز پویا الزامی است."
HelperText="کد ۶ رقمی"
Class="mb-2"
MaxLength="6"/>
@if (!string.IsNullOrWhiteSpace(_errorMessage))
{
<MudAlert Severity="Severity.Error" Dense="true" Elevation="0"
Class="mb-2">@(_errorMessage)</MudAlert>
}
@if (!string.IsNullOrWhiteSpace(_infoMessage))
{
<MudAlert Severity="Severity.Success" Dense="true" Elevation="0"
Class="mb-2">@(_infoMessage)</MudAlert>
}
<MudStack Spacing="2">
<MudButton Variant="Variant.Text" Color="Color.Secondary" Disabled="_isBusy"
OnClick="ChangePhoneAsync">تغییر شماره
</MudButton>
</MudStack>
<MudDivider Class="my-2"/>
@if (_resendRemaining > 0)
{
<MudText Typo="Typo.body2" Align="Align.Center" Class="mud-text-secondary">امکان ارسال مجدد
تا @_resendRemaining ثانیه دیگر
</MudText>
}
else
{
<MudButton Variant="Variant.Text" Color="Color.Primary" Disabled="_isBusy" OnClick="ResendOtpAsync">
ارسال مجدد رمز پویا
</MudButton>
}
</MudForm>
}
};
}
+20 -18
View File
@@ -1,6 +1,5 @@
using Blazored.LocalStorage;
using FrontOffice.BFF.User.Protobuf.Protos.User;
using FrontOffice.BFF.User.Protobuf.Validator;
using CMSMicroservice.Protobuf.Protos.User;
using FrontOffice.Main.Utilities;
using Grpc.Core;
using Microsoft.AspNetCore.Components;
@@ -25,13 +24,10 @@ public partial class AuthDialog : IDisposable
public AuthStep _currentStep = AuthStep.Phone;
private readonly CreateNewOtpTokenRequestValidator _phoneRequestValidator = new();
private readonly CreateNewOtpTokenRequest _phoneRequest = new();
private MudForm? _phoneForm;
private PhoneVerifyForm? _phoneVerifyForm;
private readonly VerifyOtpTokenRequestValidator _verifyRequestValidator = new();
private readonly VerifyOtpTokenRequest _verifyRequest = new();
private MudForm? _verifyForm;
private bool _isBusy;
private string? _phoneNumber;
@@ -82,11 +78,12 @@ public partial class AuthDialog : IDisposable
public async Task SendOtpAsync()
{
_errorMessage = null;
if (_phoneForm is null)
var phoneForm = _phoneVerifyForm?.GetPhoneForm();
if (phoneForm is null)
return;
await _phoneForm.Validate();
if (!_phoneForm.IsValid)
await phoneForm.Validate();
if (!phoneForm.IsValid)
return;
// if (EnableCaptcha)
@@ -105,10 +102,10 @@ public partial class AuthDialog : IDisposable
try
{
var validationResult = _phoneRequestValidator.Validate(_phoneRequest);
if (!validationResult.IsValid)
var validationResult = true; // _phoneRequestValidator.Validate(_phoneRequest);
if (!validationResult)
{
_errorMessage = string.Join(" ", validationResult.Errors.Select(e => e.ErrorMessage).Distinct());
_errorMessage = "لطفاً شماره تلفن معتبر وارد کنید."; // string.Join(" ", validationResult.Errors.Select(e => e.ErrorMessage).Distinct());
return;
}
@@ -157,11 +154,12 @@ public partial class AuthDialog : IDisposable
_errorMessage = null;
_infoMessage = null;
if (_verifyForm is null)
var verifyForm = _phoneVerifyForm?.GetVerifyForm();
if (verifyForm is null)
return false;
await _verifyForm.Validate();
if (!_verifyForm.IsValid)
await verifyForm.Validate();
if (!verifyForm.IsValid)
return false;
if (IsVerificationLocked)
@@ -189,10 +187,10 @@ public partial class AuthDialog : IDisposable
_verifyRequest.ParentReferralCode = storedReferralCode;
}
var validationResult = _verifyRequestValidator.Validate(_verifyRequest);
if (!validationResult.IsValid)
var validationResult = true; // _verifyRequestValidator.Validate(_verifyRequest);
if (!validationResult)
{
_errorMessage = string.Join(" ", validationResult.Errors.Select(e => e.ErrorMessage).Distinct());
_errorMessage = "لطفاً کد تأیید معتبر وارد کنید."; // string.Join(" ", validationResult.Errors.Select(e => e.ErrorMessage).Distinct());
return false;
}
@@ -239,6 +237,10 @@ public partial class AuthDialog : IDisposable
return true;
}
// اگه سرور RemainingAttempts برگردونده، از اون استفاده کن
if (response.RemainingAttempts > 0)
_attemptsLeft = response.RemainingAttempts;
RegisterFailedAttempt(string.IsNullOrWhiteSpace(response.Message) ? "کد نادرست است." : response.Message);
}
catch (RpcException rpcEx)
@@ -1,8 +1,10 @@
@using FrontOffice.BFF.ClubMembership.Protobuf.Protos.ClubMembership
@using CMSMicroservice.Protobuf.Protos.ClubMembership
@using CMSMicroservice.Protobuf.Protos.OtpToken
@using Blazored.LocalStorage
@using FrontOffice.Main.Utilities
@using MudBlazor
@inject ClubMembershipContract.ClubMembershipContractClient ClubMembershipClient
@inject OtpTokenContract.OtpTokenContractClient OtpTokenClient
@inject ILocalStorageService LocalStorage
@inject AuthService AuthService
@inject ISnackbar Snackbar
@@ -129,23 +131,26 @@
_isLoading = true;
try
{
var response = await ClubMembershipClient.RequestClubContractOtpAsync(
new RequestClubContractOtpRequest
var response = await OtpTokenClient.CreateNewOtpTokenAsync(
new CMSMicroservice.Protobuf.Protos.OtpToken.CreateNewOtpTokenRequest
{
Mobile = await GetUserMobileAsync(),
Purpose = "signClubContract",
SignGuid = _signGuid.ToString()
});
if (response.Success)
{
_step = ContractStep.EnterOtp;
_remainingSeconds = response.RemainingSeconds > 0 ? response.RemainingSeconds : 120;
_remainingSeconds = 120; // Default 2 minutes
StartCountdown();
}
else
{
Snackbar.Add(response.Message, Severity.Error);
Snackbar.Add(response.Message ?? "ارسال رمز پویا با خطا مواجه شد.", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا در ارسال کد: {ex.Message}", Severity.Error);
@@ -163,7 +168,7 @@
try
{
var response = await ClubMembershipClient.AcceptClubMembershipContractAsync(
new AcceptClubMembershipContractRequest
new CMSMicroservice.Protobuf.Protos.ClubMembership.AcceptClubMembershipContractRequest
{
OtpCode = _otpCode,
SignGuid = _signGuid.ToString(),
@@ -172,21 +177,16 @@
if (response.Success)
{
// ذخیره توکن جدید
if (!string.IsNullOrEmpty(response.Token))
{
await LocalStorage.SetItemAsync(TokenStorageKey, response.Token);
await AuthService.InitUserAuthInfo();
}
_step = ContractStep.Success;
StopCountdown();
Snackbar.Add("قرارداد باشگاه مشتریان با موفقیت امضا شد.", Severity.Success);
}
else
{
Snackbar.Add(response.Message, Severity.Error);
Snackbar.Add(response.Message ?? "امضای قرارداد با خطا مواجه شد.", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا در ثبت قرارداد: {ex.Message}", Severity.Error);
@@ -233,8 +233,7 @@
private string GetClubContractHtml()
{
return @"
<div style='direction: rtl; font-family: Tahoma, sans-serif; line-height: 2; text-align: justify;'>
return $@"<div style='direction: rtl; font-family: Tahoma, sans-serif; line-height: 2; text-align: justify;'>
<h2 style='text-align: center; margin-bottom: 20px;'>قرارداد عضویت در باشگاه مشتریان کارابازار</h2>
<p><strong>ماده ۱ - تعاریف</strong></p>
@@ -265,7 +264,21 @@
<p>۶-۱. در صورت بروز اختلاف، طرفین ابتدا از طریق مذاکره اقدام به حل و فصل خواهند کرد.</p>
<p>۶-۲. در صورت عدم توافق، مراجع قضایی صلاحیت‌دار رسیدگی خواهند کرد.</p>
<p style='text-align: center; margin-top: 30px;'><strong>شناسه قرارداد: " + _signGuid + @"</strong></p>
<p style='text-align: center; margin-top: 30px;'><strong>شناسه قرارداد: {_signGuid}</strong></p>
</div>";
}
private async Task<string> GetUserMobileAsync()
{
// Get user mobile from auth service or localStorage
var userInfo = await AuthService.GetUserAuthInfo();
return userInfo?.MobileNumber ?? "";
}
private async Task<long> GetCurrentUserIdAsync()
{
// Get current user ID from auth service
var userInfo = await AuthService.GetUserAuthInfo();
return userInfo?.UserId ?? 0;
}
}
@@ -0,0 +1,67 @@
@inject IJSRuntime JS
@* ═══════════════════════════════════════════════
EmptyState — Unified empty/not-found placeholder
Usage: <EmptyState Title="سفارشی ثبت نشده" />
<EmptyState Title="آدرسی یافت نشد"
Icon="@Icons.Material.Filled.LocationOff"
Description="ابتدا یک آدرس اضافه کنید."
ActionText="افزودن آدرس"
OnAction="OpenAddDialog" />
═══════════════════════════════════════════════ *@
<MudStack AlignItems="AlignItems.Center" Class="py-12" Spacing="3">
<MudIcon Icon="@Icon" Size="Size.Large" Color="Color.Default"
Class="mud-text-disabled" Style="font-size:3rem;" />
<MudText Typo="Typo.h6" Class="mud-text-secondary">@Title</MudText>
@if (!string.IsNullOrWhiteSpace(Description))
{
<MudText Typo="Typo.body2" Class="mud-text-secondary text-center"
Style="max-width:400px;">@Description</MudText>
}
@if (ChildContent is not null)
{
@ChildContent
}
else if (!string.IsNullOrWhiteSpace(ActionText))
{
@if (!string.IsNullOrWhiteSpace(ActionHref))
{
<MudButton Variant="Variant.Filled" Color="Color.Primary"
OnClick="GoBack" Class="mt-2">@ActionText</MudButton>
}
else
{
<MudButton Variant="Variant.Filled" Color="Color.Primary"
OnClick="OnAction" Class="mt-2">@ActionText</MudButton>
}
}
</MudStack>
@code {
/// <summary>Material icon string.</summary>
[Parameter] public string Icon { get; set; } = Icons.Material.Filled.Inbox;
/// <summary>Main heading text.</summary>
[Parameter] public string Title { get; set; } = "موردی یافت نشد";
/// <summary>Optional secondary description.</summary>
[Parameter] public string? Description { get; set; }
/// <summary>Optional CTA button text.</summary>
[Parameter] public string? ActionText { get; set; }
/// <summary>If set, CTA becomes a link instead of callback.</summary>
[Parameter] public string? ActionHref { get; set; }
/// <summary>CTA button callback.</summary>
[Parameter] public EventCallback OnAction { get; set; }
/// <summary>Optional custom content (replaces ActionText button).</summary>
[Parameter] public RenderFragment? ChildContent { get; set; }
private async Task GoBack()
{
await JS.InvokeVoidAsync("history.back");
}
}
@@ -0,0 +1,18 @@
@* ═══════════════════════════════════════════════
LoadingState — Unified loading indicator
Usage: <LoadingState />
<LoadingState Message="در حال دریافت سفارشات..." />
═══════════════════════════════════════════════ *@
<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 {
/// <summary>Optional loading message displayed below spinner.</summary>
[Parameter] public string Message { get; set; } = "در حال بارگذاری...";
}
+180 -38
View File
@@ -9,10 +9,10 @@
<MudSnackbarProvider/>
<MudLayout>
<!-- AppBar -->
<MudAppBar Elevation="0" Color="Color.Transparent" Dense="true" Class="py-2" id="top">
<MudAppBar Elevation="1" Color="Color.Surface" Dense="true" Class="py-2" id="top">
<MudContainer MaxWidth="MaxWidth.Large" Class="d-flex align-center justify-space-between w-100">
<MudHidden Breakpoint="Breakpoint.SmAndUp">
<MudHidden Breakpoint="Breakpoint.MdAndUp">
<MudIconButton Class="d-inline"
Icon="@Icons.Material.Filled.Menu"
OnClick="@ToggleDrawer"/>
@@ -21,7 +21,7 @@
<div class="d-flex align-center gap-2">
<MudLink Href="@(RouteConstants.Main.MainPage)" Underline="Underline.None">
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center">
<MudHidden Breakpoint="Breakpoint.SmAndUp" Invert="true">
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudImage ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Width="32"
@@ -32,37 +32,66 @@
<MudText Typo="Typo.h6">کارا بازار سلامت</MudText>
</MudStack>
</MudLink>
@* ── Store context badge (desktop) ── *@
@if (IsInRegularStore)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Filled" Size="Size.Small"
Class="d-none d-md-flex" Icon="@Icons.Material.Filled.Storefront">فروشگاه</MudChip>
}
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>
}
</div>
<div class="d-none d-md-flex align-center gap-10">
@if (_isAuthenticated)
@if (IsInRegularStore && _isAuthenticated)
{
<MudLink Href="@(RouteConstants.Store.Products)" Typo="Typo.subtitle1" Class="mud-link">
محصولات
</MudLink>
<MudLink Href="@(RouteConstants.Store.Cart)" Typo="Typo.subtitle1" Class="mud-link">سبد خرید
</MudLink>
<MudLink Href="@(RouteConstants.Store.Orders)" Typo="Typo.subtitle1" Class="mud-link">سفارشات
من
</MudLink>
@* ── Regular store nav ── *@
<MudLink Href="@(RouteConstants.Store.Products)" Typo="Typo.subtitle1" Class="mud-link">محصولات</MudLink>
<MudLink Href="@(RouteConstants.Store.Cart)" Typo="Typo.subtitle1" Class="mud-link">سبد خرید</MudLink>
<MudLink Href="@(RouteConstants.Store.Orders)" Typo="Typo.subtitle1" Class="mud-link">سفارشات من</MudLink>
}
<MudLink Href="@(RouteConstants.FAQ.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>
else if (IsInDiscountStore && _isAuthenticated)
{
@* ── Discount store nav ── *@
<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>
}
else
{
@* ── Default global nav (not inside any store) ── *@
@if (_isAuthenticated)
{
<MudLink Href="@(RouteConstants.Gateway.StoreChooser)" Typo="Typo.subtitle1" Class="mud-link">فروشگاه‌ها</MudLink>
}
}
<MudLink Href="@(RouteConstants.FAQ.Index)" Typo="Typo.subtitle1" Class="mud-link">سوالات متداول</MudLink>
<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>
</div>
<div class="d-flex align-center gap-2">
@if (_isAuthenticated)
{
@* <MudBadge Content="@_cartCount" Color="Color.Error" Overlap="true" Visible="@(_cartCount > 0)"> *@
@* *@
@* </MudBadge> *@
<MudIconButton Icon="@Icons.Material.Filled.ShoppingCart" Color="@(_cartCount > 0?Color.Success:Color.Inherit)"
Href="@(RouteConstants.Store.Cart)" />
@* ── Cart icon: only visible when inside a store ── *@
@if (IsInRegularStore)
{
<MudIconButton Icon="@Icons.Material.Filled.ShoppingCart"
Color="@(_cartCount > 0 ? Color.Success : Color.Inherit)"
Href="@(RouteConstants.Store.Cart)" />
}
else if (IsInDiscountStore)
{
<MudIconButton Icon="@Icons.Material.Filled.ShoppingCart"
Color="@(_discountCartCount > 0 ? Color.Error : Color.Inherit)"
Href="@(RouteConstants.DiscountStore.Cart)" />
}
@* دکمه بروزرسانی *@
@if (_hasUpdate)
{
@@ -74,7 +103,10 @@
</MudBadge>
}
<MudMenu Icon="@Icons.Material.Filled.Person" Color="Color.Inherit" Size="Size.Medium">
<MudMenuItem OnClick="NavigateToProfile" Disabled="@(!AuthService.IsCompleteRegister())">
<MudMenuItem OnClick="NavigateToProfile" Disabled="@(!_isCompleteRegister)" Icon="@Icons.Material.Outlined.Dashboard">
داشبورد
</MudMenuItem>
<MudMenuItem OnClick="NavigateToHub" Disabled="@(!_isCompleteRegister)" Icon="@Icons.Material.Outlined.Person">
پروفایل
</MudMenuItem>
<MudDivider/>
@@ -110,19 +142,49 @@
<MudStack Spacing="2">
@if (_isAuthenticated)
{
<MudLink Href="@(RouteConstants.Store.Products)" Typo="Typo.subtitle1"
OnClick="() => _drawerOpen=false">محصولات
</MudLink>
<MudLink Href="@(RouteConstants.Store.Cart)" Typo="Typo.subtitle1"
OnClick="() => _drawerOpen=false">سبد خرید
</MudLink>
<MudLink Href="@(RouteConstants.Store.Orders)" Typo="Typo.subtitle1"
OnClick="() => _drawerOpen=false">سفارشات من
</MudLink>
@if (IsInRegularStore)
{
<MudText Typo="Typo.overline" Class="mud-text-secondary">فروشگاه</MudText>
<MudLink Href="@(RouteConstants.Store.Products)" Typo="Typo.subtitle1"
OnClick="() => _drawerOpen=false">محصولات</MudLink>
<MudLink Href="@(RouteConstants.Store.Cart)" Typo="Typo.subtitle1"
OnClick="() => _drawerOpen=false">سبد خرید</MudLink>
<MudLink Href="@(RouteConstants.Store.Orders)" Typo="Typo.subtitle1"
OnClick="() => _drawerOpen=false">سفارشات من</MudLink>
<MudDivider Class="my-1" />
<MudLink Href="@(RouteConstants.Gateway.StoreChooser)" Typo="Typo.subtitle1"
OnClick="() => _drawerOpen=false" Class="mud-text-secondary">
← بازگشت به فروشگاه‌ها
</MudLink>
}
else if (IsInDiscountStore)
{
<MudText Typo="Typo.overline" Style="color:var(--mud-palette-error);">فروشگاه تخفیفی</MudText>
<MudLink Href="@(RouteConstants.DiscountStore.Products)" Typo="Typo.subtitle1"
OnClick="() => _drawerOpen=false">محصولات تخفیفی</MudLink>
<MudLink Href="@(RouteConstants.DiscountStore.Cart)" Typo="Typo.subtitle1"
OnClick="() => _drawerOpen=false">سبد خرید</MudLink>
<MudLink Href="@(RouteConstants.DiscountStore.Orders)" Typo="Typo.subtitle1"
OnClick="() => _drawerOpen=false">سفارشات من</MudLink>
<MudDivider Class="my-1" />
<MudLink Href="@(RouteConstants.Gateway.StoreChooser)" Typo="Typo.subtitle1"
OnClick="() => _drawerOpen=false" Class="mud-text-secondary">
← بازگشت به فروشگاه‌ها
</MudLink>
}
else
{
<MudLink Href="@(RouteConstants.Gateway.StoreChooser)" Typo="Typo.subtitle1"
OnClick="() => _drawerOpen=false">فروشگاه‌ها</MudLink>
}
<MudDivider Class="my-1" />
}
<MudLink Href="@(RouteConstants.FAQ.Index)" Typo="Typo.subtitle1" OnClick="() => _drawerOpen=false">
سوالات متداول
</MudLink>
<MudLink Href="@(RouteConstants.Blog.Index)" Typo="Typo.subtitle1" OnClick="() => _drawerOpen=false">
بلاگ
</MudLink>
<MudLink Href="@(RouteConstants.About.Index)" Typo="Typo.subtitle1" OnClick="() => _drawerOpen=false">
درباره ما
</MudLink>
@@ -165,13 +227,93 @@
</MudStack>
</MudDrawer>
<MudMainContent>
<MudMainContent Class="main-content-wrapper">
@Body
@if (DeviceDetector.IsDesktop())
{
<MudHidden Breakpoint="Breakpoint.SmAndDown">
<Footer/>
}
</MudHidden>
</MudMainContent>
<!-- Mobile Bottom Navigation -->
<MudHidden Breakpoint="Breakpoint.MdAndUp">
<MudPaper Class="bottom-nav" Elevation="8">
<MudStack Row="true" Justify="Justify.SpaceAround" AlignItems="AlignItems.Center" Class="py-1">
@if (IsInRegularStore && _isAuthenticated)
{
@* ── Regular store bottom nav ── *@
<MudLink Href="@(RouteConstants.Store.Products)" Class="bottom-nav-item">
<MudIcon Icon="@Icons.Material.Outlined.Storefront" Size="Size.Medium" />
<MudText Typo="Typo.caption">محصولات</MudText>
</MudLink>
<MudLink Href="@(RouteConstants.Store.Cart)" Class="bottom-nav-item">
<MudIcon Icon="@Icons.Material.Outlined.ShoppingCart" Size="Size.Medium" Color="@(_cartCount > 0 ? Color.Success : Color.Default)" />
<MudText Typo="Typo.caption">سبد خرید</MudText>
</MudLink>
<MudLink Href="@(RouteConstants.Store.Orders)" Class="bottom-nav-item">
<MudIcon Icon="@Icons.Material.Outlined.Receipt" Size="Size.Medium" />
<MudText Typo="Typo.caption">سفارشات</MudText>
</MudLink>
<MudLink Href="@(RouteConstants.Profile.Index)" Class="bottom-nav-item">
<MudIcon Icon="@Icons.Material.Outlined.Home" Size="Size.Medium" />
<MudText Typo="Typo.caption">داشبورد</MudText>
</MudLink>
}
else if (IsInDiscountStore && _isAuthenticated)
{
@* ── Discount store bottom nav ── *@
<MudLink Href="@(RouteConstants.DiscountStore.Products)" Class="bottom-nav-item">
<MudIcon Icon="@Icons.Material.Outlined.Loyalty" Size="Size.Medium" Color="Color.Error" />
<MudText Typo="Typo.caption">محصولات</MudText>
</MudLink>
<MudLink Href="@(RouteConstants.DiscountStore.Cart)" Class="bottom-nav-item">
<MudIcon Icon="@Icons.Material.Outlined.ShoppingCart" Size="Size.Medium" Color="@(_discountCartCount > 0 ? Color.Error : Color.Default)" />
<MudText Typo="Typo.caption">سبد خرید</MudText>
</MudLink>
<MudLink Href="@(RouteConstants.DiscountStore.Orders)" Class="bottom-nav-item">
<MudIcon Icon="@Icons.Material.Outlined.Receipt" Size="Size.Medium" />
<MudText Typo="Typo.caption">سفارشات</MudText>
</MudLink>
<MudLink Href="@(RouteConstants.Profile.Index)" Class="bottom-nav-item">
<MudIcon Icon="@Icons.Material.Outlined.Home" Size="Size.Medium" />
<MudText Typo="Typo.caption">داشبورد</MudText>
</MudLink>
}
else
{
@* ── Default bottom nav (outside stores) ── *@
<MudLink Href="@(RouteConstants.Profile.Index)" Class="bottom-nav-item">
<MudIcon Icon="@Icons.Material.Outlined.Home" Size="Size.Medium" />
<MudText Typo="Typo.caption">خانه</MudText>
</MudLink>
@if (_isAuthenticated)
{
<MudLink Href="@(RouteConstants.Gateway.StoreChooser)" Class="bottom-nav-item">
<MudIcon Icon="@Icons.Material.Outlined.Store" Size="Size.Medium" />
<MudText Typo="Typo.caption">فروشگاه‌ها</MudText>
</MudLink>
}
<MudLink Href="@(RouteConstants.Blog.Index)" Class="bottom-nav-item">
<MudIcon Icon="@Icons.Material.Outlined.Article" Size="Size.Medium" />
<MudText Typo="Typo.caption">بلاگ</MudText>
</MudLink>
@if (_isAuthenticated)
{
<MudLink Href="@(RouteConstants.Profile.Hub)" Class="bottom-nav-item">
<MudIcon Icon="@Icons.Material.Outlined.Person" Size="Size.Medium" />
<MudText Typo="Typo.caption">پروفایل</MudText>
</MudLink>
}
else
{
<MudLink Class="bottom-nav-item" @onclick="OpenAuthDialog">
<MudIcon Icon="@Icons.Material.Outlined.Login" Size="Size.Medium" />
<MudText Typo="Typo.caption">ورود</MudText>
</MudLink>
}
}
</MudStack>
</MudPaper>
</MudHidden>
</MudLayout>
</MudRTLProvider>
+116 -1
View File
@@ -1,6 +1,7 @@
using Blazored.LocalStorage;
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.JSInterop;
using MudBlazor;
using Microsoft.AspNetCore.Components.Authorization;
@@ -16,6 +17,15 @@ public partial class MainLayout : IDisposable
private bool _isAuthenticated;
private string? _email;
private int _cartCount;
private int _discountCartCount;
private bool _isCompleteRegister;
// ── Store Context ──
private enum StoreContext { None, Regular, Discount }
private StoreContext _storeCtx = StoreContext.None;
private bool IsInRegularStore => _storeCtx == StoreContext.Regular;
private bool IsInDiscountStore => _storeCtx == StoreContext.Discount;
private bool IsInAnyStore => _storeCtx != StoreContext.None;
// متغیرهای بروزرسانی
private bool _hasUpdate;
@@ -26,6 +36,7 @@ public partial class MainLayout : IDisposable
[Inject] private AuthService AuthService { get; set; } = default!;
[Inject] private AuthDialogService AuthDialogService { get; set; } = default!;
[Inject] private CartService CartService { get; set; } = default!;
[Inject] private DiscountCartService DiscountCartService { get; set; } = default!;
[Inject] private AppVersionService AppVersionService { get; set; } = default!;
private void ToggleTheme() => _isDark = !_isDark;
@@ -35,20 +46,34 @@ public partial class MainLayout : IDisposable
await JSRuntime.InvokeVoidAsync("history.back");
}
protected override void OnInitialized()
{
Navigation.LocationChanged += OnLocationChanged;
UpdateStoreContext();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JSRuntime.InvokeVoidAsync("changeNavBgOnBodyScroll", "top", null, 1);
await CheckAuthStatus();
await EnforceAuthGuardAsync();
if (_isAuthenticated)
{
// بررسی وضعیت ثبت‌نام کامل
_isCompleteRegister = await AuthService.IsCompleteRegisterAsync();
// لود سبد خرید فقط برای کاربر لاگین شده
await CartService.EnsureInitializedAsync();
CartService.OnChange += OnCartChanged;
_cartCount = CartService.Count;
await DiscountCartService.EnsureInitializedAsync();
DiscountCartService.OnChange += OnDiscountCartChanged;
_discountCartCount = DiscountCartService.Count;
// چک کردن بروزرسانی (بدون نمایش popup - فقط برای نشون دادن آیکون)
await CheckForUpdateSilentlyAsync();
}
@@ -57,6 +82,83 @@ public partial class MainLayout : IDisposable
}
}
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
{
UpdateStoreContext();
_ = InvokeAsync(async () =>
{
await EnforceAuthGuardAsync();
StateHasChanged();
});
}
/// <summary>
/// If the current route requires authentication and the user is not logged in, redirect to home.
/// </summary>
private async Task EnforceAuthGuardAsync()
{
if (_isAuthenticated) return;
// Re-check in case token was set after initial render
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
if (_isAuthenticated) return;
var path = new Uri(Navigation.Uri).AbsolutePath.ToLowerInvariant();
if (IsProtectedRoute(path))
{
Navigation.NavigateTo(RouteConstants.Main.MainPage, replace: true);
}
}
private static bool IsProtectedRoute(string path)
{
// Public routes — no auth needed
if (path is "/" or "/register" or "/about" or "/faq" or "/contact"
or "/packages" or "/stores" or "/products" or "/categories")
return false;
if (path.StartsWith("/blog")) return false;
if (path.StartsWith("/package/")) return false;
if (path.StartsWith("/product/")) return false;
if (path == "/discount-store" || path.StartsWith("/discount-store/product/")) return false;
// Everything else is protected
if (path.StartsWith("/profile")) return true;
if (path.StartsWith("/commission")) return true;
if (path.StartsWith("/network")) return true;
if (path.StartsWith("/club")) return true;
if (path.StartsWith("/checkout")) return true;
if (path.StartsWith("/cart")) return true;
if (path.StartsWith("/orders") || path.StartsWith("/order/") || path.StartsWith("/order-tracking/")) return true;
if (path.StartsWith("/my-packages") || path.StartsWith("/my-orders") || path.StartsWith("/my-cart")) return true;
if (path.StartsWith("/discount-store/cart") || path.StartsWith("/discount-store/checkout")
|| path.StartsWith("/discount-store/orders") || path.StartsWith("/discount-store/order/")) return true;
return false;
}
private void UpdateStoreContext()
{
var path = new Uri(Navigation.Uri).AbsolutePath.ToLowerInvariant();
// Discount store routes all start with /discount-store
if (path.StartsWith("/discount-store"))
{
_storeCtx = StoreContext.Discount;
}
// Regular store routes
else if (path.StartsWith("/products") || path.StartsWith("/product/") ||
path.StartsWith("/cart") || path.StartsWith("/checkout-summary") ||
path.StartsWith("/orders") || path.StartsWith("/order/") ||
path.StartsWith("/order-tracking/") || path.StartsWith("/categories"))
{
_storeCtx = StoreContext.Regular;
}
else
{
_storeCtx = StoreContext.None;
}
}
/// <summary>
/// چک کردن بروزرسانی بدون نمایش popup
/// </summary>
@@ -131,6 +233,12 @@ public partial class MainLayout : IDisposable
InvokeAsync(StateHasChanged);
}
private void OnDiscountCartChanged()
{
_discountCartCount = DiscountCartService.Count;
InvokeAsync(StateHasChanged);
}
private async Task CheckAuthStatus()
{
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
@@ -154,6 +262,11 @@ public partial class MainLayout : IDisposable
Navigation.NavigateTo(RouteConstants.Profile.Index);
}
private void NavigateToHub()
{
Navigation.NavigateTo(RouteConstants.Profile.Hub);
}
private async Task Logout()
{
await AuthService.LogoutAsync();
@@ -165,5 +278,7 @@ public partial class MainLayout : IDisposable
public void Dispose()
{
CartService.OnChange -= OnCartChanged;
DiscountCartService.OnChange -= OnDiscountCartChanged;
Navigation.LocationChanged -= OnLocationChanged;
}
}
@@ -0,0 +1,40 @@
@inject NavigationManager Navigation
@inject IJSRuntime JS
<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 (ShowBack)
{
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" OnClick="GoBack">بازگشت</MudButton>
}
</div>
@code {
[Parameter] public string Title { get; set; } = string.Empty;
/// <summary>
/// Optional subtitle text displayed below the title.
/// </summary>
[Parameter] public string? Subtitle { get; set; }
/// <summary>
/// Fallback URL if there's no browser history. Uses history.back() first.
/// </summary>
[Parameter] public string? BackHref { get; set; }
/// <summary>
/// Whether to show the back button. Default is true.
/// </summary>
[Parameter] public bool ShowBack { get; set; } = true;
private async Task GoBack()
{
await JS.InvokeVoidAsync("history.back");
}
}
@@ -0,0 +1,96 @@
@using static FrontOffice.Main.Shared.AuthDialog
@if (CurrentStep == AuthStep.Phone)
{
<MudText Typo="Typo.body2" Class="mb-4 mud-text-secondary" Align="Align.Center">
لطفاً شماره موبایل خود را وارد کنید تا رمز پویا ارسال شود.
</MudText>
<MudForm @ref="_phoneForm" Model="PhoneRequest">
<MudTextField @bind-Value="PhoneRequest.Mobile"
Label="شماره موبایل"
Variant="Variant.Outlined"
Immediate="true"
Required="true"
Placeholder="شماره موبایل خود را وارد کنید"
RequiredError="وارد کردن شماره موبایل الزامی است."
Class="mb-3" />
<div class="captcha-row mb-3">
<MudTextField Label="کد کپچا" Placeholder="کد نمایش داده شده" Immediate="true"
Variant="Variant.Outlined" Value="CaptchaInput"
ValueChanged="@((string v) => CaptchaInputChanged.InvokeAsync(v))"
Required="true"
RequiredError="لطفاً کد کپچا را وارد کنید." />
<MudPaper Elevation="0" Class="captcha-box d-flex align-center justify-center">
<MudText Typo="Typo.h5">@CaptchaCode</MudText>
</MudPaper>
<MudIconButton Icon="@Icons.Material.Outlined.Refresh" Color="Color.Primary"
Disabled="IsBusy" OnClick="OnRefreshCaptcha" Size="Size.Medium" />
</div>
<MudCheckBox T="bool" Required="true"
Label="شرایط و قوانین را می‌پذیرم"
RequiredError="برای ادامه باید شرایط و قوانین را بپذیرید."
Color="Color.Primary"
Class="mb-2" />
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
{
<MudAlert Severity="Severity.Error" Dense="true" Elevation="0"
Class="mb-2 rounded-lg">@ErrorMessage</MudAlert>
}
</MudForm>
}
else if (CurrentStep == AuthStep.Verify)
{
<MudText Typo="Typo.body2" Class="mb-4 mud-text-secondary" Align="Align.Center">
رمز پویا شش رقمی ارسال شده به <strong>@PhoneNumber</strong> را وارد کنید.
</MudText>
<MudForm @ref="_verifyForm" Model="VerifyRequest">
<MudTextField @bind-Value="VerifyRequest.Code"
Label="رمز پویا"
Variant="Variant.Outlined"
Immediate="true"
Required="true"
RequiredError="وارد کردن رمز پویا الزامی است."
Class="mb-3"
MaxLength="6" />
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
{
<MudAlert Severity="Severity.Error" Dense="true" Elevation="0"
Class="mb-2 rounded-lg">@ErrorMessage</MudAlert>
}
@if (!string.IsNullOrWhiteSpace(InfoMessage))
{
<MudAlert Severity="Severity.Success" Dense="true" Elevation="0"
Class="mb-2 rounded-lg">@InfoMessage</MudAlert>
}
<MudDivider Class="my-3" />
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudButton Variant="Variant.Text" Color="Color.Secondary" Disabled="IsBusy"
OnClick="OnChangePhone" Size="Size.Small">
<MudIcon Icon="@Icons.Material.Outlined.Edit" Size="Size.Small" Class="me-1" /> تغییر شماره
</MudButton>
@if (ResendRemaining > 0)
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">
ارسال مجدد تا @ResendRemaining ثانیه
</MudText>
}
else
{
<MudButton Variant="Variant.Text" Color="Color.Primary" Disabled="IsBusy"
OnClick="OnResendOtp" Size="Size.Small">
ارسال مجدد
</MudButton>
}
</MudStack>
</MudForm>
}
@@ -0,0 +1,42 @@
using CMSMicroservice.Protobuf.Protos.User;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using static FrontOffice.Main.Shared.AuthDialog;
namespace FrontOffice.Main.Shared;
public partial class PhoneVerifyForm
{
// ── Step state ──
[Parameter, EditorRequired] public AuthStep CurrentStep { get; set; }
// ── Phone step ──
[Parameter, EditorRequired] public CreateNewOtpTokenRequest PhoneRequest { get; set; } = default!;
// ── Verify step ──
[Parameter, EditorRequired] public VerifyOtpTokenRequest VerifyRequest { get; set; } = default!;
// ── Captcha ──
[Parameter] public string? CaptchaCode { get; set; }
[Parameter] public string? CaptchaInput { get; set; }
[Parameter] public EventCallback<string?> CaptchaInputChanged { get; set; }
[Parameter] public EventCallback OnRefreshCaptcha { get; set; }
// ── Shared state ──
[Parameter] public bool IsBusy { get; set; }
[Parameter] public string? ErrorMessage { get; set; }
[Parameter] public string? InfoMessage { get; set; }
[Parameter] public string? PhoneNumber { get; set; }
[Parameter] public int ResendRemaining { get; set; }
// ── Verify actions ──
[Parameter] public EventCallback OnChangePhone { get; set; }
[Parameter] public EventCallback OnResendOtp { get; set; }
// ── Internal form refs (exposed via public methods) ──
private MudForm? _phoneForm;
private MudForm? _verifyForm;
public MudForm? GetPhoneForm() => _phoneForm;
public MudForm? GetVerifyForm() => _verifyForm;
}
@@ -1,4 +1,4 @@
using FrontOffice.BFF.Configuration.Protobuf.Protos.AppVersion;
using CMSMicroservice.Protobuf.Protos.AppVersion;
using Blazored.LocalStorage;
using Microsoft.JSInterop;
@@ -6,7 +6,12 @@ public class AuthDialogService
{
private readonly IDialogService _dialogService;
private readonly IDeviceDetector _deviceDetector;
private readonly DialogOptions _normalWidth = new() { MaxWidth = MaxWidth.ExtraSmall, FullWidth = true };
private readonly DialogOptions _dialogOptions = new()
{
MaxWidth = MaxWidth.ExtraSmall,
FullWidth = true,
CloseButton = true
};
public AuthDialogService(IDialogService dialogService, IDeviceDetector deviceDetector)
{
@@ -18,12 +23,7 @@ public class AuthDialogService
public async Task ShowAuthDialogAsync()
{
// Pick dialog size based on device type
var options = _deviceDetector.IsMobile()
? new DialogOptions() { FullScreen = true}
: _normalWidth;
var dialog = await _dialogService.ShowAsync<Shared.AuthDialog>("ورود به حساب کاربری", options);
var dialog = await _dialogService.ShowAsync<Shared.AuthDialog>("ورود به حساب کاربری", _dialogOptions);
var result = await dialog.Result;
if (!result.Canceled)
@@ -1,5 +1,5 @@
using Blazored.LocalStorage;
using FrontOffice.BFF.User.Protobuf.Protos.User;
using CMSMicroservice.Protobuf.Protos.User;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using System.IdentityModel.Tokens.Jwt;
@@ -12,7 +12,7 @@ public class AuthService
private readonly NavigationManager _navigation;
private readonly ISnackbar _snackbar;
private readonly UserAuthInfo _userAuthInfo;
private readonly UserContract.UserContractClient _userContract;
private readonly CMSMicroservice.Protobuf.Protos.User.UserContract.UserContractClient _userContract;
private readonly ILogger<AuthService> _logger;
private const string TokenStorageKey = "auth:token";
@@ -22,7 +22,7 @@ public class AuthService
NavigationManager navigation,
ISnackbar snackbar,
UserAuthInfo userAuthInfo,
UserContract.UserContractClient userContract,
CMSMicroservice.Protobuf.Protos.User.UserContract.UserContractClient userContract,
ILogger<AuthService> logger)
{
_localStorage = localStorage;
@@ -54,7 +54,7 @@ public class AuthService
}
public bool IsCompleteRegister()
{
InitUserAuthInfo().GetAwaiter();
InitUserAuthInfo().GetAwaiter().GetResult();
if (!string.IsNullOrWhiteSpace(_userAuthInfo.NationalCode) && !string.IsNullOrWhiteSpace(_userAuthInfo.FirstName) && !string.IsNullOrWhiteSpace(_userAuthInfo.LastName) && _userAuthInfo.IsSignMainContract)
{
return true;
@@ -0,0 +1,60 @@
namespace FrontOffice.Main.Utilities;
/// <summary>
/// Service for fetching active blog categories (customer-facing).
/// </summary>
public class BlogCategoryService
{
private readonly CMSMicroservice.Protobuf.Protos.BlogCategory.BlogCategoryContract.BlogCategoryContractClient _client;
public BlogCategoryService(CMSMicroservice.Protobuf.Protos.BlogCategory.BlogCategoryContract.BlogCategoryContractClient client)
{
_client = client;
}
/// <summary>
/// Get all active blog categories (for blog sidebar / filter).
/// </summary>
public async Task<List<BlogCategoryDto>> GetActiveCategoriesAsync()
{
try
{
var response = await _client.GetActiveBlogCategoriesAsync(
new CMSMicroservice.Protobuf.Protos.BlogCategory.GetActiveBlogCategoriesRequest());
if (response?.Categories == null || response.Categories.Count == 0)
return new();
return response.Categories
.OrderBy(c => c.SortOrder)
.Select(c => new BlogCategoryDto
{
Id = c.Id,
Title = c.Title,
Slug = c.Slug,
Description = c.Description,
IconName = c.IconName,
PostCount = c.PostCount
})
.ToList();
}
catch (Exception ex)
{
#if DEBUG
Console.WriteLine($"BlogCategoryService.GetActiveCategoriesAsync error: {ex.Message}");
#endif
return new();
}
}
}
// ── DTO ──
public class BlogCategoryDto
{
public long Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Slug { get; set; } = string.Empty;
public string? Description { get; set; }
public string? IconName { get; set; }
public int PostCount { get; set; }
}
@@ -0,0 +1,204 @@
namespace FrontOffice.Main.Utilities;
/// <summary>
/// Service for fetching published blog posts (customer-facing).
/// Used by Landing Page (latest posts) and future Blog pages.
/// </summary>
public class BlogPostService
{
private readonly CMSMicroservice.Protobuf.Protos.BlogPost.BlogPostContract.BlogPostContractClient _client;
public BlogPostService(CMSMicroservice.Protobuf.Protos.BlogPost.BlogPostContract.BlogPostContractClient client)
{
_client = client;
}
/// <summary>
/// Get featured blog posts (for landing page hero / highlights).
/// </summary>
public async Task<List<BlogPostCardDto>> GetFeaturedPostsAsync(int count = 3)
{
try
{
var response = await _client.GetFeaturedBlogPostsAsync(
new CMSMicroservice.Protobuf.Protos.BlogPost.GetFeaturedBlogPostsRequest { Count = count });
return MapToCardDtos(response?.Models);
}
catch (Exception ex)
{
#if DEBUG
Console.WriteLine($"BlogPostService.GetFeaturedPostsAsync error: {ex.Message}");
#endif
return new();
}
}
/// <summary>
/// Get latest published posts with optional category filter.
/// </summary>
public async Task<BlogPostListResult> GetPublishedPostsAsync(int page = 1, int pageSize = 9, string? search = null, long? categoryId = null)
{
try
{
var request = new CMSMicroservice.Protobuf.Protos.BlogPost.GetPublishedBlogPostsRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = page,
PageSize = pageSize
}
};
if (!string.IsNullOrWhiteSpace(search))
request.SearchTerm = search;
if (categoryId.HasValue)
request.CategoryId = categoryId.Value;
var response = await _client.GetPublishedBlogPostsAsync(request);
return new BlogPostListResult
{
Posts = MapToCardDtos(response?.Models),
TotalCount = (int)(response?.MetaData?.TotalCount ?? 0),
TotalPages = (int)(response?.MetaData?.TotalPage ?? 0),
CurrentPage = page
};
}
catch (Exception ex)
{
#if DEBUG
Console.WriteLine($"BlogPostService.GetPublishedPostsAsync error: {ex.Message}");
#endif
return new();
}
}
/// <summary>
/// Get a single blog post by slug (for blog detail page).
/// </summary>
public async Task<BlogPostDetailDto?> GetBySlugAsync(string slug)
{
try
{
var response = await _client.GetBlogPostBySlugAsync(
new CMSMicroservice.Protobuf.Protos.BlogPost.GetBlogPostBySlugRequest { Slug = slug });
if (response == null || response.Id <= 0) return null;
return new BlogPostDetailDto
{
Id = response.Id,
Title = response.Title,
Slug = response.Slug,
Summary = response.Summary,
HtmlContent = response.HtmlContent,
FeaturedImagePath = response.FeaturedImagePath,
FeaturedImageThumbnailPath = response.FeaturedImageThumbnailPath,
PublishedAt = response.PublishedAt?.ToDateTime(),
ViewCount = response.ViewCount,
IsFeatured = response.IsFeatured,
Categories = response.Categories?.Select(c => new BlogCategoryInfo { Id = c.Id, Title = c.Title, Slug = c.Slug }).ToList() ?? new(),
Tags = response.Tags?.Select(t => new BlogTagInfo { Id = t.Id, Title = t.Title, Name = t.Name }).ToList() ?? new()
};
}
catch (Exception ex)
{
#if DEBUG
Console.WriteLine($"BlogPostService.GetBySlugAsync error: {ex.Message}");
#endif
return null;
}
}
/// <summary>
/// Increment view count for a blog post.
/// </summary>
public async Task IncrementViewCountAsync(long postId)
{
try
{
await _client.IncrementViewCountAsync(
new CMSMicroservice.Protobuf.Protos.BlogPost.IncrementViewCountRequest { Id = postId });
}
catch
{
// Fire and forget — don't fail the page
}
}
// ── Helpers ──
private static List<BlogPostCardDto> MapToCardDtos(
Google.Protobuf.Collections.RepeatedField<CMSMicroservice.Protobuf.Protos.BlogPost.BlogPostListItem>? models)
{
if (models == null || models.Count == 0) return new();
return models.Select(m => new BlogPostCardDto
{
Id = m.Id,
Title = m.Title,
Slug = m.Slug,
Summary = m.Summary,
ThumbnailUrl = m.FeaturedImageThumbnailPath,
PublishedAt = m.PublishedAt?.ToDateTime(),
ViewCount = m.ViewCount,
IsFeatured = m.IsFeatured,
Categories = m.Categories?.Select(c => new BlogCategoryInfo { Id = c.Id, Title = c.Title, Slug = c.Slug }).ToList() ?? new()
}).ToList();
}
}
// ── DTOs ──
public class BlogPostCardDto
{
public long Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Slug { get; set; } = string.Empty;
public string? Summary { get; set; }
public string? ThumbnailUrl { get; set; }
public DateTime? PublishedAt { get; set; }
public int ViewCount { get; set; }
public bool IsFeatured { get; set; }
public List<BlogCategoryInfo> Categories { get; set; } = new();
}
public class BlogPostDetailDto
{
public long Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Slug { get; set; } = string.Empty;
public string? Summary { get; set; }
public string HtmlContent { get; set; } = string.Empty;
public string? FeaturedImagePath { get; set; }
public string? FeaturedImageThumbnailPath { get; set; }
public DateTime? PublishedAt { get; set; }
public int ViewCount { get; set; }
public bool IsFeatured { get; set; }
public List<BlogCategoryInfo> Categories { get; set; } = new();
public List<BlogTagInfo> Tags { get; set; } = new();
}
public class BlogPostListResult
{
public List<BlogPostCardDto> Posts { get; set; } = new();
public int TotalCount { get; set; }
public int TotalPages { get; set; }
public int CurrentPage { get; set; }
}
public class BlogCategoryInfo
{
public long Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Slug { get; set; } = string.Empty;
}
public class BlogTagInfo
{
public long Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
}

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