Compare commits

..

1 Commits

Author SHA1 Message Date
masoodafar-web 98a68074bc chore: Update production workflow to use Nexus registry (no proxy needed)
Build and Deploy to Production / build-and-deploy (push) Failing after 0s
- Changed Dockerfile base images to use 194.5.195.53:32082 (Nexus)
- Updated workflow to use docker-sshpass image from Nexus
- Removed HTTP_PROXY/HTTPS_PROXY dependencies
- Changed deploy method to SSH instead of kubeconfig
- Registry: 194.5.195.53:30080 (Gitea internal)
- K8S Server: 45.149.79.127 (Production)
2026-01-29 22:04:31 +03:30
175 changed files with 2425 additions and 12736 deletions
-2
View File
@@ -1,2 +0,0 @@
# Keep production config from current branch during merges — never overwrite
src/FrontOffice.Main/appsettings.Production.json merge=ours
+31 -45
View File
@@ -1,4 +1,4 @@
name: Build and Deploy to Kubernetes
name: Build and Deploy
on:
push:
@@ -6,77 +6,63 @@ on:
- kub-stage
env:
REGISTRY: 194.5.195.53:30080
REGISTRY: gitea-svc:3000
IMAGE_NAME: admin/frontoffice
K8S_SERVER: 194.5.195.53
jobs:
build-and-deploy:
build:
runs-on: ubuntu-latest
container:
image: 194.5.195.53:32082/docker-sshpass:latest
image: docker:latest
options: --privileged
steps:
- name: Start Docker daemon
- name: Start Docker daemon with insecure registry
run: |
mkdir -p /etc/docker
cat > /etc/docker/daemon.json << 'DAEMON'
{
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
"insecure-registries": ["194.5.195.53:32500", "194.5.195.53:32082", "git.kbs1.ir", "gitea-svc:3000"],
"dns": ["0.0.0.0"]
}
DAEMON
echo "🚀 Starting Docker daemon..."
dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs &
# Wait up to 3 minutes for Docker to be ready
for i in $(seq 1 90); do
if docker info >/dev/null 2>&1; then
echo "✅ Docker daemon is ready (attempt $i)"
docker version
break
else
echo "⏳ Waiting for Docker daemon... (attempt $i/90)"
sleep 2
fi
# بدون اینترنت - فقط از cached images
dockerd &
for i in $(seq 1 30); do
docker info >/dev/null 2>&1 && break || sleep 2
done
docker info
# Final check
if ! docker info >/dev/null 2>&1; then
echo "❌ Docker daemon failed to start after 3 minutes"
exit 1
fi
# بررسی وجود cached images
echo "📊 Checking cached base images..."
docker images | grep -E "mcr.microsoft.com|nginx" || echo "⚠️ WARNING: Base images not cached!"
- name: Checkout code
run: |
git clone --depth 1 --branch kub-stage http://gitea-svc:3000/admin/FrontOffice.git .
- name: Login to Docker registries
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 194.5.195.53:32082 -u admin --password-stdin
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
git log -1 --format="%H %s"
- name: Build Docker Image
run: |
cd src
DOCKER_BUILDKIT=0 docker build --network host -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest -f FrontOffice.Main/Dockerfile .
# استفاده از 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 \
.
- name: Push to Registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- name: Deploy to Kubernetes
- name: Build and Push Complete
run: |
export SSHPASS="${{ secrets.SERVER_PASSWORD }}"
# Copy K8s manifests to server
sshpass -e scp -o StrictHostKeyChecking=no k8s/staging/frontoffice-deployment.yaml root@${{ env.K8S_SERVER }}:/tmp/frontoffice-deployment.yaml
# Apply manifests and restart
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
kubectl apply -f /tmp/frontoffice-deployment.yaml &&
crictl rmi ${REGISTRY}/${IMAGE_NAME}:latest 2>/dev/null || true &&
kubectl rollout restart deployment/frontoffice &&
kubectl rollout status deployment/frontoffice --timeout=180s &&
rm -f /tmp/frontoffice-deployment.yaml
"
echo "✅ Deployed!"
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 }}"
+10 -24
View File
@@ -8,7 +8,7 @@ on:
env:
REGISTRY: 194.5.195.53:30080
IMAGE_NAME: admin/frontoffice
K8S_SERVER: 194.5.195.53
K8S_SERVER: 45.149.79.127
jobs:
build-and-deploy:
@@ -22,11 +22,11 @@ jobs:
mkdir -p /etc/docker
cat > /etc/docker/daemon.json << 'DAEMON'
{
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32082"]
}
DAEMON
echo "🚀 Starting Docker daemon..."
dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs &
dockerd &
for i in $(seq 1 90); do
if docker info >/dev/null 2>&1; then
@@ -40,7 +40,7 @@ jobs:
done
if ! docker info >/dev/null 2>&1; then
echo "❌ Docker daemon failed to start after 3 minutes"
echo "❌ Docker daemon failed to start"
exit 1
fi
@@ -48,35 +48,21 @@ jobs:
run: |
git clone --depth 1 --branch production http://gitea-svc:3000/admin/FrontOffice.git .
- name: Login to Docker registries
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 194.5.195.53:32082 -u admin --password-stdin
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
- name: Build Docker Image
run: |
cd src
DOCKER_BUILDKIT=0 docker build --network host -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
docker build -f FrontOffice.Main/Dockerfile \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod \
-f FrontOffice.Main/Dockerfile .
.
- name: Push to Registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod
- name: Deploy to Production
run: |
export SSHPASS="${{ secrets.SERVER_PASSWORD }}"
# Copy K8s manifests to server
sshpass -e scp -o StrictHostKeyChecking=no k8s/production/frontoffice-deployment.yaml root@${{ env.K8S_SERVER }}:/tmp/frontoffice-deployment.yaml
# Apply manifests and restart
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
kubectl apply -f /tmp/frontoffice-deployment.yaml &&
kubectl rollout restart deployment/frontoffice &&
kubectl rollout status deployment/frontoffice --timeout=300s &&
rm -f /tmp/frontoffice-deployment.yaml
"
echo "✅ Deployed to Production!"
sshpass -p "${{ secrets.K8S_SSH_PASSWORD }}" ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} \
"kubectl rollout restart deployment/frontoffice && kubectl rollout status deployment/frontoffice --timeout=5m" || echo "Deployment pending"
+1 -1
View File
@@ -14,7 +14,7 @@
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
# Mono auto generated files
mono_crash.*
# Build results
+1
View File
@@ -0,0 +1 @@
# Test multi-remote push Sun Dec 7 19:09:00 UTC 2025
-4
View File
@@ -1,4 +0,0 @@
# Docs moved to totalDoc
See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation.
-262
View File
@@ -1,262 +0,0 @@
# سرویس‌های FrontOffice (سمت مشتری)
این سند تمام سرویس‌هایی را که **FrontOffice** (فرانت مشتری) به **CMS** از طریق gRPC Gateway (`GW_URL`) فراخوانی می‌کند، به ترتیب **Use Case** فهرست می‌کند.
- **مسیر کد سرویس‌ها:** `FrontOffice/src/FrontOffice.Main/Utilities/`
- **ثبت gRPC:** `FrontOffice/src/FrontOffice.Main/ConfigureServices.cs`
- **تاریخ:** ۱۴۰۵/۰۳/۱۷
---
## ۱. احراز هویت و ثبت‌نام
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 1.1 | `AuthService``RefreshTokenAsync` | `UserContract.RefreshToken` | `/profile` (`Profile/Index`) | تمدید JWT پس از بازگشت از درگاه پرداخت |
| 1.2 | `AuthService``InitUserAuthInfo` / claims | — (local JWT parse) | سراسری | خواندن UserId، نام، کدملی، وضعیت قرارداد/پکیج/باشگاه از توکن |
| 1.3 | `UserContract.CreateNewOtpToken` | `UserContract` | `Shared/AuthDialog` | ارسال OTP ورود/ثبت‌نام با موبایل |
| 1.4 | `UserContract.VerifyOtpToken` | `UserContract` | `Shared/AuthDialog` | تأیید OTP و دریافت JWT |
| 1.5 | `UserContract.CreateNewOtpToken` (Purpose=`signContract`) | `UserContract` | `/register` (`RegisterWizard`) | ارسال OTP برای امضای قرارداد اصلی |
| 1.6 | `UserContract.AcceptContract` | `UserContract` | `/register` (`RegisterWizard`) | ثبت امضای قرارداد اصلی + دریافت توکن به‌روز |
| 1.7 | `UserContract.GetUserForCustomer` | `UserContract` | `/register`, `/profile`, `/profile/personal`, `/profile/settings` | بارگذاری پروفایل موجود کاربر |
| 1.8 | `UserContract.UpdateCustomerProfile` | `UserContract` | `/register`, `/profile`, `/profile/personal` | ذخیره نام، نام‌خانوادگی، کدملی و… |
| 1.9 | `AuthDialogService.ShowAuthDialogAsync` | — (UI) | فروشگاه، سبد، checkout | نمایش دیالوگ ورود برای مهمان |
| 1.10 | `GuestActionGate` | — (wrapper روی Auth) | `/`, `/products`, `/product/{id}`, Discount Store | جلوگیری از افزودن به سبد بدون لاگین |
---
## ۲. پروفایل و آدرس
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 2.1 | `UserContract.GetUserForCustomer` | `UserContract` | `/profile`, `/profile/personal`, `/profile/settings` | نمایش و ویرایش اطلاعات شخصی |
| 2.2 | `UserContract.GetUser` | `UserContract` | `/profile/hub`, `Club/ActivationSection` | اطلاعات کامل کاربر (هاب پروفایل / پس از فعال‌سازی باشگاه) |
| 2.3 | `UserContract.UpdateUser` | `UserContract` | `/profile/settings` | به‌روزرسانی تنظیمات حساب |
| 2.4 | `UserAddressContract.GetCustomerAddresses` | `UserAddressContract` | `/profile`, `/profile/addresses`, `/checkout`, Store/Discount checkout | لیست آدرس‌های تحویل |
| 2.5 | `UserAddressContract.CreateCustomerAddress` | `UserAddressContract` | `Profile/Components/AddAddressDialog` | ثبت آدرس جدید |
| 2.6 | `UserAddressContract.UpdateCustomerAddress` | `UserAddressContract` | `Profile/Components/EditAddressDialog` | ویرایش آدرس |
| 2.7 | `UserAddressContract.DeleteCustomerAddress` / `DeleteUserAddress` | `UserAddressContract` | `/profile/addresses`, `/profile` | حذف آدرس |
| 2.8 | `UserAddressContract.SetCustomerDefaultAddress` / `SetAddressAsDefault` | `UserAddressContract` | `/profile/addresses`, `/profile`, `/checkout` | تنظیم آدرس پیش‌فرض |
| 2.9 | `CityContract.GetAllCitiesByFilter` | `CityContract` | `AddAddressDialog`, `EditAddressDialog` | جستجو و انتخاب شهر/استان |
---
## ۳. پکیج و خرید عضویت
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 3.1 | `PackageService.GetAllPackagesAsync` | `PackageContract.GetCustomerPackages` | `/packages`, `/my-packages`, `/`, `/checkout`, `PackagePurchaseDialog`, `ClubMembershipContractDialog`, Commission pages | لیست پکیج‌های قابل خرید |
| 3.2 | `PackageService.GetPackageByIdAsync` | `PackageContract.GetCustomerPackageDetails` | (از طریق wrapper؛ صفحه جزئیات مستقیم RPC دارد) | جزئیات یک پکیج |
| 3.3 | `PackageContract.GetCustomerPackageDetails` | `PackageContract` | `/package/{id}` (`PackageDetail`) | جزئیات، ویژگی‌ها و مشخصات پکیج |
| 3.4 | `PackageService.GetUserPackageStatusAsync` | `PackageContract.GetUserPackageStatus` | `/packages`, `/my-packages`, `/profile` | وضعیت خرید پکیج، باشگاه، موجودی کیف پول |
| 3.5 | `PackageContract.CustomerPurchasePackage` | `PackageContract` | `/profile` (`ProcessDirectPayment`), `/checkout` | شروع پرداخت درگاهی (Zarinpal) برای خرید پکیج |
| 3.6 | `PackageContract.CustomerVerifyPackagePurchase` | `PackageContract` | `/profile/payment-callback?type=package` | تأیید پرداخت پکیج پس از بازگشت از درگاه |
| 3.7 | `PackagePurchaseDialog` | — (UI + `PackageService`) | `/profile`, `/my-packages` | انتخاب پکیج و روش پرداخت (درگاه / Daya) |
| 3.8 | Daya Loan (external URL) | — | `/profile` | هدایت به `dayadiamond.ir` برای خرید اقساطی (بدون RPC) |
---
## ۴. کیف پول، شارژ و برداشت
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 4.1 | `WalletService.GetBalancesAsync` | `UserWalletContract.GetCustomerWallet` | `/profile`, `/profile/wallet`, Store checkout, PaymentCallback | موجودی اصلی، تخفیفی و شبکه |
| 4.2 | `WalletService.GetTransactionsAsync` | `UserWalletContract.GetCustomerWalletHistory` | `/profile/wallet` | تاریخچه تراکنش‌های کیف پول |
| 4.3 | `WalletService.GetMagicWalletStatusAsync` | `UserWalletContract.GetMagicWalletStatus` | `/profile`, `/profile/magic-wallet`, `/my-packages` | وضعیت کیف پول جادویی (Magic) |
| 4.4 | `WalletService.InitiateMagicChargeAsync` | `UserWalletContract.InitiateMagicCharge` | `/profile/magic-wallet` | شروع شارژ کیف پول جادویی → درگاه |
| 4.5 | `WalletService.VerifyMagicChargeAsync` | `UserWalletContract.VerifyMagicCharge` | `/profile/payment-callback?type=magic-wallet` | تأیید شارژ Magic پس از درگاه |
| 4.6 | `WalletService.InitiateDiscountChargeAsync` | `UserWalletContract.InitiateDiscountCharge` | `/profile/charge-discount-wallet` | شروع شارژ کیف پول تخفیفی → درگاه |
| 4.7 | `WalletService.VerifyDiscountChargeAsync` | `UserWalletContract.VerifyDiscountCharge` | `/profile/payment-callback?type=discount-wallet` | تأیید شارژ کیف تخفیفی |
| 4.8 | `WalletService.InitiateCreditChargeAsync` | `UserWalletContract.InitiateCreditCharge` | `/profile/charge-credit-wallet` | شروع شارژ کیف پول اصلی → درگاه |
| 4.9 | `WalletService.VerifyCreditChargeAsync` | `UserWalletContract.VerifyCreditCharge` | `/profile/payment-callback?type=credit-wallet` | تأیید شارژ کیف اصلی |
| 4.10 | `WalletService.GetWithdrawalsAsync` | `UserWalletContract.GetCustomerWithdrawals` | `/profile/withdrawal-requests` | لیست درخواست‌های برداشت |
| 4.11 | `WalletService.GetWithdrawalSettingsAsync` | `UserWalletContract.GetCustomerWithdrawalSettings` | `/profile/withdrawal-requests` | حداقل مبلغ برداشت |
| 4.12 | `WalletService.RequestWithdrawalAsync` | `UserWalletContract.CustomerWithdrawBalance` | `/profile/withdrawal-requests` | ثبت درخواست برداشت پاداش (با payoutId کمیسیون) |
| 4.13 | `CommissionService.GetWithdrawablePayoutsAsync` | `CommissionContract.GetMyCommissionPayouts` | `/profile/withdrawal-requests` | پاداش‌های قابل برداشت برای انتخاب |
---
## ۵. Callback پرداخت (مشترک)
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 5.1 | `PackageContract.CustomerVerifyPackagePurchase` | `PackageContract` | `/profile/payment-callback` (type=package) | تأیید خرید پکیج |
| 5.2 | `WalletService.VerifyMagicChargeAsync` | `UserWalletContract` | `/profile/payment-callback` (type=magic-wallet) | تأیید شارژ Magic |
| 5.3 | `WalletService.VerifyDiscountChargeAsync` | `UserWalletContract` | `/profile/payment-callback` (type=discount-wallet) | تأیید شارژ تخفیفی |
| 5.4 | `WalletService.VerifyCreditChargeAsync` | `UserWalletContract` | `/profile/payment-callback` (type=credit-wallet) | تأیید شارژ کیف اصلی |
| 5.5 | `DiscountOrderService.VerifyDiscountOrderPaymentAsync` | `DiscountOrderContract.CustomerVerifyDiscountOrderPayment` | `/profile/payment-callback` (type=discount-order) | تأیید پرداخت سفارش فروشگاه تخفیفی |
| 5.6 | `UserContract.GetUserForCustomer` + `AuthService.InitUserAuthInfo` | `UserContract` | `/profile/payment-callback` | به‌روزرسانی claims کاربر پس از پرداخت موفق |
---
## ۶. فروشگاه اصلی (Store)
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 6.1 | `ProductService.GetProductsPagedAsync` | `ProductsContract.GetProductsForCustomer` | `/products` | لیست محصولات با فیلتر/صفحه‌بندی |
| 6.2 | `ProductService.GetTopSellingAsync` | `ProductsContract` | `/` (صفحه اصلی) | پرفروش‌ترین محصولات |
| 6.3 | `ProductService.GetByIdAsync` | `ProductsContract.GetProductByIdForCustomer` | `/product/{id}` | جزئیات محصول + گالری |
| 6.4 | `CategoryService.GetAllAsync` / `GetByIdAsync` | `CategoryContract.GetAllCategoriesForCustomer` | `/categories`, `/products` | درخت دسته‌بندی محصولات |
| 6.5 | `CartService` — Add/Update/Remove/Get | `UserCartsContract.*ForCustomer` | `/`, `/products`, `/product/{id}`, `/cart` | مدیریت سبد خرید (AddNew, Update, Remove, GetCustomerCart) |
| 6.6 | `UserOrderContract.SubmitShopBuyOrder` | `UserOrderContract` | `/checkout-summary` | ثبت سفارش و پرداخت از کیف پول |
| 6.7 | `OrderService.GetOrdersAsync` | `UserOrderContract.GetCustomerOrders` | `/orders` | لیست سفارش‌های کاربر |
| 6.8 | `OrderService.GetOrderAsync` | `UserOrderContract.GetCustomerOrder` | `/order/{id}`, `/order-tracking/{id}` | جزئیات یک سفارش |
| 6.10 | `VATService.GetRateAsync` | `UserOrderContract` (via scope) | Store/Discount صفحات قیمت | نرخ مالیات بر ارزش افزوده (کش روزانه) |
| 6.11 | `WalletService.GetBalancesAsync` | `UserWalletContract` | `/checkout-summary` | بررسی موجودی برای پرداخت سفارش |
---
## ۷. فروشگاه تخفیفی (Discount Store)
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 7.1 | `DiscountProductService.GetProductsAsync` | `DiscountProductContract.GetDiscountProducts` | `/discount-store` | لیست محصولات تخفیفی |
| 7.2 | `DiscountProductService.GetTopSellingAsync` | `DiscountProductContract` | `/` | پرفروش‌های فروشگاه تخفیفی |
| 7.3 | `DiscountProductService.GetByIdAsync` | `DiscountProductContract.GetDiscountProductById` + Images | `/discount-store/product/{id}` | جزئیات محصول تخفیفی |
| 7.4 | `DiscountProductService.GetCategoriesAsync` | `DiscountCategoryContract.GetDiscountCategories` | `/discount-store` | دسته‌بندی‌های فروشگاه تخفیفی |
| 7.5 | `DiscountCartService` — Add/Update/Remove/Get | `DiscountShoppingCartContract.*` | `/discount-store`, `/discount-store/cart` | سبد خرید تخفیفی |
| 7.6 | `DiscountOrderService.PlaceOrderAsync` | `DiscountOrderContract.PlaceOrder` | `/discount-store/checkout` | ثبت سفارش (کیف تخفیفی + درگاه) |
| 7.7 | `DiscountOrderService.GetUserOrdersAsync` | `DiscountOrderContract.GetUserOrders` | `/discount-store/orders` | لیست سفارش‌های تخفیفی |
| 7.8 | `DiscountOrderService.GetOrderByIdAsync` | `DiscountOrderContract.GetOrderById` | `/discount-store/order/{id}` | جزئیات سفارش تخفیفی |
| 7.9 | `UserAddressContract.GetCustomerAddresses` | `UserAddressContract` | `/discount-store/checkout` | انتخاب آدرس تحویل |
---
## ۸. باشگاه مشتریان (Club)
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 8.1 | `ClubMembershipService.GetMyMembershipAsync` | `ClubMembershipContract.GetClubMembership` | `/club/membership` | وضعیت عضویت باشگاه |
| 8.2 | `ClubConfigurationService.GetClubConfigurationAsync` | `ConfigurationContract.GetClubConfiguration` | `/club/membership` | هزینه فعال‌سازی و هدیه عضویت |
| 8.3 | `ClubConfigurationService.GetClubFeaturesAsync` | `ConfigurationContract.GetClubFeatures` | `/club/features` | لیست امکانات باشگاه |
| 8.4 | `ClubMembershipService.ActivateMembershipAsync` | `ClubMembershipContract.ActivateClubMembership` | `Club/ActivationSection` | فعال‌سازی/تمدید عضویت باشگاه |
| 8.5 | `OtpTokenContract.CreateNewOtpToken` | `OtpTokenContract` | `ClubMembershipContractDialog` | OTP برای امضای قرارداد باشگاه |
| 8.6 | `ClubMembershipContract.AcceptClubMembershipContract` | `ClubMembershipContract` | `ClubMembershipContractDialog` | ثبت امضای قرارداد باشگاه |
---
## ۹. شبکه فروش (Network / MLM)
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 9.1 | `NetworkMembershipService.GetMyNetworkTreeAsync` | `NetworkMembershipContract.GetMyNetworkTree` | `/profile/tree` (`OrganizationChart`) | درخت سازمان فروش کاربر |
| 9.2 | `NetworkMembershipService.GetSubordinateTreeAsync` | `NetworkMembershipContract.GetSubordinateTree` | `/profile/tree` | drill-down به زیرمجموعه |
| 9.3 | `NetworkMembershipService.GetMyNetworkStatisticsAsync` | `NetworkMembershipContract.GetMyNetworkStatistics` | `/profile/tree`, `/network/statistics` | آمار پا چپ/راست، تعداد اعضا |
---
## ۱۰. کمیسیون و پاداش
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 10.1 | `CommissionService.GetWeekDefinitionsAsync` | `CommissionContract.GetWeekDefinitions` | `Shared/WeekSelector`, Commission pages | لیست هفته‌های محاسبه پاداش |
| 10.2 | `CommissionService.GetMyCommissionPayoutsAsync` | `CommissionContract.GetMyCommissionPayouts` | `/commission/dashboard` | تاریخچه پرداخت پاداش‌ها |
| 10.3 | `CommissionService.GetMyWeeklyBalanceAsync` | `CommissionContract.GetMyWeeklyBalances` | `/commission/weekly-balance` | جزئیات بالانس هفتگی (چپ/راست) |
| 10.4 | `PackageService.GetAllPackagesAsync` | `PackageContract` | Commission pages | فیلتر پاداش بر اساس پکیج |
---
## ۱۱. محتوای سایت، بلاگ و صفحات ثابت
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 11.1 | `SitePageSettingsService.GetPageAsync` | `SitePageSettingsContract.GetPageSettings` | `/` (landing), `/about`, `/contact`, `/licenses` | محتوای داینامیک صفحات (hero، تصاویر، JSON تنظیمات) |
| 11.2 | `BlogPostService.GetFeaturedPostsAsync` | `BlogPostContract.GetFeaturedBlogPosts` | `/`, `/profile` | پست‌های ویژه |
| 11.3 | `BlogPostService.GetPublishedPostsAsync` | `BlogPostContract.GetPublishedBlogPosts` | `/`, `/blog` | لیست پست‌های منتشرشده |
| 11.4 | `BlogPostService.GetBySlugAsync` | `BlogPostContract.GetBlogPostBySlug` | `/blog/{slug}` | محتوای کامل یک پست |
| 11.5 | `BlogPostService.IncrementViewCountAsync` | `BlogPostContract.IncrementViewCount` | `/blog/{slug}` | افزایش شمارنده بازدید |
| 11.6 | `BlogCategoryService.GetActiveCategoriesAsync` | `BlogCategoryContract` | `/blog` | فیلتر دسته‌بندی بلاگ |
| 11.7 | FAQ | — (hardcoded) | `/faq` | سوالات متداول — **بدون فراخوانی API** |
---
## ۱۲. زیرساخت مشترک (Infrastructure)
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 12.1 | `ImageCacheService.ResolveAsync` | `ImageResolverContract` | `Shared/AppImage` (سراسری) | resolve مسیر تصویر CMS به data-URI |
| 12.2 | `AppVersionService.CheckVersionAsync` | `AppVersionContract` | `App.razor`, `MainLayout` | بررسی نسخه جدید اپ |
| 12.3 | `AppVersionService.ApplyUpdateAsync` / `SkipVersionAsync` | — (localStorage + reload) | `App.razor`, `MainLayout` | اعمال یا رد آپدیت |
| 12.4 | `MainService.OnChangeHandler` | — (in-memory) | `/`, `AuthDialog` | اطلاع‌رسانی تغییر state سراسری (مثلاً پس از login) |
| 12.5 | `IChromiumPdfService` | — (local endpoint `/contract/generate`) | `/register` | تولید PDF قرارداد (Chromium headless) |
---
## ۱۳. Gateway / انتخاب‌گر
| صفحه | سرویس | کاربرد |
|------|--------|--------|
| `/stores` | — | انتخاب بین فروشگاه اصلی و تخفیفی |
| `/my-orders` | — | انتخاب لیست سفارش‌ها |
| `/my-cart` | — | انتخاب سبد خرید |
این صفحات فقط مسیریابی UI هستند و مستقیماً gRPC صدا نمی‌زنند.
---
## ۱۴. سرویس‌های ثبت‌شده ولی بدون استفاده در UI
| سرویس / Contract | وضعیت |
|------------------|--------|
| `SitePageService` (`SitePageContract`) | ثبت در DI؛ **هیچ صفحه‌ای inject نمی‌کند** (جایگزین: `SitePageSettingsService`) |
| `TransactionsContract` | ثبت در DI؛ **استفاده نشده** |
| `UserWalletHistoryContract` | ثبت در DI؛ **استفاده نشده** (تاریخچه از `UserWalletContract.GetCustomerWalletHistory` می‌آید) |
| `TokenNotificationService` | ثبت در DI؛ **هنوز به Layout وصل نشده** (SignalR برای invalidation توکن) |
---
## نمودار جریان پرداخت
```mermaid
flowchart LR
subgraph initiate [شروع پرداخت]
A1[Profile / Checkout]
A2[MagicWallet / ChargeDiscount]
A3[Discount Checkout]
end
subgraph gateway [Zarinpal]
G[درگاه پرداخت]
end
subgraph callback [PaymentCallback]
C1[Verify Package]
C2[Verify Magic]
C3[Verify Discount Wallet]
C4[Verify Discount Order]
end
A1 --> G
A2 --> G
A3 --> G
G --> callback
```
---
## فهرست gRPC Contractهای فعال
| Contract | Wrapper اصلی |
|----------|----------------|
| `UserContract` | `AuthService`, صفحات Profile/Register |
| `UserAddressContract` | Profile, Checkout pages |
| `CityContract` | Address dialogs |
| `PackageContract` | `PackageService`, PaymentCallback, Checkout |
| `UserWalletContract` | `WalletService` |
| `UserOrderContract` | `OrderService`, CheckoutSummary |
| `UserCartsContract` | `CartService` |
| `ProductsContract` | `ProductService` |
| `CategoryContract` | `CategoryService` |
| `DiscountProductContract` / `DiscountCategoryContract` | `DiscountProductService` |
| `DiscountShoppingCartContract` | `DiscountCartService` |
| `DiscountOrderContract` | `DiscountOrderService` |
| `ClubMembershipContract` | `ClubMembershipService`, Dialog |
| `OtpTokenContract` | Club contract dialog |
| `ConfigurationContract` | `ClubConfigurationService` |
| `NetworkMembershipContract` | `NetworkMembershipService` |
| `CommissionContract` | `CommissionService` |
| `BlogPostContract` | `BlogPostService` |
| `BlogCategoryContract` | `BlogCategoryService` |
| `SitePageSettingsContract` | `SitePageSettingsService` |
| `AppVersionContract` | `AppVersionService` |
| `ImageResolverContract` | `ImageCacheService` |
-3
View File
@@ -1,3 +0,0 @@
# Docs moved to totalDoc
See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation.
@@ -1,56 +0,0 @@
---
# FrontOffice (Blazor Server) production.
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontoffice
namespace: default
labels:
app: frontoffice
environment: production
spec:
replicas: 1
selector:
matchLabels:
app: frontoffice
template:
metadata:
labels:
app: frontoffice
spec:
containers:
- name: frontoffice
image: 194.5.195.53:30080/admin/frontoffice:prod
imagePullPolicy: Always
ports:
- containerPort: 80
name: http
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Production"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
imagePullSecrets:
- name: gitea-registry-secret
---
apiVersion: v1
kind: Service
metadata:
name: frontoffice-svc
namespace: default
labels:
app: frontoffice
spec:
selector:
app: frontoffice
ports:
- port: 80
targetPort: 80
name: http
type: ClusterIP
-56
View File
@@ -1,56 +0,0 @@
---
# FrontOffice (Blazor Server) staging.
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontoffice
namespace: default
labels:
app: frontoffice
environment: staging
spec:
replicas: 1
selector:
matchLabels:
app: frontoffice
template:
metadata:
labels:
app: frontoffice
spec:
containers:
- name: frontoffice
image: 194.5.195.53:30080/admin/frontoffice:latest
imagePullPolicy: Always
ports:
- containerPort: 80
name: http
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Staging"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
imagePullSecrets:
- name: gitea-registry-secret
---
apiVersion: v1
kind: Service
metadata:
name: frontoffice-svc
namespace: default
labels:
app: frontoffice
spec:
selector:
app: frontoffice
ports:
- port: 80
targetPort: 80
name: http
type: ClusterIP
+46 -114
View File
@@ -5,32 +5,23 @@ using Grpc.Net.Client;
using MudBlazor.Services;
using System.Text.Json;
using System.Text.Json.Serialization;
// 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.UserWalletHistory;
using CMSMicroservice.Protobuf.Protos.UserAddress;
using CMSMicroservice.Protobuf.Protos.Configuration;
using CMSMicroservice.Protobuf.Protos.NetworkMembership;
using CMSMicroservice.Protobuf.Protos.Commission;
using CMSMicroservice.Protobuf.Protos.AppVersion;
using CMSMicroservice.Protobuf.Protos.SitePage;
using CMSMicroservice.Protobuf.Protos.SitePageSettings;
using CMSMicroservice.Protobuf.Protos.BlogPost;
using CMSMicroservice.Protobuf.Protos.BlogCategory;
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
using CMSMicroservice.Protobuf.Protos.DiscountCategory;
using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
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;
using FrontOffice.Main.Utilities;
using FrontOffice.Main.Utilities.Seo;
namespace Microsoft.Extensions.DependencyInjection;
@@ -57,7 +48,6 @@ public static class ConfigureServices
services.AddSingleton<UserAuthInfo>();
services.AddScoped<AuthService>();
services.AddScoped<AuthDialogService>();
services.AddScoped<GuestActionGate>();
// Storefront services
services.AddScoped<CartService>();
services.AddScoped<ProductService>();
@@ -74,20 +64,6 @@ 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>();
// Site Page Settings Service (simplified page management)
services.AddScoped<SitePageSettingsService>();
// Device detection: very light, dependency-free
services.AddTransient<IDeviceDetector, DeviceDetector>();
// PDF generation (Chromium only)
@@ -96,98 +72,58 @@ public static class ConfigureServices
// SignalR Token Notification Service
services.AddScoped<TokenNotificationService>();
// SEO
services.AddScoped<SeoMetadataProvider>();
services.AddScoped<SitemapGenerator>();
return services;
}
public static IServiceCollection AddGrpcServices(this IServiceCollection services, IConfiguration configuration)
{
var baseUrl = ResolveGatewayUrl(configuration)
?? throw new InvalidOperationException("Gateway URL is missing. Set GW_URL or GwUrl.");
var baseUrl = configuration["GwUrl"];
var isHttp = baseUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase);
// When the base URL is plain HTTP (e.g. in-cluster http://cms-svc:8080), we must force HTTP/1.1
// so that GrpcChannel does not upgrade to h2c and trigger HTTP_1_1_REQUIRED from Kestrel.
// For HTTPS the default negotiation is fine.
// Register optimized HttpClient for gRPC
services.AddScoped(sp =>
{
HttpMessageHandler inner = isHttp
? new SocketsHttpHandler
{
MaxConnectionsPerServer = 10,
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate
}
: new HttpClientHandler
{
MaxConnectionsPerServer = 10,
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate
};
var handler = new HttpClientHandler
{
MaxConnectionsPerServer = 10,
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate
};
return new HttpClient(new GrpcWebHandler(GrpcWebMode.GrpcWeb, inner))
return new HttpClient(new GrpcWebHandler(GrpcWebMode.GrpcWeb, handler))
{
Timeout = TimeSpan.FromMinutes(10),
BaseAddress = new Uri(baseUrl),
DefaultRequestVersion = isHttp ? System.Net.HttpVersion.Version11 : System.Net.HttpVersion.Version20,
DefaultVersionPolicy = isHttp
? System.Net.Http.HttpVersionPolicy.RequestVersionExact
: System.Net.Http.HttpVersionPolicy.RequestVersionOrHigher
BaseAddress = new Uri(baseUrl)
};
});
// 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.UserWalletHistory.UserWalletHistoryContract.UserWalletHistoryContractClient>);
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<SitePageSettingsContract.SitePageSettingsContractClient>);
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>);
// 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>);
return services;
}
private static string? ResolveGatewayUrl(IConfiguration configuration)
{
var envUrl = configuration["GW_URL"];
if (!string.IsNullOrWhiteSpace(envUrl))
return envUrl.TrimEnd('/');
return configuration["GwUrl"]?.TrimEnd('/');
}
private static TClient CreateAuthenticatedClient<TClient>(IServiceProvider sp)
where TClient : class
{
var httpClient = sp.GetRequiredService<HttpClient>();
var localStorage = sp.GetRequiredService<ILocalStorageService>();
var baseUrl = httpClient.BaseAddress?.ToString() ?? throw new InvalidOperationException("Base URL not configured");
var isHttps = baseUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
var credentials = CallCredentials.FromInterceptor(async (context, metadata) =>
{
@@ -207,14 +143,10 @@ public static class ConfigureServices
}
});
var channelCredentials = isHttps
? ChannelCredentials.Create(new SslCredentials(), credentials)
: ChannelCredentials.Create(ChannelCredentials.Insecure, credentials);
var channel = GrpcChannel.ForAddress(baseUrl, new GrpcChannelOptions
{
UnsafeUseInsecureChannelCallCredentials = !isHttps,
Credentials = channelCredentials,
UnsafeUseInsecureChannelCallCredentials = true,
Credentials = ChannelCredentials.Create(new SslCredentials(), credentials),
HttpClient = httpClient,
MaxReceiveMessageSize = 1000 * 1024 * 1024, // 1 GB
MaxSendMessageSize = 1000 * 1024 * 1024 // 1 GB
-4
View File
@@ -20,10 +20,6 @@ FROM 194.5.195.53:32082/dotnet/aspnet:9.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
# Trust the staging-ca so server-side gRPC calls to https://cms.se.kbs1.ir succeed without PartialChain
COPY ["FrontOffice.Main/staging-ca.crt", "/usr/local/share/ca-certificates/staging-ca.crt"]
RUN update-ca-certificates
ENV ASPNETCORE_URLS=http://+:80
EXPOSE 80
+30 -4
View File
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
@@ -10,9 +10,29 @@
<ItemGroup>
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
<!-- Replace all FrontOffice.BFF protobuf packages with CMS protobuf -->
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.210" />
<!-- <ProjectReference Include="../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj" />-->
<PackageReference Include="Foursat.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" /> -->
<PackageReference Include="MudBlazor" Version="8.14.0" />
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="Mapster" Version="7.4.0" />
@@ -56,6 +76,12 @@
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
<Content Remove="Pages\Package\Packages.razor" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Pages\Package\Packages.razor.cs" />
</ItemGroup>
</Project>
+12 -1
View File
@@ -2,13 +2,24 @@
<configuration>
<packageSources>
<clear />
<!-- Nexus (hosts FourSat packages + proxies nuget.org) -->
<!-- Nexus as primary source (proxies nuget.org + caches packages) -->
<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>
+176 -246
View File
@@ -1,262 +1,192 @@
@attribute [Route(RouteConstants.About.Index)]
@inject NavigationManager Navigation
@inject SitePageSettingsService PageSettingsService
<PageTitle>درباره ما | کارا بازار سلامت</PageTitle>
@if (_loading)
{
<MudContainer MaxWidth="MaxWidth.Large" Class="py-16">
<LoadingState />
</MudContainer>
}
else
{
<!-- 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">
@(_page?.HeroTitle ?? "پلتفرم هوشمند تیم‌سازی و مدیریت فروش")
</MudText>
<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>
<!-- 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>
</MudContainer>
</section>
<MudStack Spacing="8">
<!-- 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="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">
تبدیل شدن به پیشروترین پلتفرم مدیریت تیم‌های فروش در منطقه، با ارائه راهکارهای هوشمند و کاربرپسند برای کسب‌وکارهای کوچک و بزرگ.
</MudText>
</MudPaper>
</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);">
<AppImage Path="@(_page?.HeroImagePath ?? "images/team-work.jpg")"
Alt="جلسه تیم فروش"
ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Style="width:100%"
Class="rounded-xl" />
<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>
</MudItem>
</MudGrid>
</MudContainer>
</section>
<MudStack Spacing="8">
<!-- 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>
<!-- 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">
@(_settings?.VisionTitle ?? "چشم‌انداز")
</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
@(_settings?.VisionText ?? "تبدیل شدن به پیشروترین پلتفرم مدیریت تیم‌های فروش در منطقه، با ارائه راهکارهای هوشمند و کاربرپسند برای کسب‌وکارهای کوچک و بزرگ.")
</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" md="6">
<MudPaper Elevation="2" Class="pa-6 text-center h-100">
<MudIcon Icon="@Icons.Material.Filled.TrackChanges" Size="Size.Large" Color="Color.Success" Class="mb-4" />
<MudText Typo="Typo.h5" Class="mb-3">
@(_settings?.MissionTitle ?? "مأموریت")
</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
@(_settings?.MissionText ?? "توانمندسازی کسب‌وکارها از طریق فناوری‌های نوین، ساده‌سازی فرآیندهای پیچیده و ایجاد فرصت‌های جدید برای رشد و توسعه پایدار.")
</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 (_valueImages.Any())
{
@foreach (var value in _valueImages)
{
<MudItem xs="12" sm="6" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-xl text-center">
@if (!string.IsNullOrWhiteSpace(value.IconName))
{
<MudIcon Icon="@($"Icons.Material.Filled.{value.IconName}")" Size="Size.Large" Color="Color.Primary" Class="mb-3" />
}
<MudText Typo="Typo.h6" Class="mb-2">@value.Title</MudText>
@if (!string.IsNullOrWhiteSpace(value.Description))
{
<MudText Typo="Typo.body2" Class="mud-text-secondary">
@value.Description
</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 (_teamImages.Any())
{
@foreach (var member in _teamImages)
{
<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.Description))
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">
@member.Description
</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>
}
<!-- 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>
+2 -41
View File
@@ -1,47 +1,8 @@
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages;
public partial class About
{
private bool _loading = true;
private PageSettingsDto? _page;
private AboutSettings? _settings;
private List<PageSettingsImageDto> _valueImages = new();
private List<PageSettingsImageDto> _teamImages = new();
protected override async Task OnInitializedAsync()
private void ScrollToContact()
{
try
{
_page = await PageSettingsService.GetPageAsync("about");
if (_page != null)
{
_settings = _page.GetSettings<AboutSettings>();
_valueImages = _page.GetImages("values");
_teamImages = _page.GetImages("team");
}
}
catch
{
// Fallback: page remains null → hardcoded content will render
}
finally
{
_loading = false;
}
}
private class AboutSettings
{
public string? VisionTitle { get; set; }
public string? VisionText { get; set; }
public string? VisionIcon { get; set; }
public string? MissionTitle { get; set; }
public string? MissionText { get; set; }
public string? MissionIcon { get; set; }
public string? ValuesTitle { get; set; }
public string? TeamTitle { get; set; }
// TODO: Implement smooth scroll to contact section
}
}
-172
View File
@@ -1,172 +0,0 @@
@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>
@@ -1,91 +0,0 @@
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
@@ -1,122 +0,0 @@
@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>
}
@@ -1,51 +0,0 @@
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");
}
+82 -35
View File
@@ -5,11 +5,10 @@
<PageTitle>تکمیل خرید</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<PageHeader Title="تکمیل خرید" BackHref="@RouteConstants.Package.List" />
<MudGrid Spacing="4">
<!-- Header -->
<MudItem xs="12">
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudPaper Elevation="4" Class="pa-6">
<MudStack Spacing="3">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
<MudIcon Icon="@Icons.Material.Filled.ShoppingCart" Size="Size.Large" Color="Color.Primary" />
@@ -24,14 +23,9 @@
<MudItem xs="12" md="8">
<MudStack>
<!-- Package Details -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudPaper Elevation="4" Class="pa-6">
<MudText Typo="Typo.h5" Class="mb-4">جزئیات پکیج</MudText>
@* ── G2: توضیح خرید پکیج (Q28) ── *@
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3">
با خرید این پکیج، سقف پاداش هفتگی، ضریب کیف‌پول جادویی و دسترسی به فیچرهای اختصاصی برای شما فعال می‌شود.
</MudAlert>
@if (_selectedPackage != null)
{
<MudCard>
@@ -39,9 +33,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);">
<AppImage Path="@_selectedPackage.Image"
<MudImage Src="@_selectedPackage.Image"
Alt="@_selectedPackage.Title"
ImgHeight="150"
Height="150"
ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Style="width:100%"
@@ -71,6 +65,80 @@
</MudStack>
}
</MudPaper>
<!-- Address Selection -->
<MudPaper Elevation="4" Class="pa-6">
<MudText Typo="Typo.h5" Class="mb-4">انتخاب آدرس</MudText>
@if (_isLoadingAddresses)
{
<MudStack AlignItems="AlignItems.Center" Class="py-4">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Medium" />
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-2">بارگذاری آدرس‌ها...</MudText>
</MudStack>
}
else if (_addresses.Any())
{
<MudStack Spacing="2">
@foreach (var address in _addresses)
{
<MudPaper Outlined="@(_selectedAddress?.Id != address.Id)"
Elevation="@(_selectedAddress?.Id == address.Id ? 4 : 0)"
Class="pa-3 rounded-xl cursor-pointer"
Style="@(_selectedAddress?.Id == address.Id ? "border: 2px solid var(--mud-palette-primary);" : "")"
@onclick="() => SetAddressAsDefault(address.Id)">
<MudStack Spacing="1">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudCheckBox @bind-Value="address.IsDefault"
Disabled="true"
Color="Color.Primary" />
<MudText Typo="Typo.subtitle2">@(address.Title)</MudText>
@if (address.IsDefault)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Filled" Size="Size.Small">پیش‌فرض</MudChip>
}
@if (!address.IsDefault)
{
@if (_isSettingDefaultAddress && _settingDefaultAddressId == address.Id)
{
<MudProgressCircular Size="Size.Small" Color="Color.Primary" Indeterminate="true" />
}
else
{
<MudButton Variant="Variant.Text"
Color="Color.Primary"
Size="Size.Small"
OnClick="() => SetAddressAsDefault(address.Id)">
تنظیم به عنوان پیش‌فرض
</MudButton>
}
}
</MudStack>
<MudStack Row="true">
<MudText Typo="Typo.body2" Class="mud-text-secondary">@(address.Address)</MudText>
<MudSpacer />
<MudText Typo="Typo.caption" Class="mud-text-secondary">کد پستی: @(address.PostalCode)</MudText>
</MudStack>
</MudStack>
</MudPaper>
}
</MudStack>
}
else
{
<MudStack AlignItems="AlignItems.Center" Class="py-4">
<MudIcon Icon="@Icons.Material.Filled.LocationOff" Size="Size.Large" Color="Color.Default" />
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-2">آدرسی ثبت نشده است.</MudText>
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="() => Navigation.NavigateTo(RouteConstants.Profile.Index)"
Class="mt-2">
افزودن آدرس
</MudButton>
</MudStack>
}
</MudPaper>
</MudStack>
</MudItem>
@@ -81,7 +149,7 @@
<MudItem xs="12" md="4">
<MudStack Spacing="4">
<!-- Discount Code Section -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudPaper Elevation="4" Class="pa-6">
<MudText Typo="Typo.h5" Class="mb-4">کد تخفیف</MudText>
<MudStack Spacing="3">
<MudTextField @bind-Value="_discountCode"
@@ -108,7 +176,7 @@
<!-- Order Summary -->
@if (_selectedPackage != null)
{
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudPaper Elevation="4" Class="pa-6">
<MudText Typo="Typo.h5" Class="mb-4">خلاصه سفارش</MudText>
<MudStack Spacing="3">
@@ -138,36 +206,15 @@
FullWidth="true"
StartIcon="@Icons.Material.Filled.Payment"
OnClick="ProcessPayment"
Disabled="@(!CanProceedToPayment || _isProcessingPayment || !_selectedPackage.SupportsDirectPurchase)"
Disabled="@(!CanProceedToPayment || _isProcessingPayment)"
Class="mt-2">
@(_isProcessingPayment ? "در حال پردازش..." : "پرداخت آنلاین")
</MudButton>
@if (_selectedPackage.SupportsDayaPurchase)
{
<MudButton Variant="Variant.Filled"
Color="Color.Tertiary"
Size="Size.Large"
FullWidth="true"
StartIcon="@Icons.Material.Filled.Diamond"
OnClick="DayaLoanPayment"
Disabled="@(!CanProceedToPayment)"
Class="mt-2">
تأمین اعتبار الماسی دایا
</MudButton>
}
@if (!_selectedPackage.SupportsDirectPurchase && !_selectedPackage.SupportsDayaPurchase)
{
<MudAlert Severity="Severity.Warning" Dense="true" Class="mt-2">
این پکیج در حال حاضر قابل خرید نیست.
</MudAlert>
}
@if (!CanProceedToPayment)
{
<MudText Typo="Typo.caption" Color="Color.Error" Align="Align.Center">
لطفاً پکیج را انتخاب کنید.
لطفاً پکیج و آدرس را انتخاب کنید.
</MudText>
}
</MudStack>
+108 -48
View File
@@ -1,7 +1,10 @@
using CMSMicroservice.Protobuf.Protos.Package;
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 FrontOffice.Main.Utilities;
using Google.Protobuf.WellKnownTypes;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor;
using Severity = MudBlazor.Severity;
@@ -9,12 +12,17 @@ namespace FrontOffice.Main.Pages;
public partial class Checkout
{
[Inject] private PackageService PackageService { get; set; } = default!;
[Inject] private PackageContract.PackageContractClient PackageClient { get; set; } = default!;
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
[Inject] private UserOrderContract.UserOrderContractClient UserOrderContract { get; set; } = default!;
[Inject] private TransactionContract.TransactionContractClient TransactionContract { get; set; } = default!;
[Parameter] public long? PackageId { get; set; }
private Pack? _selectedPackage;
private List<GetAllUserAddressByFilterResponseModel> _addresses = new();
private GetAllUserAddressByFilterResponseModel? _selectedAddress;
private bool _isLoadingAddresses;
private bool _isProcessingPayment;
// Discount code
@@ -25,11 +33,16 @@ public partial class Checkout
private long _discountAmount;
private long _finalPrice;
private bool CanProceedToPayment => _selectedPackage != null;
// Address management
private bool _isSettingDefaultAddress;
private long? _settingDefaultAddressId;
private bool CanProceedToPayment => _selectedPackage != null && _selectedAddress != null;
protected override async Task OnInitializedAsync()
{
await LoadPackageDetails();
await LoadAddresses();
}
private async Task LoadPackageDetails()
@@ -38,18 +51,15 @@ public partial class Checkout
{
try
{
var packages = await PackageService.GetAllPackagesAsync();
var pkg = packages.FirstOrDefault(p => p.Id == PackageId.Value);
if (pkg != null)
var response = await PackageClient.GetPackageAsync(new() { Id = PackageId.Value });
if (response != null)
{
_selectedPackage = new Pack(
Id: pkg.Id,
Title: pkg.Title,
Body: pkg.Description,
Image: pkg.ImageUrl,
Price: pkg.Price,
SupportsDirectPurchase: pkg.SupportsDirectPurchase,
SupportsDayaPurchase: pkg.SupportsDayaPurchase
Id: response.Id,
Title: response.Title,
Body: response.Description,
Image: UrlUtility.DownloadUrl + response.ImagePath,
Price: response.Price
);
_finalPrice = _selectedPackage.Price;
}
@@ -61,6 +71,60 @@ public partial class Checkout
}
}
private async Task LoadAddresses()
{
_isLoadingAddresses = true;
try
{
var response = await UserAddressContract.GetAllUserAddressByFilterAsync(request: new());
if (response?.Models?.Any() == true)
{
_addresses = response.Models.ToList();
// Select default address if available
_selectedAddress = _addresses.FirstOrDefault(a => a.IsDefault) ?? _addresses.First();
}
else
{
_addresses = new List<GetAllUserAddressByFilterResponseModel>();
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری آدرس‌ها: {ex.Message}", Severity.Error);
_addresses = new List<GetAllUserAddressByFilterResponseModel>();
}
finally
{
_isLoadingAddresses = false;
await InvokeAsync(StateHasChanged);
}
}
private async Task SetAddressAsDefault(long addressId)
{
if (_isSettingDefaultAddress) return;
_isSettingDefaultAddress = true;
_settingDefaultAddressId = addressId;
try
{
await UserAddressContract.SetAddressAsDefaultAsync(new() { Id = addressId });
await LoadAddresses(); // Reload addresses to reflect the change
Snackbar.Add("آدرس پیش‌فرض با موفقیت تغییر یافت.", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در تغییر آدرس پیش‌فرض: {ex.Message}", Severity.Error);
}
finally
{
_isSettingDefaultAddress = false;
_settingDefaultAddressId = null;
await InvokeAsync(StateHasChanged);
}
}
private async Task ApplyDiscountCode()
{
if (string.IsNullOrWhiteSpace(_discountCode))
@@ -92,7 +156,7 @@ public partial class Checkout
_finalPrice = _selectedPackage!.Price;
}
}
catch (Exception)
catch (Exception ex)
{
_discountMessage = "خطا در اعمال کد تخفیف.";
_discountApplied = false;
@@ -108,11 +172,12 @@ public partial class Checkout
private async Task ProcessPayment()
{
if (_isProcessingPayment) return;
Snackbar.Add("درگاه پرداخت متصل نیست! لطفا در زمان دیگری مجددا تلاش فرمایید!", Severity.Warning);
return;
if (!CanProceedToPayment || _selectedPackage == null)
if (!CanProceedToPayment || _selectedPackage == null || _selectedAddress == null)
{
Snackbar.Add("لطفاً پکیج را انتخاب کنید.", Severity.Warning);
Snackbar.Add("لطفاً پکیج و آدرس را انتخاب کنید.", Severity.Warning);
return;
}
@@ -120,26 +185,33 @@ public partial class Checkout
try
{
var response = await PackageClient.CustomerPurchasePackageAsync(new CustomerPurchasePackageRequest
// Step 1: Create payment request
var paymentRequest = new PaymentRequestRequest
{
Amount = _finalPrice,
CallbackUrl = $"{Navigation.BaseUri}checkout/callback",
Description = $"خرید پکیج {_selectedPackage.Title}",
Currency = CurrencyEnum.Irt,
Type = TransactionTypeEnum.Real
};
var paymentResponse = await TransactionContract.PaymentRequestAsync(paymentRequest);
if (string.IsNullOrEmpty(paymentResponse.PaymentGWUrl))
Snackbar.Add("آدرس درگاه پرداخت دریافت نشد.", Severity.Error);
// Step 2: Create user order
var orderRequest = new CreateNewUserOrderRequest
{
Price = _finalPrice,
PackageId = _selectedPackage.Id,
PurchaseMethod = PurchaseMethodEnum.PurchaseMethodGateway
});
PaymentStatus = false // Will be updated after payment verification
};
if (!response.Success)
{
Snackbar.Add(response.Message ?? "خطا در آغاز فرآیند پرداخت", Severity.Error);
return;
}
var orderResponse = await UserOrderContract.CreateNewUserOrderAsync(orderRequest);
if (!string.IsNullOrEmpty(response.PaymentGatewayUrl))
{
Navigation.NavigateTo(response.PaymentGatewayUrl, forceLoad: true);
}
else
{
Snackbar.Add("خطا در دریافت آدرس درگاه پرداخت", Severity.Error);
}
// Step 3: Redirect to payment gateway
Navigation.NavigateTo(paymentResponse.PaymentGWUrl);
}
catch (Exception ex)
{
@@ -152,17 +224,5 @@ public partial class Checkout
}
}
private async Task DayaLoanPayment()
{
if (_selectedPackage == null)
{
Snackbar.Add("لطفاً پکیج را انتخاب کنید.", Severity.Warning);
return;
}
var url = "https://dayadiamond.ir/profile/creditpurchase/?merchantcode=56146364";
await JSRuntime.InvokeVoidAsync("open", url, "_blank");
}
private record Pack(long Id, string Title, string Body, string Image, long Price, bool SupportsDirectPurchase = true, bool SupportsDayaPurchase = false);
}
private record Pack(long Id, string Title, string Body, string Image, long Price);
}
@@ -3,12 +3,7 @@
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-3">فعال‌سازی یا تمدید عضویت</MudText>
@* ── G7: راهنمای هزینه فعال‌سازی (Q28) ── *@
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info">
هزینه تخمینی بر اساس پکیج انتخابی محاسبه می‌شود. هر پکیج قیمت و شرایط فعال‌سازی متفاوتی دارد.
</MudAlert>
<MudForm @ref="_form" @bind-IsValid="_formIsValid">
<MudGrid Spacing="2">
<MudItem xs="12" sm="6">
@@ -1,5 +1,5 @@
using FrontOffice.Main.Utilities;
using CMSMicroservice.Protobuf.Protos.User;
using FrontOffice.BFF.User.Protobuf.Protos.User;
using Microsoft.AspNetCore.Components;
using MudBlazor;
@@ -10,7 +10,6 @@ public partial class ActivationSection : ComponentBase
[Inject] private ClubMembershipService ClubService { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!;
[Inject] private UserContract.UserContractClient UserContract { get; set; } = default!;
[Inject] private PackageService PackageService { get; set; } = default!;
[Parameter] public EventCallback OnActivationSuccess { get; set; }
@@ -22,18 +21,6 @@ public partial class ActivationSection : ComponentBase
private long _packageId = 1;
private int _durationMonths = 1;
private string? _activationCode;
private long _basePackagePrice;
protected override async Task OnInitializedAsync()
{
try
{
var packages = await PackageService.GetAllPackagesAsync();
var basePackage = packages.FirstOrDefault(p => p.IsBasePackage) ?? packages.FirstOrDefault();
_basePackagePrice = basePackage?.Price ?? 0;
}
catch { /* fallback to 0 */ }
}
private async Task HandleActivateAsync()
{
@@ -88,8 +75,9 @@ public partial class ActivationSection : ComponentBase
private string GetEstimatedCost()
{
if (_basePackagePrice <= 0) return "در حال بارگذاری...";
var total = _basePackagePrice * _durationMonths;
// فرمول تقریبی: 56M per month (base amount from BFF implementation)
var baseAmount = 56_000_000;
var total = baseAmount * _durationMonths;
return $"{total:N0} تومان";
}
}
@@ -3,13 +3,15 @@
@using MudBlazor
@inject ClubConfigurationService ClubConfigService
@inject IDialogService DialogService
@inject IJSRuntime JS
<PageTitle>ویژگی‌های باشگاه مشتریان</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="ویژگی‌های باشگاه مشتریان" BackHref="@RouteConstants.Club.Membership" />
<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>
<!-- توضیحات کلی باشگاه -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
@@ -28,13 +30,13 @@
@if (_isLoading)
{
<LoadingState Message="در حال دریافت ویژگی‌ها..." />
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
}
else if (_features == null || !_features.Any())
{
<EmptyState Icon="@Icons.Material.Filled.Stars"
Title="هنوز ویژگی‌ای برای شما فعال نشده است"
Description="با خرید پکیج یا ارتقاء عضویت، ویژگی‌های اختصاصی فعال می‌شوند." />
<MudAlert Severity="Severity.Info">
هنوز ویژگی‌ای برای شما فعال نشده است.
</MudAlert>
}
else
{
@@ -71,7 +73,7 @@
FullWidth="true"
Size="Size.Large"
StartIcon="@Icons.Material.Filled.ArrowBack"
OnClick="GoBack">
Href="@RouteConstants.Club.Membership">
بازگشت به صفحه باشگاه
</MudButton>
</MudStack>
@@ -188,6 +190,4 @@
_selectedFeature = feature;
_showDetailDialog = true;
}
private async Task GoBack() => await JS.InvokeVoidAsync("history.back");
}
@@ -7,16 +7,14 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="عضویت باشگاه مشتریان" BackHref="@RouteConstants.Profile.Index" />
@* ── G6: راهنمای قرارداد باشگاه (Q28) ── *@
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Icon="@Icons.Material.Filled.Gavel">
قرارداد باشگاه مشتریان فقط یک‌بار امضا می‌شود و با هر بار خرید پکیج جدید نیازی به امضای مجدد نیست.
</MudAlert>
<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>
@if (_isLoading)
{
<LoadingState Message="در حال دریافت اطلاعات عضویت..." />
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
}
else if (_membership is not null)
{
@@ -58,6 +56,19 @@
<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>
@@ -69,8 +80,8 @@
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="pa-2">
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Color="Color.Success" Size="Size.Large" />
<MudStack Spacing="0">
<MudText Typo="Typo.subtitle1">شارژ کیف پول فروشگاه اعتباری</MudText>
<MudText Typo="Typo.caption" Color="Color.Default">شارژ برابر ارزش پکیج فعال در کیف پول فروشگاه اعتباری</MudText>
<MudText Typo="Typo.subtitle1">شارژ کیف پول فروشگاه تخفیفی</MudText>
<MudText Typo="Typo.caption" Color="Color.Default">شارژ ۵۶ میلیون تومان کیف پول فروشگاه تخفیفی</MudText>
</MudStack>
</MudStack>
<MudDivider />
@@ -92,6 +103,11 @@
</MudStack>
</MudPaper>
<!-- فعال‌سازی یا تمدید عضویت -->
@if (!_membership.IsActive || (_membership.DaysRemaining.HasValue && _membership.DaysRemaining.Value <= 30))
{
<ActivationSection OnActivationSuccess="HandleActivationSuccess" />
}
<!-- دکمه مشاهده ویژگی‌های بیشتر -->
<MudButton Variant="Variant.Outlined"
@@ -8,17 +8,15 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="پاداش‌های من" BackHref="@RouteConstants.Profile.Index" />
@* ── G5: توضیح محاسبه پاداش (Q28) ── *@
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Icon="@Icons.Material.Filled.Info">
پاداش هفتگی بر اساس هر پکیج جداگانه محاسبه می‌شود. هر پکیج Pool پورسانت مستقل دارد و Carryover بر اساس پکیج زیرمجموعه‌هاست.
</MudAlert>
<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>
<!-- فیلترها -->
<MudPaper Elevation="2" Class="pa-3 rounded-lg">
<MudGrid Spacing="2">
<MudItem xs="12" sm="6" md="3">
<MudItem xs="12" sm="6" md="4">
<WeekSelector @ref="_weekSelector"
@bind-Value="_selectedWeekDefinition"
Label="انتخاب هفته"
@@ -28,7 +26,7 @@
Clearable="true"
OnlyActive="false" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudItem xs="12" sm="6" md="4">
<MudSelect T="string" @bind-Value="_filterStatus" Label="وضعیت" Variant="Variant.Outlined">
<MudSelectItem T="string" Value="@string.Empty">همه</MudSelectItem>
<MudSelectItem T="string" Value="@("Pending")">در انتظار</MudSelectItem>
@@ -39,16 +37,7 @@
<MudSelectItem T="string" Value="@("Cancelled")">لغو شده</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="long?" @bind-Value="_filterPackageId" Label="پکیج" Variant="Variant.Outlined" Clearable="true">
<MudSelectItem T="long?" Value="@((long?)null)">همه پکیج‌ها</MudSelectItem>
@foreach (var pkg in _packages)
{
<MudSelectItem T="long?" Value="@((long?)pkg.Id)">@pkg.Title</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudItem xs="12" sm="12" md="4">
<MudStack Row="true" Spacing="2" Style="height: 100%;" AlignItems="AlignItems.End">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ApplyFiltersAsync" StartIcon="@Icons.Material.Filled.FilterList" FullWidth="true">
اعمال فیلتر
@@ -63,7 +52,7 @@
@if (_isLoading)
{
<LoadingState Message="در حال دریافت پاداش‌ها..." />
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
}
else
{
@@ -101,7 +90,6 @@
<MudTable Items="_payouts" Hover="true" Striped="true" Dense="true" FixedHeader="true" Height="400px">
<HeaderContent>
<MudTh>هفته</MudTh>
<MudTh>پکیج</MudTh>
<MudTh>امتیاز</MudTh>
<MudTh>مبلغ (تومان)</MudTh>
<MudTh>وضعیت</MudTh>
@@ -112,11 +100,6 @@
<MudTd DataLabel="هفته">
<MudChip T="string" Color="Color.Primary" Size="Size.Small" Variant="Variant.Outlined">@context.WeekDisplayName</MudChip>
</MudTd>
<MudTd DataLabel="پکیج">
<MudChip T="string" Color="Color.Tertiary" Size="Size.Small">
@(string.IsNullOrEmpty(context.PackageTitle) ? "-" : context.PackageTitle)
</MudChip>
</MudTd>
<MudTd DataLabel="امتیاز">@context.BalancesEarned</MudTd>
<MudTd DataLabel="مبلغ">
<MudText Color="Color.Success"><strong>@context.AmountFormatted</strong></MudText>
@@ -147,10 +130,6 @@
<MudChip T="string" Color="Color.Primary" Size="Size.Small" Variant="Variant.Outlined">@payout.WeekDisplayName</MudChip>
<MudChip T="string" Color="@GetStatusColor(payout.StatusColor)" Size="Size.Small">@payout.StatusText</MudChip>
</MudStack>
@if (!string.IsNullOrEmpty(payout.PackageTitle))
{
<MudChip T="string" Color="Color.Tertiary" Size="Size.Small">@payout.PackageTitle</MudChip>
}
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h6" Color="Color.Success">@payout.AmountFormatted</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">امتیاز: @payout.BalancesEarned</MudText>
@@ -8,10 +8,8 @@ namespace FrontOffice.Main.Pages.Commission;
public partial class CommissionDashboardPage : ComponentBase
{
[Inject] private CommissionService CommissionService { get; set; } = default!;
[Inject] private PackageService PackageService { get; set; } = default!;
private List<CommissionPayoutDto> _payouts = new();
private List<PackageDto> _packages = new();
private int _totalCount;
private long _totalAmount;
private int _pageNumber = 1;
@@ -21,26 +19,12 @@ public partial class CommissionDashboardPage : ComponentBase
private WeekSelector? _weekSelector;
private WeekDefinitionDto? _selectedWeekDefinition;
private string _filterStatus = string.Empty;
private long? _filterPackageId;
protected override async Task OnInitializedAsync()
{
await LoadPackagesAsync();
await LoadPayoutsAsync();
}
private async Task LoadPackagesAsync()
{
try
{
_packages = await PackageService.GetAllPackagesAsync();
}
catch
{
_packages = new List<PackageDto>();
}
}
private async Task LoadPayoutsAsync()
{
try
@@ -51,8 +35,7 @@ public partial class CommissionDashboardPage : ComponentBase
weekDefinitionId,
_filterStatus,
_pageNumber,
_pageSize,
_filterPackageId
_pageSize
);
_payouts = result.Payouts;
_totalCount = result.TotalCount;
@@ -78,7 +61,6 @@ public partial class CommissionDashboardPage : ComponentBase
{
_selectedWeekDefinition = null;
_filterStatus = string.Empty;
_filterPackageId = null;
_pageNumber = 1;
await LoadPayoutsAsync();
}
@@ -7,12 +7,15 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="گزارش هفتگی پاداش" BackHref="@RouteConstants.Commission.Dashboard" />
<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>
<!-- انتخاب هفته -->
<MudPaper Elevation="2" Class="pa-3 rounded-lg">
<MudGrid Spacing="2">
<MudItem xs="8" sm="4" md="4">
<MudItem xs="8" sm="6" md="6">
<WeekSelector @ref="_weekSelector"
@bind-Value="_selectedWeekDefinition"
Label="انتخاب هفته"
@@ -21,15 +24,15 @@
Dense="false"
OnlyActive="true" />
</MudItem>
<MudItem xs="4" sm="4" md="4">
<MudSelect T="long?" @bind-Value="_filterPackageId" Label="پکیج" Variant="Variant.Outlined" Clearable="true">
<MudSelectItem T="long?" Value="@((long?)null)">همه پکیج‌ها</MudSelectItem>
@foreach (var pkg in _packages)
{
<MudSelectItem T="long?" Value="@((long?)pkg.Id)">@pkg.Title</MudSelectItem>
}
</MudSelect>
</MudItem>
@* <MudItem xs="4" sm="3" md="3"> *@
@* <MudButton Variant="Variant.Outlined" *@
@* OnClick="LoadCurrentWeekAsync" *@
@* StartIcon="@Icons.Material.Filled.Today" *@
@* FullWidth="true" *@
@* Style="height: 56px;"> *@
@* هفته جاری *@
@* </MudButton> *@
@* </MudItem> *@
<MudItem xs="4" sm="3" md="3" Class="pt-6">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
@@ -47,7 +50,7 @@
@if (_isLoading)
{
<LoadingState Message="در حال دریافت گزارش..." />
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
}
else if (_weeklyBalance is not null)
{
@@ -66,12 +69,6 @@
<MudItem xs="6" sm="4">
<MudText Typo="Typo.caption" Class="mud-text-secondary">پایان: <strong>@_weeklyBalance.EndDatePersian</strong></MudText>
</MudItem>
@if (!string.IsNullOrEmpty(_weeklyBalance.PackageTitle))
{
<MudItem xs="12" sm="12">
<MudChip T="string" Color="Color.Info" Size="Size.Small" Icon="@Icons.Material.Filled.Inventory2">@_weeklyBalance.PackageTitle</MudChip>
</MudItem>
}
</MudGrid>
</MudPaper>
@@ -79,11 +76,11 @@
<MudGrid Spacing="2">
<MudItem xs="6">
<MudPaper Elevation="2" Class="pa-4 rounded-lg text-center bg-success-soft stat-card-ds">
<MudPaper Elevation="3" Class="pa-4 rounded-lg text-center" Style="background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);">
<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" Class="dash-stat-value">@_weeklyBalance.RightBalanceFormatted</MudText>
<MudText Typo="Typo.h5" Color="Color.Success" Style="font-weight: bold;">@_weeklyBalance.RightBalanceFormatted</MudText>
<MudProgressLinear Color="Color.Success"
Value="@GetRightPercentage()"
Size="Size.Medium"
@@ -96,11 +93,11 @@
</MudPaper>
</MudItem>
<MudItem xs="6">
<MudPaper Elevation="2" Class="pa-4 rounded-lg text-center bg-info-soft stat-card-ds">
<MudPaper Elevation="3" Class="pa-4 rounded-lg text-center" Style="background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);">
<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" Class="dash-stat-value">@_weeklyBalance.LeftBalanceFormatted</MudText>
<MudText Typo="Typo.h5" Color="Color.Info" Style="font-weight: bold;">@_weeklyBalance.LeftBalanceFormatted</MudText>
<MudProgressLinear Color="Color.Info"
Value="@GetLeftPercentage()"
Size="Size.Medium"
@@ -117,7 +114,7 @@
<!-- محاسبات پاداش - فشرده‌تر -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.subtitle1" Class="mb-2 fw-bold">محاسبات پاداش</MudText>
<MudText Typo="Typo.subtitle1" Class="mb-2" Style="font-weight: bold;">محاسبات پاداش</MudText>
<MudSimpleTable Dense="true" Hover="true" Style="background: transparent;">
<tbody>
<tr>
@@ -8,7 +8,6 @@ namespace FrontOffice.Main.Pages.Commission;
public partial class WeeklyBalancePage : ComponentBase
{
[Inject] private CommissionService CommissionService { get; set; } = default!;
[Inject] private PackageService PackageService { get; set; } = default!;
[Inject] private ISnackbar SnackbarService { get; set; } = default!;
[Parameter]
@@ -18,8 +17,6 @@ public partial class WeeklyBalancePage : ComponentBase
private WeekSelector? _weekSelector;
private WeekDefinitionDto? _selectedWeekDefinition;
private WeeklyBalanceDto? _weeklyBalance;
private List<PackageDto> _packages = new();
private long? _filterPackageId;
private bool _isLoading = true;
private bool _hasError = false;
@@ -37,8 +34,6 @@ public partial class WeeklyBalancePage : ComponentBase
{
if (firstRender)
{
await LoadPackagesAsync();
// Wait for WeekSelector to initialize and load its cached weeks
if (_weekSelector != null)
{
@@ -93,7 +88,7 @@ public partial class WeeklyBalancePage : ComponentBase
long? weekDefinitionId = _selectedWeekDefinition?.Id;
_weeklyBalance = await CommissionService.GetMyWeeklyBalanceAsync(weekDefinitionId, _filterPackageId);
_weeklyBalance = await CommissionService.GetMyWeeklyBalanceAsync(weekDefinitionId);
// Update week display name from selected definition if available
if (_selectedWeekDefinition != null && _weeklyBalance != null)
@@ -118,18 +113,6 @@ public partial class WeeklyBalancePage : ComponentBase
}
}
private async Task LoadPackagesAsync()
{
try
{
_packages = await PackageService.GetAllPackagesAsync();
}
catch
{
_packages = new();
}
}
private async Task LoadCurrentWeekAsync()
{
if (_weekSelector != null)
+23 -23
View File
@@ -1,5 +1,4 @@
@attribute [Route(RouteConstants.Contact.Index)]
@inject SitePageSettingsService PageSettingsService
<PageTitle>ارتباط با ما | کارا بازار سلامت</PageTitle>
@@ -9,10 +8,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>
@@ -26,7 +25,7 @@
<MudGrid Spacing="6">
<!-- Contact Form -->
<MudItem xs="12" lg="8">
<MudPaper Elevation="2" Class="pa-6">
<MudPaper Elevation="3" Class="pa-6">
<MudText Typo="Typo.h5" Class="mb-6">فرم تماس</MudText>
<MudForm @ref="_form" Model="_contactForm">
@@ -73,12 +72,12 @@
Variant="Variant.Outlined"
Required="true"
RequiredError="انتخاب موضوع الزامی است.">
<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>
<MudSelectItem Value="@("general")">عمومی</MudSelectItem>
<MudSelectItem Value="@("support")">پشتیبانی فنی</MudSelectItem>
<MudSelectItem Value="@("sales")">فروش و قیمت‌گذاری</MudSelectItem>
<MudSelectItem Value="@("partnership")">شریک تجاری</MudSelectItem>
<MudSelectItem Value="@("complaint")">شکایت</MudSelectItem>
<MudSelectItem Value="@("suggestion")">پیشنهاد</MudSelectItem>
</MudSelect>
</MudItem>
@@ -124,7 +123,7 @@
<MudItem xs="12" lg="4">
<MudStack Spacing="4">
<!-- Contact Details -->
<MudPaper Elevation="2" Class="pa-6">
<MudPaper Elevation="3" Class="pa-6">
<MudText Typo="Typo.h6" Class="mb-4">اطلاعات تماس</MudText>
<MudStack Spacing="4">
@@ -133,7 +132,7 @@
<div>
<MudText Typo="Typo.body2" >آدرس</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
@(_settings?.Address ?? "کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶")
کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶
</MudText>
</div>
</MudStack>
@@ -142,7 +141,7 @@
<MudIcon Icon="@Icons.Material.Filled.Phone" Color="Color.Success" Size="Size.Large" />
<div>
<MudText Typo="Typo.body2" >تلفن</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@(_settings?.Phone ?? "026-34233563")</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">026-34233563</MudText>
</div>
</MudStack>
@@ -150,7 +149,7 @@
<MudIcon Icon="@Icons.Material.Filled.Email" Color="Color.Info" Size="Size.Large" />
<div>
<MudText Typo="Typo.body2" >ایمیل</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@(_settings?.Email ?? "info@kbs1.co")</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">info@kbs1.co</MudText>
</div>
</MudStack>
@@ -159,7 +158,8 @@
<div>
<MudText Typo="Typo.body2" >ساعات کاری</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
@((MarkupString)(_settings?.WorkingHours ?? "شنبه تا پنج‌شنبه<br />۹ صبح تا ۶ عصر"))
شنبه تا پنج‌شنبه<br />
۹ صبح تا ۶ عصر
</MudText>
</div>
</MudStack>
@@ -167,14 +167,14 @@
</MudPaper>
<!-- Social Media -->
<MudPaper Elevation="2" Class="pa-6">
<MudPaper Elevation="3" 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="@(_settings?.TelegramUrl ?? "https://t.me/kbs1")"
Href="https://t.me/kbs1"
Target="_blank"
FullWidth="true">
تلگرام
@@ -183,7 +183,7 @@
<MudButton Variant="Variant.Outlined"
Color="Color.Inherit"
StartIcon="@Icons.Custom.Brands.Instagram"
Href="@(_settings?.InstagramUrl ?? "https://instagram.com/kbs1")"
Href="https://instagram.com/kbs1"
Target="_blank"
FullWidth="true">
اینستاگرام
@@ -192,7 +192,7 @@
<MudButton Variant="Variant.Outlined"
Color="Color.Inherit"
StartIcon="@Icons.Custom.Brands.LinkedIn"
Href="@(_settings?.LinkedinUrl ?? "https://linkedin.com/company/kbs1")"
Href="https://linkedin.com/company/kbs1"
Target="_blank"
FullWidth="true">
لینکدین
@@ -201,7 +201,7 @@
<MudButton Variant="Variant.Outlined"
Color="Color.Inherit"
StartIcon="@Icons.Custom.Brands.WhatsApp"
Href="@(_settings?.WhatsappUrl ?? "https://wa.me/989123456789")"
Href="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="2" Class="pa-4 overflow-hidden">
<MudPaper Elevation="3" Class="pa-4 overflow-hidden">
<!-- Placeholder for Map -->
<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;">
<div style="height: 400px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center;">
<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">
@(_settings?.Address ?? "کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶")
کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶
</MudText>
</MudStack>
</div>
+4 -38
View File
@@ -1,10 +1,8 @@
using FluentValidation;
using FrontOffice.Main.Utilities;
using MudBlazor;
using Severity = MudBlazor.Severity;
namespace FrontOffice.Main.Pages;
public partial class Contact
{
private ContactForm _contactForm = new();
@@ -12,27 +10,6 @@ public partial class Contact
private bool _isSubmitting;
private readonly ContactFormValidator _contactFormValidator = new();
// Dynamic CMS data (new simplified system)
private PageSettingsDto? _page;
private ContactSettings? _settings;
protected override async Task OnInitializedAsync()
{
try
{
_page = await PageSettingsService.GetPageAsync("contact");
if (_page != null)
{
_settings = _page.GetSettings<ContactSettings>();
}
}
catch
{
// Fallback: all values remain null → hardcoded defaults will render
}
}
private async Task SubmitContactForm()
{
if (_form is null) return;
@@ -78,12 +55,14 @@ public partial class Contact
private void CallSupport()
{
Snackbar.Add($"شماره تماس: {_settings?.Phone ?? "026-34233563"}", Severity.Info);
// TODO: Initiate phone call or show phone number
Snackbar.Add("شماره تماس: ۰۲۱-۱۲۳۴۵۶۷۸", Severity.Info);
}
private void SendEmail()
{
Snackbar.Add($"ایمیل: {_settings?.Email ?? "info@kbs1.co"}", Severity.Info);
// TODO: Open email client or redirect to email page
Snackbar.Add("ایمیل: info@kbs1.co", Severity.Info);
}
public class ContactForm
@@ -108,17 +87,4 @@ public partial class Contact
RuleFor(x => x.Message).NotEmpty().MinimumLength(10).WithMessage("پیام باید حداقل ۱۰ کاراکتر باشد");
}
}
// DTO class for SettingsJson deserialization
private class ContactSettings
{
public string? Address { get; set; }
public string? Phone { get; set; }
public string? Email { get; set; }
public string? WorkingHours { get; set; }
public string? TelegramUrl { get; set; }
public string? InstagramUrl { get; set; }
public string? LinkedinUrl { get; set; }
public string? WhatsappUrl { get; set; }
}
}
@@ -1,204 +0,0 @@
@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="rounded-lg" ObjectFit="ObjectFit.Cover" />
<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-lg" ObjectFit="ObjectFit.Cover" />
<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>
}
@if (VAT.IsEnabled)
{
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.caption" Class="mud-text-secondary">مالیات ارزش افزوده (@VAT.VatPercentage%):</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@FormatPrice(VatAmount)+ تومان</MudText>
</MudStack>
}
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h6" Class="fw-bold">حداقل پرداخت:</MudText>
<MudText Typo="Typo.h6" Color="Color.Primary" Class="fw-bold">@FormatPrice(TotalWithVat) تومان</MudText>
</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>
}
@if (VAT.IsEnabled)
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">مالیات بر ارزش افزوده (@VAT.VatPercentage%):</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@FormatPrice(VatAmount)+ تومان</MudText>
</MudStack>
}
<MudDivider Class="my-1" />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1" Class="fw-bold">حداقل مبلغ پرداخت درگاه:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="fw-bold">@FormatPrice(TotalWithVat) تومان</MudText>
</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>
@@ -1,75 +0,0 @@
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!;
[Inject] private VATService VAT { get; set; } = default!;
[Inject] private AuthDialogService AuthDialogService { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
if (!await AuthService.IsAuthenticatedAsync())
{
await AuthDialogService.ShowAuthDialogAsync();
}
await DiscountCart.EnsureInitializedAsync();
DiscountCart.OnChange += StateHasChanged;
}
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);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await VAT.LoadAsync();
StateHasChanged();
}
}
private long VatAmount => VAT.CalculateVAT(DiscountCart.FinalPrice);
private long TotalWithVat => DiscountCart.FinalPrice + VatAmount;
private static string FormatPrice(long price) => $"{price:N0} ";
private static string GetImageUrl(string? imageUrl)
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/');
public void Dispose()
{
DiscountCart.OnChange -= StateHasChanged;
}
}
@@ -1,159 +0,0 @@
@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" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="OpenAddAddressDialog">افزودن آدرس</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>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
@if (address.IsDefault)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Outlined" Size="Size.Small">پیش‌فرض</MudChip>
}
<span @onclick:stopPropagation="true">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Size="Size.Small"
Color="Color.Primary"
aria-label="ویرایش آدرس"
OnClick="@(() => OpenEditAddressDialog(address))" />
</span>
</MudStack>
</MudStack>
</MudPaper>
}
<MudButton Class="mt-2" Variant="Variant.Text" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="OpenAddAddressDialog">افزودن آدرس جدید</MudButton>
</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-lg" ObjectFit="ObjectFit.Cover" />
<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">مالیات بر ارزش افزوده (@VAT.VatPercentage٪):</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@FormatPrice(VatAmount)+ تومان</MudText>
</MudStack>
<MudDivider Class="my-1" />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1" Class="fw-bold">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="fw-bold">
@FormatPrice(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>
@@ -1,156 +0,0 @@
using Microsoft.AspNetCore.Components;
using CMSMicroservice.Protobuf.Protos.UserAddress;
using FrontOffice.Main.Pages.Profile.Components;
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!;
[Inject] private VATService VAT { get; set; } = default!;
[Inject] private AuthDialogService AuthDialogService { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!;
private List<CustomerAddressModel> _addresses = new();
private CustomerAddressModel? _selectedAddress;
private bool _loadingAddresses;
private string? _notes;
private bool _placing;
/// <summary>مبلغ درگاه قبل از مالیات (جمع کل - اعتبار)</summary>
private long NetGatewayAmount => DiscountCart.TotalPrice - DiscountCart.TotalDiscount;
/// <summary>مالیات بر ارزش افزوده</summary>
private long VatAmount => VAT.CalculateVAT(NetGatewayAmount);
/// <summary>مبلغ نهایی قابل پرداخت (شامل VAT)</summary>
private long FinalGatewayAmount => NetGatewayAmount + VatAmount;
private bool CanPlaceOrder => DiscountCart.Items.Count > 0 && _selectedAddress is not null;
protected override async Task OnInitializedAsync()
{
if (!await AuthService.IsAuthenticatedAsync())
{
await AuthDialogService.ShowAuthDialogAsync();
}
await VAT.LoadAsync();
await DiscountCart.EnsureInitializedAsync();
var userInfo = await AuthService.GetUserAuthInfo();
if (userInfo.HasAddress)
await LoadAddresses();
else
_addresses = new();
}
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();
_selectedAddress = null;
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری آدرس‌ها: {ex.Message}", Severity.Error);
_addresses = new();
_selectedAddress = null;
}
finally
{
_loadingAddresses = false;
await InvokeAsync(StateHasChanged);
}
}
private async Task OpenAddAddressDialog()
{
var dialog = await DialogService.ShowAsync<AddAddressDialog>("افزودن آدرس جدید");
var result = await dialog.Result;
if (result is { Canceled: false })
{
await AuthService.RefreshTokenAsync();
await LoadAddresses();
}
}
private async Task OpenEditAddressDialog(CustomerAddressModel address)
{
var dialog = await DialogService.ShowAsync<EditAddressDialog>("ویرایش آدرس", new DialogParameters<EditAddressDialog>
{
{ x => x.Model, address }
});
var result = await dialog.Result;
if (result is { Canceled: false })
{
await LoadAddresses();
}
}
private async Task PlaceOrder()
{
if (!CanPlaceOrder || _selectedAddress is null)
{
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('/');
}
@@ -1,217 +0,0 @@
@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>
}
@if (VAT.IsEnabled)
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">مالیات بر ارزش افزوده (@VAT.VatPercentage%):</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@FormatPrice(OrderVatAmount)+</MudText>
</MudStack>
}
<MudDivider Class="my-1" />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<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>
}
@@ -1,92 +0,0 @@
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!;
[Inject] private VATService VAT { get; set; } = default!;
private DiscountOrderDetail? _order;
private bool _loading = true;
private string? _paymentMessage;
private MudBlazor.Severity _paymentSeverity;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await VAT.LoadAsync();
StateHasChanged();
}
}
/// <summary>مبلغ قبل از مالیات (جمع کل - اعتبار)</summary>
private long PreVatGateway => _order is null ? 0 : (_order.TotalPrice - _order.DiscountBalanceUsed);
/// <summary>مالیات بر ارزش افزوده</summary>
private long OrderVatAmount => VAT.CalculateVAT(PreVatGateway);
protected override async Task OnInitializedAsync()
{
// نمایش پیام نتیجه پرداخت (بعد از بازگشت از درگاه)
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");
}
}
}
@@ -1,130 +0,0 @@
@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>
@@ -1,65 +0,0 @@
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");
}
}
}
@@ -1,173 +0,0 @@
@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.Cover"
Style="width:100%; aspect-ratio:1/1; 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 -->
@if (_isAuthenticated)
{
<MudPaper Class="pa-4 rounded-lg discount-price-box" Elevation="0">
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2" Class="mud-text-secondary">قیمت محصول:</MudText>
<MudText Typo="Typo.body1" Class="fw-bold">
@($"{_product.Price:N0}") تومان
</MudText>
</MudStack>
@if (VAT.IsEnabled)
{
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2" Class="mud-text-secondary">مالیات ارزش افزوده (@VAT.VatPercentage%):</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
@($"{VAT.CalculateVAT(_product.Price):N0}") تومان
</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2" Class="mud-text-secondary">قیمت با مالیات:</MudText>
<MudText Typo="Typo.h5" Color="Color.Primary" Class="fw-bold">
@($"{VAT.AddVAT(_product.Price):N0}") تومان
</MudText>
</MudStack>
}
else
{
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2" Class="mud-text-secondary">قیمت نهایی:</MudText>
<MudText Typo="Typo.h5" Color="Color.Primary" Class="fw-bold">
@($"{_product.Price:N0}") تومان
</MudText>
</MudStack>
}
@if (_product.MaxDiscountPercent > 0)
{
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2" Class="mud-text-secondary">اعتبار از کیف اعتباری:</MudText>
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">
@_product.MaxDiscountPercent% (@($"{_product.Price * _product.MaxDiscountPercent / 100:N0}") تومان)
</MudChip>
</MudStack>
}
</MudStack>
</MudPaper>
}
else
{
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined" Icon="@Icons.Material.Filled.Lock">
برای مشاهده قیمت ابتدا وارد شوید
</MudAlert>
}
<!-- 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>
@@ -1,85 +0,0 @@
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!;
[Inject] private VATService VAT { get; set; } = default!;
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!;
private bool _isAuthenticated;
private DiscountProductDetail? _product;
private string _selectedImage = string.Empty;
private int _quantity = 1;
private bool _loading = true;
private bool _addingToCart;
protected override async Task OnInitializedAsync()
{
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
await LoadProductAsync();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await VAT.LoadAsync();
StateHasChanged();
}
}
private async Task LoadProductAsync()
{
_loading = true;
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
{
var productId = _product.Id;
var qty = _quantity;
var title = _product.Title;
await GuestGate.RunAsync(async () =>
{
await DiscountCartService.AddAsync(productId, qty);
Snackbar.Add($"{title} به سبد خرید اضافه شد", MudBlazor.Severity.Success);
});
}
catch
{
Snackbar.Add("خطا در افزودن به سبد خرید", MudBlazor.Severity.Error);
}
finally
{
_addingToCart = false;
}
}
}
@@ -1,161 +0,0 @@
@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 (_products.Count == 0)
{
<MudAlert Severity="Severity.Info" Class="my-4">محصولی یافت نشد.</MudAlert>
}
else
{
<!-- Products Grid -->
<MudGrid Spacing="1">
@foreach (var p in _products)
{
<MudItem xs="6" sm="6" md="3"
onclick="@(() => NavigateToProduct(p.Id))">
<div id="@($"shop-product-{p.Id}")" class="h-100">
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden"
Style="cursor:pointer;">
<MudCardContent Class="d-flex flex-column pa-1 h-100">
<div style="aspect-ratio:1/1;width:100%;background-image: url('@(string.IsNullOrWhiteSpace(p.ThumbnailUrl) ? "/images/product-placeholder.svg" : p.ThumbnailUrl)');background-size: cover; background-position: center;border-radius: 0.5rem; position: relative;">
@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>
@if (_isAuthenticated)
{
<div>
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatPrice(p.Price)</MudText>
<MudText Typo="Typo.overline" Class="mud-text-secondary" Style="font-size:0.6rem;line-height:1;">(+ ارزش افزوده)</MudText>
</div>
}
else
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">
<MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Small" Class="me-1"/>برای مشاهده قیمت وارد شوید
</MudText>
}
</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>
</div>
</MudItem>
}
</MudGrid>
@* Lazy Load — بارگذاری بیشتر *@
@if (_loadingMore)
{
<MudStack AlignItems="AlignItems.Center" Class="py-4">
<MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true"/>
<MudText Typo="Typo.caption" Class="mud-text-secondary">بارگذاری بیشتر...</MudText>
</MudStack>
}
else if (_hasMore)
{
<div class="d-flex justify-center py-4">
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.ExpandMore"
OnClick="LoadMore">
نمایش محصولات بیشتر
</MudButton>
</div>
}
else if (_products.Count > 0)
{
<MudText Typo="Typo.caption" Align="Align.Center" Class="py-4 mud-text-secondary">
همه @(_products.Count) محصول نمایش داده شد
</MudText>
}
}
</MudContainer>
@@ -1,212 +0,0 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.DiscountStore;
public partial class Products : ComponentBase, IDisposable
{
[Inject] private DiscountProductService ProductService { get; set; } = default!;
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!;
[Inject] private IJSRuntime Js { get; set; } = default!;
private bool _isAuthenticated;
private string _search = string.Empty;
private long? _selectedCategoryId;
private int _currentPage = 1;
private bool _loading;
private bool _loadingMore;
private bool _hasMore = true;
private int _totalCount;
private const int PageSize = 12;
private const string DefaultSortBy = "price desc";
private List<DiscountProductCard> _products = new();
private List<DiscountCategoryNode> _categories = new();
private bool _ignoreNextLocationChange;
private bool _pendingScrollRestore;
protected override async Task OnInitializedAsync()
{
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
await DiscountCart.EnsureInitializedAsync();
DiscountCart.OnChange += StateHasChanged;
Navigation.LocationChanged += HandleLocationChanged;
_categories = await ProductService.GetCategoriesAsync();
ApplyStateFromUri();
_loading = true;
await LoadPages(_currentPage);
_loading = false;
_pendingScrollRestore = true;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (_pendingScrollRestore && !_loading && _products.Count > 0)
{
_pendingScrollRestore = false;
var payload = await ShopListScrollRestore.TakeAsync(Js, ShopListScrollRestore.DiscountKey);
if (payload is not null)
await ShopListScrollRestore.RestoreAsync(Js, payload);
}
await base.OnAfterRenderAsync(firstRender);
}
private void ApplyStateFromUri()
{
var state = ShopListQueryState.Parse(Navigation.ToAbsoluteUri(Navigation.Uri));
_search = state.Query;
_selectedCategoryId = state.CategoryId;
_currentPage = state.Pages;
}
private ShopListQueryState CaptureState() => new()
{
Query = _search,
CategoryId = _selectedCategoryId,
Pages = Math.Max(1, _currentPage)
};
private void SyncUrl()
{
var target = CaptureState().ToRelativeUrl(RouteConstants.DiscountStore.Products);
var currentPathAndQuery = Navigation.ToAbsoluteUri(Navigation.Uri).PathAndQuery;
if (string.Equals(currentPathAndQuery, target, StringComparison.OrdinalIgnoreCase))
return;
_ignoreNextLocationChange = true;
Navigation.NavigateTo(target, replace: true);
}
private async Task LoadPages(int pagesToLoad)
{
_products.Clear();
_hasMore = true;
pagesToLoad = Math.Max(1, pagesToLoad);
var search = string.IsNullOrWhiteSpace(_search) ? null : _search;
for (var page = 1; page <= pagesToLoad; page++)
{
var result = await ProductService.GetProductsAsync(
page: page,
pageSize: PageSize,
search: search,
categoryId: _selectedCategoryId,
sortBy: DefaultSortBy);
if (page == 1)
{
_products = result.Products;
_totalCount = result.TotalCount;
}
else
{
_products.AddRange(result.Products);
}
_currentPage = page;
_hasMore = result.CurrentPage < result.TotalPages;
if (!_hasMore)
break;
}
}
private async Task ReloadFromFilters()
{
_loading = true;
_currentPage = 1;
StateHasChanged();
await LoadPages(1);
SyncUrl();
_loading = false;
StateHasChanged();
}
private async Task LoadMore()
{
if (_loadingMore || !_hasMore) return;
_loadingMore = true;
StateHasChanged();
_currentPage++;
var result = await ProductService.GetProductsAsync(
page: _currentPage,
pageSize: PageSize,
search: string.IsNullOrWhiteSpace(_search) ? null : _search,
categoryId: _selectedCategoryId,
sortBy: DefaultSortBy);
_products.AddRange(result.Products);
_hasMore = result.CurrentPage < result.TotalPages;
SyncUrl();
_loadingMore = false;
StateHasChanged();
}
private async Task SearchProducts() => await ReloadFromFilters();
private async Task OnSearchKeyUp(KeyboardEventArgs e)
{
if (e.Key == "Enter")
await ReloadFromFilters();
}
private async Task OnCategoryChanged(long? value)
{
_selectedCategoryId = value;
await ReloadFromFilters();
}
private async Task AddToCart(DiscountProductCard p)
{
await GuestGate.RunAsync(() => DiscountCart.AddAsync(p.Id));
}
private async Task NavigateToProduct(long id)
{
await ShopListScrollRestore.SaveAsync(Js, ShopListScrollRestore.DiscountKey, id);
Navigation.NavigateTo($"{RouteConstants.DiscountStore.ProductDetail}{id}");
}
private void HandleLocationChanged(object? sender, LocationChangedEventArgs args)
{
if (_ignoreNextLocationChange)
{
_ignoreNextLocationChange = false;
return;
}
var uri = Navigation.ToAbsoluteUri(args.Location);
if (!uri.AbsolutePath.Equals(RouteConstants.DiscountStore.Products, StringComparison.OrdinalIgnoreCase))
return;
var incoming = ShopListQueryState.Parse(uri);
if (incoming.Matches(CaptureState()))
return;
_ = InvokeAsync(async () =>
{
ApplyStateFromUri();
_loading = true;
await LoadPages(_currentPage);
_loading = false;
_pendingScrollRestore = true;
StateHasChanged();
});
}
private static string FormatPrice(long price) => $"{price:N0} تومان";
public void Dispose()
{
DiscountCart.OnChange -= StateHasChanged;
Navigation.LocationChanged -= HandleLocationChanged;
}
}
+1 -1
View File
@@ -116,7 +116,7 @@
<!-- Contact Support -->
<section class="py-12 bg-grey-50">
<MudContainer MaxWidth="MaxWidth.Large">
<MudPaper Elevation="2" Class="pa-8 text-center">
<MudPaper Elevation="3" 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">
+1 -1
View File
@@ -55,7 +55,7 @@ public partial class FAQ
Icon = Icons.Material.Filled.Build,
Questions = new List<FAQQuestion>
{
new() { Question = "سازمان فروش چگونه کار می‌کند؟", Answer = "سازمان فروش بصری نمایش سلسله مراتبی تیم شما را نشان می‌دهد و امکان ردیابی روابط ارجاعی را فراهم می‌کند." },
new() { Question = "شجره‌نامه چگونه کار می‌کند؟", Answer = "شجره‌نامه بصری نمایش سلسله مراتبی تیم شما را نشان می‌دهد و امکان ردیابی روابط ارجاعی را فراهم می‌کند." },
new() { Question = "گزارش‌گیری به چه صورت است؟", Answer = "سیستم گزارش‌های جامع مالی، عملکردی و آماری ارائه می‌دهد که قابل فیلتر و دانلود به فرمت Excel است." },
new() { Question = "آیا از موبایل قابل استفاده است؟", Answer = "بله، اپلیکیشن کاملاً responsive است و تجربه کاربری عالی در موبایل و تبلت ارائه می‌دهد." }
}
@@ -1,62 +0,0 @@
@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>
@@ -1,47 +0,0 @@
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");
}
@@ -1,49 +0,0 @@
@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");
}
@@ -1,56 +0,0 @@
@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>
@@ -1,43 +0,0 @@
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;
}
}
+321 -486
View File
@@ -1,519 +1,354 @@
@attribute [Route(RouteConstants.Main.MainPage)]
@inject IJSRuntime JS
@inject NavigationManager Navigation
<PageTitle>صفحه اصلی | کارا بازار سلامت</PageTitle>
<PageTitle>صفحه اصلی</PageTitle>
@* ═══════════════════════════════════════════════
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">
باشگاه مشتریان KBS کارا بازار سلامت
</MudChip>
<!-- 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>
<MudText Typo="Typo.h1" Class="hero-title" Style="max-width:680px;">
@(_pageData?.HeroTitle ?? "رشد تیم، فروش واقعی، پاداش شفاف")
</MudText>
<MudText Typo="Typo.body1" Class="hero-subtitle" Style="max-width:520px;">
@(_pageData?.HeroSubtitle ?? "با دعوت از دوستان، از خریدهای واقعی محصولات سلامت پاداش بگیرید. ثبت‌نام سریع، داشبورد لحظه‌ای.")
</MudText>
<MudStack Row="true" Spacing="2" Class="mt-2 flex-wrap" Justify="Justify.Center">
<MudButton Variant="Variant.Filled"
Class="rounded-pill hero-cta-primary"
Size="Size.Large"
OnClick="NavigateToRegistrationWizard">
@(_settings?.HeroButtonPrimaryText ?? "شروع رایگان")
</MudButton>
<MudButton Variant="Variant.Outlined"
Class="rounded-pill hero-cta-outline"
Size="Size.Large"
OnClick="@(() => Navigation.NavigateTo("/blog"))">
@(_settings?.HeroButtonSecondaryText ?? "آخرین اخبار")
</MudButton>
</MudStack>
@* ── 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>
</MudPaper>
</MudContainer>
</section>
}
@* ═══════════════════════════════════════════════
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">@(_settings?.StepsTitle ?? "سه گام تا شروع")</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>
@* ═══════════════════════════════════════════════
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">@(_settings?.FeaturesTitle ?? "چرا کارا بازار سلامت؟")</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">
@foreach (var (icon, title, desc, delay) in _features)
{
<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 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>
</MudPaper>
</MudItem>
}
<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>
</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-meeting.jpg"
Alt="جلسه تیم فروش"
ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Style="width:100%"
Class="rounded-xl" />
</MudPaper>
</MudItem>
</MudGrid>
</MudContainer>
</section>
<MudStack Spacing="4">
@* ═══════════════════════════════════════════════
4. STATS — Counter cards
═══════════════════════════════════════════════ *@
<section class="section-landing">
<MudContainer MaxWidth="MaxWidth.Medium">
<MudGrid Spacing="3" Justify="Justify.Center">
@foreach (var stat in _stats)
{
<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>
<!-- 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">
سه دلیل روشن برای شروع امروز.
</MudText>
<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>
}
</MudGrid>
</MudContainer>
</section>
@* ═══════════════════════════════════════════════
1c. TOP-SELLING REGULAR PRODUCTS
═══════════════════════════════════════════════ *@
@if (_loadingTopProducts)
{
<section class="section-landing" style="background:var(--mud-palette-background-gray);">
<MudContainer MaxWidth="MaxWidth.Large">
<MudStack AlignItems="AlignItems.Center" Class="py-4">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Small" />
</MudStack>
</MudContainer>
</section>
}
else
{
@if (_topRegularProducts.Any())
{
<section class="section-landing" style="background:var(--mud-palette-background-gray);">
<MudContainer MaxWidth="MaxWidth.Large">
<div class="text-center mb-6 fade-in-up">
<MudText Typo="Typo.h3">محصولات پرفروش فروشگاه</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
محبوب‌ترین محصولات کارا بازار سلامت
</MudText>
</div>
<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>
<MudGrid Spacing="2" Justify="Justify.FlexStart">
@foreach (var p in _topRegularProducts)
{
<MudItem xs="6" sm="6" md="4">
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden landing-product-card"
Style="cursor:pointer;"
@onclick="() => NavigateToRegularProduct(p.Id)">
<MudCardContent Class="d-flex flex-column pa-1 h-100">
<div style="aspect-ratio:1/1;width:100%;background-image:url('@p.ImageUrl');background-size:cover;background-position:center;border-radius:0.5rem;position:relative;">
@if (p.RemainingCount <= 0)
{
<div style="position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;border-radius:0.5rem;">
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">ناموجود</MudChip>
</div>
}
else if (p.RemainingCount <= 5)
{
<MudChip T="string" Color="Color.Warning" Variant="Variant.Filled" Size="Size.Small"
Style="position:absolute;top:8px;right:8px;">
فقط @p.RemainingCount عدد
</MudChip>
}
</div>
<div class="pa-1 flex-grow-1 d-flex flex-column justify-space-between">
<MudText Typo="Typo.subtitle1" Style="display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">@p.Title</MudText>
@if (_isAuthenticated)
{
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatPrice(p.Price)</MudText>
}
else
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">
<MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Small" Class="me-1"/>برای مشاهده قیمت وارد شوید
</MudText>
}
</div>
</MudCardContent>
<MudCardActions Class="mt-auto pa-2" @onclick:stopPropagation="true">
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
StartIcon="@Icons.Material.Filled.AddShoppingCart"
Disabled="@(p.RemainingCount <= 0)"
OnClick="@(() => AddRegularToCart(p))">
@(p.RemainingCount <= 0 ? "ناموجود" : "افزودن به سبد")
</MudButton>
</MudCardActions>
</MudCard>
</MudItem>
}
</MudGrid>
<MudText Class="ps-10 mud-text-secondary">
قوانین روشن، دسترسی آسان به سوابق و گزارش‌ها.
</MudText>
</MudStack>
</MudCard>
</MudItem>
<div class="text-center mt-6">
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Class="rounded-pill"
Href="@RouteConstants.Store.Products"
EndIcon="@Icons.Material.Filled.ArrowBack">
مشاهده همه محصولات
</MudButton>
</div>
</MudContainer>
</section>
}
<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>
@* ═══════════════════════════════════════════════
1d. TOP-SELLING DISCOUNT STORE PRODUCTS
═══════════════════════════════════════════════ *@
@if (_topDiscountProducts.Any())
{
<section class="section-landing">
<MudContainer MaxWidth="MaxWidth.Large">
<div class="text-center mb-6 fade-in-up">
<MudText Typo="Typo.h3">محصولات پرفروش فروشگاه اعتباری</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
محصولات ویژه اعضای باشگاه با پرداخت اعتباری
</MudText>
</div>
<MudGrid Spacing="2" Justify="Justify.FlexStart">
@foreach (var p in _topDiscountProducts)
{
<MudItem xs="6" sm="6" md="4">
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden landing-product-card"
Style="cursor:pointer;"
@onclick="() => NavigateToDiscountProduct(p.Id)">
<MudCardContent Class="d-flex flex-column pa-1 h-100">
<div style="aspect-ratio:1/1;width:100%;background-image:url('@(string.IsNullOrWhiteSpace(p.ThumbnailUrl) ? p.ImageUrl : p.ThumbnailUrl)');background-size:cover;background-position:center;border-radius:0.5rem;position:relative;">
@if (p.RemainingCount <= 0)
{
<div style="position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;border-radius:0.5rem;">
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">ناموجود</MudChip>
</div>
}
else if (p.RemainingCount <= 5)
{
<MudChip T="string" Color="Color.Warning" Variant="Variant.Filled" Size="Size.Small"
Style="position:absolute;top:8px;right:8px;">
فقط @p.RemainingCount عدد
</MudChip>
}
@if (p.MaxDiscountPercent > 0)
{
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled" Size="Size.Small"
Style="position:absolute;top:8px;left:8px;">
@p.MaxDiscountPercent% اعتبار
</MudChip>
}
</div>
<div class="pa-1 flex-grow-1 d-flex flex-column justify-space-between">
<MudText Typo="Typo.subtitle1" Style="display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">@p.Title</MudText>
@if (_isAuthenticated)
{
<div>
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatDiscountPrice(p.Price)</MudText>
<MudText Typo="Typo.overline" Class="mud-text-secondary" Style="font-size:0.6rem;line-height:1;">(+ ارزش افزوده)</MudText>
</div>
}
else
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">
<MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Small" Class="me-1"/>برای مشاهده قیمت وارد شوید
</MudText>
}
</div>
</MudCardContent>
<MudCardActions Class="mt-auto pa-2" @onclick:stopPropagation="true">
<MudButton Variant="Variant.Filled" Color="Color.Secondary" FullWidth="true"
StartIcon="@Icons.Material.Filled.AddShoppingCart"
Disabled="@(p.RemainingCount <= 0)"
OnClick="@(() => AddDiscountToCart(p))">
@(p.RemainingCount <= 0 ? "ناموجود" : "افزودن به سبد")
</MudButton>
</MudCardActions>
</MudCard>
</MudItem>
}
</MudGrid>
<div class="text-center mt-6">
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" Class="rounded-pill"
Href="@RouteConstants.DiscountStore.Products"
EndIcon="@Icons.Material.Filled.ArrowBack">
مشاهده همه محصولات اعتباری
</MudButton>
</div>
</MudContainer>
</section>
}
}
@* ═══════════════════════════════════════════════
5. LATEST BLOG POSTS
═══════════════════════════════════════════════ *@
@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 />
<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>
</MudItem>
}
<MudText Class="ps-10 mud-text-secondary">
تجربه‌ای روان در موبایل و دسکتاپ.
</MudText>
</MudStack>
</MudCard>
</MudItem>
</MudGrid>
<div class="text-center mt-6">
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Class="rounded-pill"
Href="/blog">
مشاهده همه مقالات
</MudButton>
</div>
</MudContainer>
</section>
}
@* ═══════════════════════════════════════════════
6. TESTIMONIALS
═══════════════════════════════════════════════ *@
@if (_testimonials.Count > 0)
{
<section class="section-landing">
<MudContainer MaxWidth="MaxWidth.Large">
<div class="text-center mb-8 fade-in-up">
<MudText Typo="Typo.h3">@(_settings?.TestimonialsTitle ?? "اعتماد مشتریان")</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
بخشی از تجربه استفاده از «کارا بازار سلامت»
@* <!-- FEATURES: معرفی پکیج‌ها -->
<section id="features" class="py-20">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h4" GutterBottom="true">
پکیج‌های سرمایه گذاری
</MudText>
</div>
<MudGrid Spacing="4" Justify="Justify.Center">
@foreach (var t in _testimonials)
<MudText Typo="Typo.subtitle1" Class="mud-text-secondary mb-8">
بر اساس اندازهٔ تیم خود انتخاب کنید.
</MudText>
@if (_isLoadingPackages)
{
<MudItem xs="12" md="4">
<MudPaper Elevation="2" Class="pa-5 rounded-xl testimonial-card fade-in-up" data-delay="@t.Delay">
<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>
}
else if (_packs.Any())
{
<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>
<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>
</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>
</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">
<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>
<MudImage ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Src="images/avatar1.jpg"></MudImage>
</MudAvatar>
<div>
<MudText Typo="Typo.subtitle2">@t.Name</MudText>
<MudText Typo="Typo.overline" Class="mud-text-secondary">@t.Role</MudText>
<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>
}
@* ═══════════════════════════════════════════════
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">@(_settings?.FaqTitle ?? "سوالات متداول")</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
پاسخ به سوالات رایج شما
<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>
</div>
<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>
</MudContainer>
</section>
<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">
@(_settings?.CtaTitle ?? "آماده شروع هستید؟")
</MudText>
<MudText Typo="Typo.body1" Align="Align.Center" Class="hero-subtitle">
@(_settings?.CtaDescription ?? "همین الان ثبت‌نام کنید و از مزایای کارا بازار سلامت بهره‌مند شوید.")
</MudText>
<MudButton Variant="Variant.Filled"
Size="Size.Large"
Class="rounded-pill mt-2 hero-cta-primary"
OnClick="NavigateToRegistrationWizard">
@(_settings?.CtaButtonText ?? "شروع رایگان")
</MudButton>
</MudStack>
</div>
</MudContainer>
</section>
</MudStack>
+83 -319
View File
@@ -1,286 +1,44 @@
using FrontOffice.Main.Utilities;
using FrontOffice.BFF.Package.Protobuf.Protos.Package;
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 BlogPostService BlogPostService { get; set; } = default!;
[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 AuthService AuthService { get; set; } = default!;
[Inject] private SitePageSettingsService PageSettingsService { get; set; } = default!;
[Inject] private ProductService ProductService { get; set; } = default!;
[Inject] private DiscountProductService DiscountProductService { get; set; } = default!;
[Inject] private CartService Cart { get; set; } = default!;
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
[Inject] private VATService VAT { get; set; } = default!;
private bool _isAuthenticated;
// ── CMS page data ──
private PageSettingsDto? _pageData;
private LandingSettings? _settings;
// ── Top-selling product sections ──
private const int LandingTopProductCount = 6;
private List<Product> _topRegularProducts = new();
private List<DiscountProductCard> _topDiscountProducts = new();
private bool _loadingTopProducts = true;
// ── Latest blog posts (loaded from CMS) ──
private List<BlogPostCardDto> _latestPosts = new();
// ── Data lists (populated from DB or fallback) ──
private List<(string Icon, string Text)> _trustBadges = new();
private List<(string Title, string Desc)> _steps = new();
private List<(string Icon, string Title, string Desc, int Delay)> _features = new();
private List<StatItem> _stats = new();
private List<TestimonialItem> _testimonials = new();
private List<QA> _faqs = new();
// Track whether animations need re-initialization after data load
private bool _dataLoaded;
private bool _animationsInitialized;
protected override async Task OnInitializedAsync()
{
MainService.OnChangeHandler += OnStateChanged;
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
// Load landing page settings from CMS
try
MainService.OnChangeHandler +=async () =>
{
_pageData = await PageSettingsService.GetPageAsync("landing");
if (_pageData != null)
{
_settings = _pageData.GetSettings<LandingSettings>();
}
}
catch
{
// Fallback: settings remain null → defaults below
}
await InvokeAsync(StateHasChanged);
};
// await LoadPackagesAsync();
//string mobileNumber = "09387342688";
PopulateFromSettings();
_dataLoaded = true;
//// انکریپت کردن
//string encrypted = Encryptor.EncryptMobileNumber(mobileNumber);
//Console.WriteLine($"Encrypted: {encrypted}");
// Load top-selling in-stock products and blog posts in parallel
var topRegTask = ProductService.GetTopSellingAsync(LandingTopProductCount, inStock: true);
var topDiscTask = DiscountProductService.GetTopSellingAsync(LandingTopProductCount, inStock: true);
var featuredPostsTask = BlogPostService.GetFeaturedPostsAsync(2);
try
{
await Task.WhenAll(topRegTask, topDiscTask, featuredPostsTask);
_topRegularProducts = topRegTask.Result.Products;
_topDiscountProducts = topDiscTask.Result.Products;
}
catch
{
// Fallback: sections remain empty
}
_loadingTopProducts = false;
// Load latest published blog posts
try
{
_latestPosts = featuredPostsTask.IsCompletedSuccessfully
? featuredPostsTask.Result
: await BlogPostService.GetFeaturedPostsAsync(2);
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();
}
//// دیکریپت کردن برای تست
//string decrypted = Encryptor.DecryptMobileNumber(encrypted);
//Console.WriteLine($"Decrypted: {decrypted}");
}
private void PopulateFromSettings()
{
// ── Trust badges ──
if (_settings?.TrustBadges?.Any() == true)
{
_trustBadges = _settings.TrustBadges
.Select(b => (ResolveIcon(b.IconName), b.Text ?? ""))
.ToList();
}
else
{
_trustBadges = new()
{
(Icons.Material.Outlined.Timer, "ثبت‌نام زیر ۲ دقیقه"),
(Icons.Material.Outlined.SupportAgent, "پشتیبانی ۷×۲۴"),
(Icons.Material.Outlined.Lock, "پرداخت ایمن"),
(Icons.Material.Outlined.Verified, "تضمین کیفیت"),
};
}
// ── Steps ──
if (_settings?.Steps?.Any() == true)
{
_steps = _settings.Steps
.Select(s => (s.Title ?? "", s.Description ?? ""))
.ToList();
}
else
{
_steps = new()
{
("ثبت‌نام و احراز هویت", "یک حساب بسازید، شماره موبایل را تأیید و اطلاعات هویتی را تکمیل کنید."),
("دعوت دوستان", "لینک دعوت اختصاصی خود را با دوستان و آشنایان به اشتراک بگذارید."),
("دریافت پاداش", "از خریدهای واقعی اعضای تیمتان پاداش شفاف و لحظه‌ای دریافت کنید."),
};
}
// ── Features ──
if (_settings?.Features?.Any() == true)
{
_features = _settings.Features
.Select((f, i) => (ResolveIcon(f.IconName), f.Title ?? "", f.Description ?? "", i * 100))
.ToList();
}
else
{
_features = new()
{
(Icons.Material.Outlined.VerifiedUser, "ثبت‌نام سریع و ساده", "در چند گام کوتاه حساب بسازید و شروع کنید.", 0),
(Icons.Material.Outlined.StarBorder, "پاداش‌های شفاف", "قوانین روشن، دسترسی آسان به سوابق و گزارش‌ها.", 100),
(Icons.Material.Outlined.Devices, "طراحی واکنش‌گرا", "تجربه‌ای روان در موبایل و دسکتاپ.", 200),
(Icons.Material.Outlined.Groups, "تیم‌سازی هوشمند", "ساختار درختی شبکه و مدیریت تیم‌های فروش.", 300),
(Icons.Material.Outlined.Insights, "گزارش‌های لحظه‌ای", "داشبورد پویا برای مشاهده عملکرد و کمیسیون.", 400),
(Icons.Material.Outlined.Lock, "امنیت بالا", "رمزنگاری اطلاعات و احراز هویت چندمرحله‌ای.", 500),
};
}
// ── Stats ──
if (_settings?.Stats?.Any() == true)
{
_stats = _settings.Stats
.Select((s, i) => new StatItem(
$"stat-{i}",
$"{s.Value}{s.Suffix}",
s.Value,
s.Suffix ?? "",
ResolveColor(s.Color),
s.Label ?? ""))
.ToList();
}
else
{
_stats = new()
{
new("stat-growth", "+۵۰٪", 50, "%+", Color.Success, "رشد میانگین تیم"),
new("stat-uptime", "۹۹.۹٪", 99.9, "%", Color.Primary, "آپ‌تایم سرویس"),
new("stat-deploy", "۳ روز", 3, " روز", Color.Warning, "میانگین زمان استقرار"),
new("stat-coverage", "+۲۰ کشور", 20, "+", Color.Secondary, "پوشش ارسال کد"),
};
}
// ── Testimonials ──
// اگر تنظیمات از CMS لود شده، لیست خالی یعنی ادمین عمداً حذف کرده — بدون fallback hardcode
if (_settings is not null)
{
_testimonials = (_settings.Testimonials ?? [])
.Select((t, i) => new TestimonialItem(t.Quote ?? "", t.Name ?? "", t.Role ?? "", i * 150))
.ToList();
}
else
{
_testimonials = new()
{
new("با کارا بازار سلامت، محاسبه کارمزدها و پایش تیم‌ها بدون اکسل و دردسر انجام می‌شود.", "شرکت سینا نت", "مدیر عملیات", 0),
new("سازمان فروش بصری و گزارش‌های دقیق باعث شد رشد تیم را لحظه‌ای ببینیم.", "هولدینگ آریانا", "مدیر فروش", 150),
new("سادگی ثبت‌نام و شفافیت پاداش‌ها مهم‌ترین مزیت این پلتفرم است.", "گروه بهداشتی نوین", "مدیر توسعه", 300),
};
}
// ── FAQs ──
if (_settings?.Faqs?.Any() == true)
{
_faqs = _settings.Faqs
.Select(f => new QA(f.Question ?? "", f.Answer ?? ""))
.ToList();
}
else
{
_faqs = new()
{
new("دامنه اختصاصی دارم؛ قابل اتصال است؟", "بله، پشت دامنه و گواهی SSL خودتان مستقر می‌شود."),
new("با دیتابیس خودم کار می‌کند؟", "کاملاً. SQL Server، PostgreSQL و MySQL پشتیبانی می‌شود."),
new("چه درگاه‌هایی پشتیبانی می‌شود؟", "Stripe و PayPal یا درگاه اختصاصی از طریق وب‌هوک‌ها."),
new("می‌توانم داده‌ها را خروجی بگیرم؟", "هر زمان از داشبورد ادمین خروجی CSV/Excel بگیرید."),
};
}
}
/// <summary>
/// Resolve icon name from SettingsJson to MudBlazor icon string.
/// Falls back to a generic icon.
/// </summary>
private static string ResolveIcon(string? iconName)
{
if (string.IsNullOrWhiteSpace(iconName)) return Icons.Material.Outlined.Info;
// Try to get from MudBlazor Icons via reflection
var field = typeof(Icons.Material.Outlined).GetField(iconName,
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
if (field != null) return (string)(field.GetValue(null) ?? Icons.Material.Outlined.Info);
// Also try Filled
field = typeof(Icons.Material.Filled).GetField(iconName,
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
if (field != null) return (string)(field.GetValue(null) ?? Icons.Material.Outlined.Info);
return Icons.Material.Outlined.Info;
}
private static Color ResolveColor(string? colorName) => colorName?.ToLowerInvariant() switch
{
"primary" => Color.Primary,
"secondary" => Color.Secondary,
"success" => Color.Success,
"warning" => Color.Warning,
"error" => Color.Error,
"info" => Color.Info,
_ => Color.Default
};
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// Init/re-init scroll animations after data is loaded and rendered
if ((firstRender || (_dataLoaded && !_animationsInitialized)) && _steps.Any())
{
_animationsInitialized = true;
// 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);
}
@@ -289,8 +47,56 @@ public partial class Index : IDisposable
Navigation.NavigateTo(RouteConstants.Registration.Wizard);
}
}
await base.OnAfterRenderAsync(firstRender);
}
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}");
}
private void NavigateToRegistrationWizard()
@@ -298,67 +104,25 @@ public partial class Index : IDisposable
Navigation.NavigateTo(RouteConstants.Registration.Wizard);
}
private void NavigateToPost(string slug)
{
Navigation.NavigateTo($"/blog/{slug}");
}
private record Pack(long Id, string Title, string Body, string Image, long Price);
private void NavigateToRegularProduct(long id)
=> Navigation.NavigateTo(RouteConstants.Store.ProductDetail + id);
private record Plan(string Name, string Price, bool Highlight, IEnumerable<string> Features);
private void NavigateToDiscountProduct(long id)
=> Navigation.NavigateTo(RouteConstants.DiscountStore.ProductDetail + id);
private async Task AddRegularToCart(Product p)
=> await GuestGate.RunAsync(() => Cart.Add(p, 1));
private async Task AddDiscountToCart(DiscountProductCard p)
=> await GuestGate.RunAsync(() => DiscountCart.AddAsync(p.Id));
private string FormatPrice(long price) => $"{VAT.AddVAT(price):N0} تومان";
private string FormatDiscountPrice(long price) => $"{price:N0} تومان";
private async void OnStateChanged()
{
await InvokeAsync(StateHasChanged);
}
// ── Records ──
private record QA(string Q, string A);
private record StatItem(string ElementId, string Display, double Target, string Suffix, Color Color, string Label);
private record TestimonialItem(string Quote, string Name, string Role, int Delay);
// ── SettingsJson DTO ──
private class LandingSettings
private readonly List<QA> _faqs = new()
{
public string? HeroButtonPrimaryText { get; set; }
public string? HeroButtonSecondaryText { get; set; }
public List<TrustBadgeItem>? TrustBadges { get; set; }
public string? StepsTitle { get; set; }
public List<StepItem>? Steps { get; set; }
public string? FeaturesTitle { get; set; }
public List<FeatureItem>? Features { get; set; }
public string? StatsTitle { get; set; }
public List<StatSettingItem>? Stats { get; set; }
public string? TestimonialsTitle { get; set; }
public List<TestimonialSettingItem>? Testimonials { get; set; }
public string? FaqTitle { get; set; }
public List<FaqItem>? Faqs { get; set; }
public string? CtaTitle { get; set; }
public string? CtaDescription { get; set; }
public string? CtaButtonText { get; set; }
public bool? FeaturedBlogEnabled { get; set; }
}
private class TrustBadgeItem { public string? IconName { get; set; } public string? Text { get; set; } }
private class StepItem { public string? Title { get; set; } public string? Description { get; set; } }
private class FeatureItem { public string? IconName { get; set; } public string? Title { get; set; } public string? Description { get; set; } }
private class StatSettingItem { public string? Label { get; set; } public double Value { get; set; } public string? Suffix { get; set; } public string? Color { get; set; } }
private class TestimonialSettingItem { public string? Quote { get; set; } public string? Name { get; set; } public string? Role { get; set; } }
private class FaqItem { public string? Question { get; set; } public string? Answer { get; set; } }
new("دامنهٔ اختصاصی دارم؛ قابل اتصال است؟", "بله، پشت دامنه و گواهی SSL خودتان مستقر می‌شود."),
new("با دیتابیس خودم کار می‌کند؟", "کاملاً. SQL Server، PostgreSQL و MySQL پشتیبانی می‌شود."),
new("چه درگاه‌هایی پشتیبانی می‌شود؟", "Stripe و PayPal یا درگاه اختصاصی از طریق وب‌هوک‌ها."),
new("می‌توانم داده‌ها را خروجی بگیرم؟", "هر زمان از داشبورد ادمین خروجی CSV/Excel بگیرید."),
};
public void Dispose()
{
MainService.OnChangeHandler -= OnStateChanged;
MainService.OnChangeHandler -=async () =>
{
await InvokeAsync(StateHasChanged);
};
}
}
-99
View File
@@ -1,99 +0,0 @@
@attribute [Route(RouteConstants.Licenses.Index)]
@inject SitePageSettingsService PageSettingsService
<PageTitle>مجوزها و گواهینامه‌ها | کارا بازار سلامت</PageTitle>
@if (_loading)
{
<MudContainer MaxWidth="MaxWidth.Large" Class="py-16">
<LoadingState />
</MudContainer>
}
else
{
<!-- Hero Section -->
<section class="licenses-hero-section py-16">
<MudContainer MaxWidth="MaxWidth.Large">
<MudStack Spacing="3" AlignItems="AlignItems.Center" Class="text-center">
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled">مجوزها و گواهینامه‌ها</MudChip>
<MudText Typo="Typo.h2" Class="mb-3">
@(_pageData?.HeroTitle ?? "مجوزها و گواهینامه‌های رسمی")
</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary" Style="max-width: 700px;">
@(_pageData?.HeroSubtitle ?? "ما با دریافت مجوزهای رسمی و تأییدیه‌های معتبر، اطمینان شما را در استفاده از خدمات خود جلب کرده‌ایم.")
</MudText>
</MudStack>
</MudContainer>
</section>
<MudStack Spacing="8" Class="pb-12">
<!-- Licenses Gallery -->
<section class="py-12">
<MudContainer MaxWidth="MaxWidth.Large">
<MudText Typo="Typo.h3" Align="Align.Center" Class="mb-8">مجوزهای ما</MudText>
@if (_licenseImages.Any())
{
<MudGrid Spacing="4" Justify="Justify.Center">
@foreach (var license in _licenseImages)
{
<MudItem xs="6" sm="6" md="4">
<MudCard Elevation="2" Class="h-100">
<MudCardMedia Image="@license.ImagePath" Height="250" />
<MudCardContent>
@if (!string.IsNullOrWhiteSpace(license.Title))
{
<MudText Typo="Typo.h6" Align="Align.Center" Class="mb-1">@license.Title</MudText>
}
@if (!string.IsNullOrWhiteSpace(license.Description))
{
<MudText Typo="Typo.body2" Align="Align.Center" Class="mud-text-secondary">@license.Description</MudText>
}
</MudCardContent>
@if (!string.IsNullOrWhiteSpace(license.LinkUrl))
{
<MudCardActions Class="justify-center">
<MudButton Variant="Variant.Text"
Color="Color.Primary"
Href="@license.LinkUrl"
Target="_blank"
StartIcon="@Icons.Material.Filled.OpenInNew">
مشاهده جزئیات
</MudButton>
</MudCardActions>
}
</MudCard>
</MudItem>
}
</MudGrid>
}
else
{
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Class="my-8" Dense="true">
اطلاعات مجوزها به‌زودی بارگذاری خواهد شد.
</MudAlert>
}
</MudContainer>
</section>
<!-- CTA Section -->
<section class="py-12">
<MudContainer MaxWidth="MaxWidth.Medium">
<MudPaper Elevation="3" Class="pa-8 rounded-xl text-center">
<MudIcon Icon="@Icons.Material.Filled.VerifiedUser" Size="Size.Large" Color="Color.Primary" Class="mb-4" />
<MudText Typo="Typo.h4" Class="mb-3">نیاز به اطلاعات بیشتر دارید؟</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary mb-6">
برای دریافت اطلاعات بیشتر درباره مجوزها و گواهینامه‌های ما، با ما تماس بگیرید.
</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
OnClick="() => Navigation.NavigateTo(RouteConstants.Contact.Index)">
تماس با ما
</MudButton>
</MudPaper>
</MudContainer>
</section>
</MudStack>
}
@@ -1,31 +0,0 @@
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages;
public partial class Licenses
{
private bool _loading = true;
private PageSettingsDto? _pageData;
private List<PageSettingsImageDto> _licenseImages = new();
protected override async Task OnInitializedAsync()
{
try
{
_pageData = await PageSettingsService.GetPageAsync("licenses");
if (_pageData != null)
{
_licenseImages = _pageData.GetImages("licenses");
}
}
catch
{
// Fallback: page remains null → hardcoded content will render
}
finally
{
_loading = false;
}
}
}
@@ -6,11 +6,14 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="آمار باشگاه توسعه دهندگان" BackHref="@RouteConstants.Profile.Index" />
<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>
@if (_isLoading)
{
<LoadingState Message="در حال دریافت آمار..." />
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
}
else if (_statistics is not null)
{
@@ -3,7 +3,16 @@
<PageTitle>پکیج‌های من</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-8">
<PageHeader Title="پکیج‌های من" BackHref="@RouteConstants.Profile.Index" />
<!-- 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>
@if (_isLoading)
{
@@ -20,7 +29,7 @@
<MudIcon Icon="@Icons.Material.Filled.CardGiftcard" Size="Size.Large" Color="Color.Primary" Style="font-size: 80px;" />
<MudText Typo="Typo.h5">شما هنوز پکیجی خریداری نکرده‌اید</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary" Style="max-width: 500px;">
با خرید پکیج، به باشگاه مشتریان بپیوندید و از مزایای ویژه مانند پاداش هفتگی و تیم‌سازی بهره‌مند شوید.
با خرید پکیج طلایی، به باشگاه مشتریان بپیوندید و از مزایای ویژه مانند پاداش هفتگی و تیم‌سازی بهره‌مند شوید.
</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
@@ -34,76 +43,7 @@
}
else
{
@* ── G3: راهنمای پکیج‌های من (Q28) ── *@
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-2" Icon="@Icons.Material.Filled.Info">
بعد از تکمیل چرخه کیف‌پول جادویی، می‌توانید مجدداً پکیج خریداری کرده و دور جدیدی را آغاز کنید.
</MudAlert>
<MudGrid Spacing="4">
@* Re-Purchase / Cycle Progress Section *@
@if (_canRepurchase)
{
<MudItem xs="12">
<MudAlert Severity="Severity.Success" Icon="@Icons.Material.Filled.Celebration" Class="mb-0">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Style="width:100%">
<MudText Typo="Typo.body1">
🎉 چرخه جادویی تکمیل شد! می‌توانید پکیج جدید بخرید.
</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.ShoppingCart"
OnClick="@(() => Navigation.NavigateTo(RouteConstants.Package.List))">
خرید پکیج جدید
</MudButton>
</MudStack>
</MudAlert>
</MudItem>
}
else if (_magicStatus != null && _magicStatus.WalletMode == 1)
{
<MudItem xs="12">
<MudPaper Elevation="2" Class="pa-6">
<MudStack Spacing="3">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.AutoAwesome" Color="Color.Warning" />
<MudText Typo="Typo.h6">پیشرفت چرخه جادویی</MudText>
@if (_magicStatus.PurchaseCycleCount > 0)
{
<MudChip T="string" Size="Size.Small" Color="Color.Info">دور @(_magicStatus.PurchaseCycleCount + 1)</MudChip>
}
</MudStack>
<MudDivider />
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">واریز انجام‌شده:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(_magicStatus.MagicTotalDeposited)</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">سقف واریز:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(_magicStatus.MagicMaxDeposit)</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">باقیمانده:</MudText>
<MudText Typo="Typo.body2" Color="Color.Warning">@FormatPrice(_magicStatus.MagicRemainingDeposit)</MudText>
</MudStack>
<MudProgressLinear Color="Color.Primary" Value="@GetDepositProgress()" Class="my-2" Rounded="true" Size="Size.Large">
<MudText Typo="Typo.caption"><b>@GetDepositProgress()%</b></MudText>
</MudProgressLinear>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">اعتبار دریافتی:</MudText>
<MudText Typo="Typo.body2" Color="Color.Success">@FormatPrice(_magicStatus.MagicTotalCredited)</MudText>
</MudStack>
</MudStack>
</MudStack>
</MudPaper>
</MudItem>
}
<!-- Package Status Card -->
<MudItem xs="12" md="6">
<MudPaper Elevation="2" Class="pa-6 h-100">
@@ -118,7 +58,7 @@
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">نوع پکیج:</MudText>
<MudText Typo="Typo.body2">@_userStatus.PackageTitle</MudText>
<MudText Typo="Typo.body2">پکیج طلایی</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
@@ -215,7 +155,7 @@
<MudStack Spacing="3">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Stars" Color="Color.Warning" />
<MudText Typo="Typo.h6">مزایای پکیج</MudText>
<MudText Typo="Typo.h6">مزایای پکیج طلایی</MudText>
</MudStack>
<MudDivider />
@@ -240,7 +180,7 @@
<MudItem xs="12" sm="6" md="3">
<MudPaper Outlined="true" Class="pa-4 text-center">
<MudIcon Icon="@Icons.Material.Filled.ShoppingBag" Color="Color.Secondary" Size="Size.Large" />
<MudText Typo="Typo.subtitle2" Class="mt-2">فروشگاه اعتباری</MudText>
<MudText Typo="Typo.subtitle2" Class="mt-2">فروشگاه تخفیفی</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">خرید با تخفیف ویژه</MudText>
</MudPaper>
</MudItem>
@@ -9,9 +9,7 @@ public partial class MyPackages : ComponentBase
[Inject] private PackageService PackageService { get; set; } = default!;
private UserPackageStatusDto? _userStatus;
private MagicWalletStatus? _magicStatus;
private bool _isLoading = true;
private bool _canRepurchase = false;
private List<BreadcrumbItem> _breadcrumbItems = new()
{
@@ -32,18 +30,6 @@ public partial class MyPackages : ComponentBase
try
{
_userStatus = await PackageService.GetUserPackageStatusAsync();
if (_userStatus?.HasPurchasedPackage == true)
{
_magicStatus = await WalletService.GetMagicWalletStatusAsync();
// Cycle complete: wallet back to Normal mode, at least one cycle done,
// and deposit ceiling fully used (remaining = 0)
_canRepurchase = _magicStatus != null
&& _magicStatus.WalletMode == 0 // Normal mode
&& _magicStatus.PurchaseCycleCount >= 1 // At least one cycle completed
&& _magicStatus.MagicRemainingDeposit == 0; // Deposit ceiling used up
}
}
catch (Exception ex)
{
@@ -81,11 +67,4 @@ public partial class MyPackages : ComponentBase
{
return string.Format("{0:N0} تومان", price);
}
private int GetDepositProgress()
{
if (_magicStatus == null || _magicStatus.MagicMaxDeposit <= 0) return 0;
var pct = (int)((_magicStatus.MagicTotalDeposited * 100) / _magicStatus.MagicMaxDeposit);
return Math.Min(pct, 100);
}
}
@@ -3,12 +3,13 @@
<PageTitle>پکیج‌ها</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-8">
<PageHeader Title="پکیج‌های سرمایه‌گذاری" BackHref="@RouteConstants.Profile.Index" />
@* ── G1: راهنمای سیستم پکیج‌بیس (Q28) ── *@
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-4" Icon="@Icons.Material.Filled.Info">
هر پکیج ویژگی‌ها و مزایای منحصربه‌فردی دارد. با انتخاب پکیج مناسب، از سقف پاداش و ضریب کیف‌پول جادویی متفاوتی بهره‌مند می‌شوید.
</MudAlert>
<!-- Header -->
<MudStack Class="mb-6">
<MudText Typo="Typo.h4">پکیج‌های سرمایه‌گذاری</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
با خرید پکیج طلایی، به باشگاه مشتریان بپیوندید و از مزایای ویژه بهره‌مند شوید.
</MudText>
</MudStack>
@if (_isLoading)
{
@@ -32,7 +33,7 @@
{
<MudAlert Severity="Severity.Success" Class="mb-6" Icon="@Icons.Material.Filled.CheckCircle">
<MudText>
شما قبلاً پکیج را خریداری کرده‌اید.
شما قبلاً پکیج طلایی را خریداری کرده‌اید.
@if (_userStatus.IsClubMemberActive)
{
<span>عضویت باشگاه شما فعال است.</span>
@@ -69,43 +70,20 @@
<MudText Typo="Typo.h6" Color="Color.Success">@package.FormattedPrice</MudText>
</MudStack>
<!-- Package Highlights -->
<!-- Features Preview -->
<MudStack Spacing="1" Class="mt-2">
@if (package.SupportsDirectPurchase)
{
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.caption">پرداخت مستقیم</MudText>
</MudStack>
}
@if (package.SupportsDayaPurchase)
{
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.caption">پرداخت با اعتبار دایا</MudText>
</MudStack>
}
@if (package.DiscountMultiplier > 0)
{
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.caption">ضریب اعتبار: @package.DiscountMultiplier.ToString("F1")x</MudText>
</MudStack>
}
@if (package.MagicWalletMultiplier > 0)
{
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.caption">کیف پول جادویی: @package.MagicWalletMultiplier.ToString("F1")x</MudText>
</MudStack>
}
@if (package.IsBasePackage)
{
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Star" Size="Size.Small" Color="Color.Warning" />
<MudText Typo="Typo.caption">پکیج پایه</MudText>
</MudStack>
}
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.caption">عضویت در باشگاه مشتریان</MudText>
</MudStack>
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.caption">دریافت پاداش هفتگی</MudText>
</MudStack>
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.caption">تیم‌سازی نامحدود</MudText>
</MudStack>
</MudStack>
</MudStack>
</MudCardContent>
@@ -128,9 +106,9 @@
<MudGrid Spacing="4">
<MudItem xs="12" md="8">
<MudStack Spacing="2">
<MudText Typo="Typo.h5" Style="color: white;">چرا پکیج بخریم؟</MudText>
<MudText Typo="Typo.h5" Style="color: white;">چرا پکیج طلایی؟</MudText>
<MudText Typo="Typo.body1" Style="color: rgba(255,255,255,0.9);">
با خرید پکیج، علاوه بر دسترسی به محصولات ویژه، می‌توانید تیم فروش خود را بسازید
با خرید پکیج طلایی، علاوه بر دسترسی به محصولات ویژه، می‌توانید تیم فروش خود را بسازید
و از پاداش‌های هفتگی بهره‌مند شوید. هر چه تیم شما گسترده‌تر، درآمد شما بیشتر!
</MudText>
</MudStack>
+16 -15
View File
@@ -4,25 +4,26 @@
@if (_isLoading)
{
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<LoadingState Message="در حال بارگذاری..." />
</MudContainer>
<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>
}
else if (_package == null)
{
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<EmptyState Icon="@Icons.Material.Filled.Error"
Title="پکیج یافت نشد"
Description="پکیج مورد نظر وجود ندارد یا حذف شده است."
ActionText="بازگشت به صفحه اصلی"
ActionHref="@RouteConstants.Main.MainPage" />
</MudContainer>
<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>
}
else
{
<!-- Breadcrumb -->
<MudContainer MaxWidth="MaxWidth.Large" Class="py-4">
<PageHeader Title="جزئیات پکیج" BackHref="@RouteConstants.Package.List" />
<MudBreadcrumbs Items="_breadcrumbItems" />
</MudContainer>
@@ -35,9 +36,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);">
<AppImage Path="@_package.Image"
<MudImage Src="@_package.Image"
Alt="@_package.Title"
ImgHeight="250"
Height="250"
ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Style="width:100%"
@@ -227,9 +228,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)">
<AppImage Path="@relatedPackage.Image"
<MudImage Src="@relatedPackage.Image"
Alt="@relatedPackage.Title"
ImgHeight="200"
Height="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 CMSMicroservice.Protobuf.Protos.Package;
using FrontOffice.BFF.Package.Protobuf.Protos.Package;
using FrontOffice.Main.Shared;
using FrontOffice.Main.Utilities;
using Grpc.Core;
@@ -64,38 +64,36 @@ public partial class PackageDetail : IDisposable
try
{
// Load package details via Customer RPC
var response = await PackageClient.GetCustomerPackageDetailsAsync(
new GetCustomerPackageDetailsRequest { PackageId = Id },
cancellationToken: _loadCts.Token);
// Load package details
var packageResponse = await PackageClient.GetPackageAsync(request: new() { Id = Id}, cancellationToken: _loadCts.Token);
if (response != null)
if (packageResponse != null)
{
// Build features from API response
var features = response.Features?.Select(f => f.Title).ToList() ?? new List<string>();
// Build specifications from API features (highlighted ones)
var specs = response.Features?
.Where(f => f.IsHighlighted)
.Select(f => new Specification
{
Name = f.Title,
Value = f.Description,
Icon = string.IsNullOrEmpty(f.Icon) ? Icons.Material.Filled.Star : f.Icon
}).ToList() ?? new List<Specification>();
_package = new PackageDetailDto
{
Id = response.Id,
Title = response.Title,
Body = response.Description,
Image = response.ImagePath ?? string.Empty,
Specifications = specs,
Features = features,
Id = packageResponse.Id,
Title = packageResponse.Title,
Body = packageResponse.Description,
Image = UrlUtility.DownloadUrl + packageResponse.ImagePath,
Specifications = new List<Specification>
{
new() { Name = "ظرفیت", Value = "تا ۲۰۰ عضو", Icon = Icons.Material.Filled.Group },
new() { Name = "شجره‌نامه", Value = "پیشرفته", Icon = Icons.Material.Filled.AccountTree },
new() { Name = "گزارش‌گیری", Value = "جامع", Icon = Icons.Material.Filled.Analytics },
new() { Name = "پشتیبانی", Value = "۲۴ ساعته", Icon = Icons.Material.Filled.Support }
},
Features = new List<string>
{
"مدیریت تیم نامحدود",
"شجره‌نامه بصری",
"محاسبه کارمزد خودکار",
"گزارش‌های مالی",
"پشتیبانی اولویت‌دار"
},
Pricing = new PricingInfo
{
OriginalPrice = response.Price,
FinalPrice = response.Price,
OriginalPrice = packageResponse.Price,
FinalPrice = packageResponse.Price,
HasDiscount = false,
DiscountPercent = 0
}
@@ -4,7 +4,10 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="مدیریت آدرس‌ها" BackHref="@RouteConstants.Profile.Index" />
<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>
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="4">
@@ -1,5 +1,4 @@
using FrontOffice.Main.Utilities;
using CMSMicroservice.Protobuf.Protos.UserAddress;
using FrontOffice.BFF.UserAddress.Protobuf.Protos.UserAddress;
using FrontOffice.Main.Pages.Profile.Components;
using Microsoft.AspNetCore.Components;
using MudBlazor;
@@ -9,9 +8,8 @@ namespace FrontOffice.Main.Pages.Profile;
public partial class Addresses : ComponentBase
{
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!;
private List<CustomerAddressModel> _addresses = new();
private List<GetAllUserAddressByFilterResponseModel> _addresses = new();
private bool _isLoadingAddresses;
protected override async Task OnInitializedAsync()
@@ -24,7 +22,7 @@ public partial class Addresses : ComponentBase
_isLoadingAddresses = true;
try
{
var response = await UserAddressContract.GetCustomerAddressesAsync(new());
var response = await UserAddressContract.GetAllUserAddressByFilterAsync(new());
_addresses = response?.Models?.ToList() ?? new();
}
catch (Exception ex)
@@ -43,13 +41,10 @@ public partial class Addresses : ComponentBase
var dialog = await DialogService.ShowAsync<AddAddressDialog>("افزودن آدرس جدید");
var result = await dialog.Result;
if (result is not null && !result.Canceled)
{
await LoadAddresses();
await AuthService.RefreshTokenAsync();
}
}
private async Task OpenEditAddressDialog(CustomerAddressModel address)
private async Task OpenEditAddressDialog(GetAllUserAddressByFilterResponseModel address)
{
var dialog = await DialogService.ShowAsync<EditAddressDialog>("ویرایش آدرس", new DialogParameters<EditAddressDialog>
{
@@ -64,7 +59,7 @@ public partial class Addresses : ComponentBase
{
try
{
await UserAddressContract.SetCustomerDefaultAddressAsync(new() { Id = id });
await UserAddressContract.SetAddressAsDefaultAsync(new() { Id = id });
Snackbar.Add("آدرس پیش‌فرض تغییر کرد.", Severity.Success);
await LoadAddresses();
}
@@ -81,10 +76,9 @@ public partial class Addresses : ComponentBase
{
try
{
await UserAddressContract.DeleteCustomerAddressAsync(new() { Id = id });
await UserAddressContract.DeleteUserAddressAsync(new() { Id = id });
Snackbar.Add("آدرس حذف شد.", Severity.Success);
await LoadAddresses();
await AuthService.RefreshTokenAsync();
}
catch (Exception ex)
{
@@ -4,7 +4,10 @@
<MudContainer MaxWidth="MaxWidth.Small" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="تغییر رمز عبور" BackHref="@RouteConstants.Profile.Settings" />
<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>
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="3">
@@ -1,212 +0,0 @@
@attribute [Route(RouteConstants.Profile.ChargeCreditWallet)]
@attribute [Authorize]
<PageTitle>شارژ کیف‌پول اصلی</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="💳 شارژ کیف‌پول اصلی" BackHref="@RouteConstants.Profile.Wallet" />
@if (_paymentResult != null)
{
<MudAlert Severity="@(_paymentResult == "success" ? MudBlazor.Severity.Success : MudBlazor.Severity.Error)"
Class="rounded-lg" Variant="Variant.Filled">
@if (_paymentResult == "success")
{
<MudStack Spacing="2">
<span>شارژ کیف پول اصلی با موفقیت انجام شد! ✅</span>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Small"
StartIcon="@Icons.Material.Filled.ShoppingCart"
OnClick="ContinueShoppingAsync">
ادامه خرید
</MudButton>
</MudStack>
}
else if (_paymentResult == "cancelled")
{
<span>پرداخت توسط شما لغو شد.</span>
}
else
{
<span>پرداخت ناموفق بود. لطفاً دوباره تلاش کنید.</span>
}
</MudAlert>
}
@if (_authLoaded && _redirectingToMagic)
{
<MudAlert Severity="MudBlazor.Severity.Info" Class="rounded-lg" Variant="Variant.Outlined">
<MudStack Spacing="2" AlignItems="AlignItems.Center">
<MudProgressCircular Size="Size.Small" Indeterminate="true" Color="Color.Primary" />
<MudText>
کیف پول شما در حالت جادویی است. در حال انتقال به صفحه شارژ کیف پول جادویی...
</MudText>
<MudButton Size="Size.Small" Variant="Variant.Filled" Color="Color.Primary"
Href="@RouteConstants.Profile.MagicWallet"
StartIcon="@Icons.Material.Filled.AutoAwesome">
رفتن به شارژ کیف جادویی
</MudButton>
</MudStack>
</MudAlert>
}
else if (_authLoaded && !_isClubMemberActive)
{
<MudAlert Severity="MudBlazor.Severity.Warning" Class="rounded-lg" Variant="Variant.Outlined">
<MudStack Spacing="2">
<MudText>
برای شارژ کیف پول اصلی ابتدا باید پکیج را خریداری کرده و قرارداد باشگاه مشتریان را امضا کنید.
</MudText>
<MudStack Row="true" Spacing="1" Class="flex-wrap">
@if (!_hasPurchasedPackage)
{
<MudButton Size="Size.Small" Variant="Variant.Filled" Color="Color.Primary"
Href="@RouteConstants.Package.List"
StartIcon="@Icons.Material.Filled.CardGiftcard">
مشاهده پکیج‌ها
</MudButton>
}
else
{
<MudButton Size="Size.Small" Variant="Variant.Filled" Color="Color.Primary"
Href="@RouteConstants.Club.Membership"
StartIcon="@Icons.Material.Filled.Handshake">
امضای قرارداد باشگاه
</MudButton>
}
<MudButton Size="Size.Small" Variant="Variant.Text"
Href="@RouteConstants.Profile.Wallet">
بازگشت به کیف پول
</MudButton>
</MudStack>
</MudStack>
</MudAlert>
}
else if (_authLoaded && _needsMagicCeilingConsent && !_allowNormalDespiteMagic && !_redirectingToMagic)
{
<MudAlert Severity="MudBlazor.Severity.Warning" Class="rounded-lg" Variant="Variant.Outlined">
<MudStack Spacing="3">
<MudText Typo="Typo.subtitle1">
سقف شارژ کیف پول جادویی در این دور پر شده است.
</MudText>
<MudText Typo="Typo.body2">
شارژ از این صفحه بدون ضریب (۱:۱) به موجودی اضافه می‌شود و ضریب جادویی اعمال نمی‌شود.
برای ادامه، تأیید کنید.
</MudText>
<MudStack Row="true" Spacing="1" Class="flex-wrap">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Check"
OnClick="AcceptMagicCeilingConsent">
ادامه شارژ بدون ضریب
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.AutoAwesome"
OnClick="DeclineMagicCeilingConsent">
بازگشت به کیف جادویی
</MudButton>
</MudStack>
</MudStack>
</MudAlert>
}
else if (ShowChargeForm)
{
<MudPaper Elevation="2" Class="pa-5 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-3">
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Class="ml-1" />
شارژ موجودی اصلی
</MudText>
@if (_allowNormalDespiteMagic)
{
<MudAlert Severity="MudBlazor.Severity.Warning" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Warning">
این شارژ بدون ضریب جادویی است (۱:۱). سقف واریز جادویی شما در این دور پر شده است.
</MudAlert>
}
<MudAlert Severity="MudBlazor.Severity.Info" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info">
مبلغ وارد شده (به تومان) از طریق درگاه پرداخت به موجودی اصلی شما اضافه می‌شود.
از موجودی اصلی برای خرید از فروشگاه عادی و پرداخت سفارش استفاده می‌شود.
</MudAlert>
<MudStack Spacing="2">
<MudNumericField @bind-Value="_chargeAmount"
Label="مبلغ (تومان)"
Variant="Variant.Outlined"
Min="10_000"
Max="1_000_000_000"
Format="N0"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Payments"
HelperText="حداقل ۱۰,۰۰۰ و حداکثر ۱,۰۰۰,۰۰۰,۰۰۰ تومان" />
<MudStack Row="true" Spacing="1" Class="flex-wrap">
@foreach (var preset in _presetAmounts)
{
<MudButton Variant="Variant.Outlined" Size="Size.Small"
Color="@(_chargeAmount == preset ? Color.Primary : Color.Default)"
OnClick="() => _chargeAmount = preset"
Class="rounded-pill">
@FormatToman(preset)
</MudButton>
}
</MudStack>
@if (_chargeAmount > 0)
{
<MudPaper Outlined="true" Class="pa-3 rounded-lg" Style="background: #eff6ff;">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">مبلغ پرداخت:</MudText>
<MudText Typo="Typo.body2"><strong>@FormatToman(_chargeAmount)</strong></MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">موجودی دریافتی:</MudText>
<MudText Typo="Typo.body2" Color="Color.Primary"><strong>@FormatToman(_chargeAmount)</strong></MudText>
</MudStack>
</MudPaper>
}
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
FullWidth="true"
Disabled="@(_isProcessing || _chargeAmount < 10_000)"
OnClick="StartCreditCharge"
StartIcon="@Icons.Material.Filled.Payment"
Class="rounded-lg mt-2">
@if (_isProcessing)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="me-2" />
<span>در حال انتقال به درگاه...</span>
}
else
{
<span>پرداخت با کارت بانکی</span>
}
</MudButton>
</MudStack>
</MudPaper>
<MudExpansionPanels Elevation="1" Class="rounded-lg">
<MudExpansionPanel Text="قوانین شارژ کیف پول اصلی" MaxHeight="500" IsInitiallyExpanded="false">
<MudList T="string" Dense="true">
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
موجودی اصلی برای خرید از فروشگاه عادی و پرداخت سفارش قابل استفاده است
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
حداقل مبلغ شارژ: ۱۰,۰۰۰ تومان
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
مبلغ واریزی بدون ضریب به موجودی اضافه می‌شود (۱:۱)
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.Info" IconColor="Color.Info">
پرداخت از طریق درگاه بانکی انجام می‌شود
</MudListItem>
</MudList>
</MudExpansionPanel>
</MudExpansionPanels>
}
</MudStack>
</MudContainer>
@@ -1,199 +0,0 @@
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor;
namespace FrontOffice.Main.Pages.Profile;
public partial class ChargeCreditWallet : ComponentBase
{
[Inject] private AuthService AuthService { get; set; } = default!;
private bool _isProcessing;
private long _chargeAmount;
private string? _paymentResult;
private string? _pendingReturnUrl;
private bool _isClubMemberActive;
private bool _hasPurchasedPackage;
private bool _authLoaded;
private bool _redirectingToMagic;
private bool _allowNormalDespiteMagic;
private bool _needsMagicCeilingConsent;
private readonly long[] _presetAmounts = { 500_000, 1_000_000, 5_000_000, 10_000_000, 20_000_000, 50_000_000 };
[SupplyParameterFromQuery(Name = "payment")]
public string? PaymentQueryParam { get; set; }
[SupplyParameterFromQuery(Name = "amount")]
public long? AmountQueryParam { get; set; }
[SupplyParameterFromQuery(Name = "returnUrl")]
public string? ReturnUrlQueryParam { get; set; }
private bool ShowChargeForm =>
_authLoaded
&& _isClubMemberActive
&& !_redirectingToMagic
&& (!_needsMagicCeilingConsent || _allowNormalDespiteMagic);
protected override async Task OnInitializedAsync()
{
_paymentResult = PaymentQueryParam;
if (AmountQueryParam is > 0)
_chargeAmount = CreditChargeNavigation.NormalizeChargeAmount(AmountQueryParam.Value);
if (CreditChargeNavigation.IsValidReturnUrl(ReturnUrlQueryParam))
_pendingReturnUrl = ReturnUrlQueryParam;
try
{
var userInfo = await AuthService.GetUserAuthInfo();
_isClubMemberActive = userInfo.IsClubMemberActive;
_hasPurchasedPackage = userInfo.HasPurchasedPackage;
}
catch
{
_isClubMemberActive = false;
_hasPurchasedPackage = false;
}
finally
{
_authLoaded = true;
}
if (_isClubMemberActive)
await EvaluateMagicWalletGateAsync();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && !string.IsNullOrEmpty(_pendingReturnUrl) && _isClubMemberActive && !_redirectingToMagic)
await CreditChargeNavigation.SaveReturnUrlAsync(JSRuntime, _pendingReturnUrl);
await base.OnAfterRenderAsync(firstRender);
}
private void AcceptMagicCeilingConsent()
{
_allowNormalDespiteMagic = true;
_redirectingToMagic = false;
}
private void DeclineMagicCeilingConsent()
{
_redirectingToMagic = true;
Navigation.NavigateTo(RouteConstants.Profile.MagicWallet);
}
private async Task StartCreditCharge()
{
if (!_isClubMemberActive)
{
Snackbar.Add(
"برای شارژ کیف پول اصلی ابتدا باید پکیج را خریداری کرده و قرارداد باشگاه مشتریان را امضا کنید.",
Severity.Warning);
return;
}
if (await EvaluateMagicWalletGateAsync())
return;
if (_needsMagicCeilingConsent && !_allowNormalDespiteMagic)
return;
if (_chargeAmount <= 0 || _isProcessing) return;
_isProcessing = true;
StateHasChanged();
try
{
if (!string.IsNullOrEmpty(_pendingReturnUrl))
await CreditChargeNavigation.SaveReturnUrlAsync(JSRuntime, _pendingReturnUrl);
else
{
var storedReturnUrl = await CreditChargeNavigation.GetReturnUrlAsync(JSRuntime);
if (CreditChargeNavigation.IsValidReturnUrl(storedReturnUrl))
_pendingReturnUrl = storedReturnUrl;
}
var (success, gatewayUrl, error) = await WalletService.InitiateCreditChargeAsync(_chargeAmount);
if (success && !string.IsNullOrEmpty(gatewayUrl))
{
Navigation.NavigateTo(gatewayUrl, forceLoad: true);
}
else
{
Snackbar.Add(error ?? "خطا در ایجاد درخواست پرداخت", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_isProcessing = false;
StateHasChanged();
}
}
/// <summary>
/// Magic با ظرفیت باقی‌مانده → ریدایرکت به MagicWallet.
/// Magic با سقف پر → نمایش تأیید روی صفحه برای شارژ ۱:۱.
/// true یعنی مسیر شارژ فعلی باید متوقف شود.
/// </summary>
private async Task<bool> EvaluateMagicWalletGateAsync()
{
if (_allowNormalDespiteMagic)
return false;
try
{
var status = await WalletService.GetMagicWalletStatusAsync();
if (status.WalletMode != 1)
{
_needsMagicCeilingConsent = false;
return false;
}
if (status.MagicRemainingDeposit > 0)
{
var multiplier = status.MagicMultiplier > 0 ? status.MagicMultiplier : 2.5;
var multiplierText = multiplier.ToString("0.#");
_redirectingToMagic = true;
_needsMagicCeilingConsent = false;
Snackbar.Add(
$"کیف پول شما در حالت جادویی است. شارژ از این صفحه موجودی را چندبرابر نمی‌کند. برای دریافت ضریب جادویی (×{multiplierText}) به صفحه شارژ کیف پول جادویی هدایت می‌شوید.",
Severity.Info);
Navigation.NavigateTo(RouteConstants.Profile.MagicWallet);
return true;
}
_needsMagicCeilingConsent = true;
return !_allowNormalDespiteMagic;
}
catch
{
// در صورت خطا در خواندن وضعیت، مانع شارژ عادی نشو
return false;
}
}
private async Task ContinueShoppingAsync()
{
var returnUrl = await CreditChargeNavigation.GetReturnUrlAsync(JSRuntime);
await CreditChargeNavigation.ClearReturnUrlAsync(JSRuntime);
if (CreditChargeNavigation.IsValidReturnUrl(returnUrl))
Navigation.NavigateTo(returnUrl!);
}
private static string FormatToman(long toman)
=> string.Format("{0:N0} تومان", toman);
}
@@ -1,121 +0,0 @@
@attribute [Route(RouteConstants.Profile.ChargeDiscountWallet)]
@attribute [Authorize]
<PageTitle>شارژ کیف‌پول اعتباری</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="💳 شارژ کیف‌پول اعتباری" BackHref="@RouteConstants.Profile.Wallet" />
@* ─── نتیجه پرداخت (اگه از callback برگشته) ─── *@
@if (_paymentResult != null)
{
<MudAlert Severity="@(_paymentResult == "success" ? MudBlazor.Severity.Success : MudBlazor.Severity.Error)"
Class="rounded-lg" Variant="Variant.Filled">
@if (_paymentResult == "success")
{
<span>شارژ اعتباری با موفقیت انجام شد! ✅</span>
}
else if (_paymentResult == "cancelled")
{
<span>پرداخت توسط شما لغو شد.</span>
}
else
{
<span>پرداخت ناموفق بود. لطفاً دوباره تلاش کنید.</span>
}
</MudAlert>
}
@* ─── فرم شارژ ─── *@
<MudPaper Elevation="2" Class="pa-5 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-3">
<MudIcon Icon="@Icons.Material.Filled.CreditCard" Class="ml-1" />
شارژ موجودی اعتباری
</MudText>
<MudAlert Severity="MudBlazor.Severity.Info" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info">
مبلغ وارد شده (به تومان) از طریق درگاه پرداخت به موجودی اعتباری شما اضافه می‌شود.
از موجودی اعتباری فقط برای خرید از فروشگاه اعتباری می‌توانید استفاده کنید.
</MudAlert>
<MudStack Spacing="2">
<MudNumericField @bind-Value="_chargeAmount"
Label="مبلغ (تومان)"
Variant="Variant.Outlined"
Min="10_000"
Max="1_000_000_000"
Format="N0"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Payments"
HelperText="حداقل ۱۰,۰۰۰ و حداکثر ۱,۰۰۰,۰۰۰,۰۰۰ تومان" />
@* دکمه‌های مبلغ سریع *@
<MudStack Row="true" Spacing="1" Class="flex-wrap">
@foreach (var preset in _presetAmounts)
{
<MudButton Variant="Variant.Outlined" Size="Size.Small"
Color="@(_chargeAmount == preset ? Color.Primary : Color.Default)"
OnClick="() => _chargeAmount = preset"
Class="rounded-pill">
@FormatToman(preset)
</MudButton>
}
</MudStack>
@if (_chargeAmount > 0)
{
<MudPaper Outlined="true" Class="pa-3 rounded-lg" Style="background: #f0fdf4;">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">مبلغ پرداخت:</MudText>
<MudText Typo="Typo.body2"><strong>@FormatToman(_chargeAmount)</strong></MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">اعتبار دریافتی:</MudText>
<MudText Typo="Typo.body2" Color="Color.Success"><strong>@FormatToman(_chargeAmount)</strong></MudText>
</MudStack>
</MudPaper>
}
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
FullWidth="true"
Disabled="@(_isProcessing || _chargeAmount < 10_000)"
OnClick="StartDiscountCharge"
StartIcon="@Icons.Material.Filled.Payment"
Class="rounded-lg mt-2">
@if (_isProcessing)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="me-2" />
<span>در حال انتقال به درگاه...</span>
}
else
{
<span>پرداخت با کارت بانکی</span>
}
</MudButton>
</MudStack>
</MudPaper>
@* ─── قوانین ─── *@
<MudExpansionPanels Elevation="1" Class="rounded-lg">
<MudExpansionPanel Text="قوانین شارژ اعتباری" MaxHeight="500" IsInitiallyExpanded="false">
<MudList T="string" Dense="true">
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
موجودی اعتباری فقط قابل استفاده در فروشگاه اعتباری است
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
حداقل مبلغ شارژ: ۱۰,۰۰۰ تومان
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
مبلغ واریزی بدون ضریب به موجودی اضافه می‌شود (۱:۱)
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.Info" IconColor="Color.Info">
پرداخت از طریق درگاه بانکی انجام می‌شود
</MudListItem>
</MudList>
</MudExpansionPanel>
</MudExpansionPanels>
</MudStack>
</MudContainer>
@@ -1,57 +0,0 @@
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace FrontOffice.Main.Pages.Profile;
public partial class ChargeDiscountWallet : ComponentBase
{
private bool _isProcessing;
private long _chargeAmount;
private string? _paymentResult;
private readonly long[] _presetAmounts = { 500_000, 1_000_000, 5_000_000, 10_000_000, 20_000_000, 50_000_000 };
[SupplyParameterFromQuery(Name = "payment")]
public string? PaymentQueryParam { get; set; }
protected override void OnInitialized()
{
_paymentResult = PaymentQueryParam;
}
private async Task StartDiscountCharge()
{
if (_chargeAmount <= 0 || _isProcessing) return;
_isProcessing = true;
StateHasChanged();
try
{
// مبلغ به تومان — CMS خودش موقع ارسال به درگاه ×۱۰ می‌کنه
var (success, gatewayUrl, error) = await WalletService.InitiateDiscountChargeAsync(_chargeAmount);
if (success && !string.IsNullOrEmpty(gatewayUrl))
{
Navigation.NavigateTo(gatewayUrl, forceLoad: true);
}
else
{
Snackbar.Add(error ?? "خطا در ایجاد درخواست پرداخت", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_isProcessing = false;
StateHasChanged();
}
}
private static string FormatToman(long toman)
=> string.Format("{0:N0} تومان", toman);
}
@@ -1,11 +1,10 @@
@using CMSMicroservice.Protobuf.Protos.City
<MudDialog>
<MudDialog >
<TitleContent>
<MudText Typo="Typo.h4" Align="Align.Center">افزودن آدرس جدید</MudText>
</TitleContent>
<DialogContent>
<MudForm @ref="_form" Model="_request">
<MudForm @ref="_form" Model="_request" Validation="@(_validator.ValidateValue)">
<MudStack Spacing="3">
<MudTextField @bind-Value="_request.Title"
For="@(() => _request.Title)"
@@ -30,12 +29,12 @@
Required="true"
RequiredError="کد پستی الزامی است." />
<MudAutocomplete T="object"
<MudAutocomplete T="CityProto.GetAllCitiesByFilterResponseModel"
@bind-Value="_selectedCity"
Label="شهر"
Variant="Variant.Outlined"
SearchFunc="SearchCities"
ToStringFunc="@(city => city != null ? $"{((CMSMicroservice.Protobuf.Protos.City.CityDto)city).Native} ({((CMSMicroservice.Protobuf.Protos.City.CityDto)city).StateName})" : string.Empty)"
ToStringFunc="@(city => city != null ? $"{city.Native} ({city.StateName})" : string.Empty)"
Required="true"
RequiredError="شهر الزامی است."
Clearable="true"
@@ -50,8 +49,8 @@
<ItemTemplate Context="city">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.LocationCity" Size="Size.Small" />
<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>
<MudText>@city.Native</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">(@city.StateName)</MudText>
</MudStack>
</ItemTemplate>
<NoItemsTemplate>
@@ -1,42 +1,43 @@
using CMSMicroservice.Protobuf.Protos.UserAddress;
using CMSMicroservice.Protobuf.Protos.City;
using FrontOffice.BFF.UserAddress.Protobuf.Protos.UserAddress;
using FrontOffice.BFF.UserAddress.Protobuf.Validator;
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 CityContract.CityContractClient CityContract { get; set; } = default!;
[Inject] private CityProto.CityContract.CityContractClient CityContract { get; set; } = default!;
private MudForm? _form;
private readonly CreateNewUserAddressRequestValidator _validator = new();
private bool _isSaving;
private CreateCustomerAddressRequest _request = new();
private object? _selectedCity;
private CreateNewUserAddressRequest _request = new();
private CityProto.GetAllCitiesByFilterResponseModel? _selectedCity;
private async Task<IEnumerable<object>> SearchCities(string value, CancellationToken ct)
private async Task<IEnumerable<CityProto.GetAllCitiesByFilterResponseModel>> SearchCities(string value,
CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(value) || value.Length < 2)
return Enumerable.Empty<object>();
return Enumerable.Empty<CityProto.GetAllCitiesByFilterResponseModel>();
try
{
var response = await CityContract.GetAllCitiesByFilterAsync(new GetAllCitiesByFilterRequest
var response = await CityContract.GetAllCitiesByFilterAsync(new CityProto.GetAllCitiesByFilterRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.City.PaginationState { PageNumber = 1, PageSize = 20 },
Filter = new GetAllCitiesByFilterFilter { Name = value }
});
return response?.Cities?.Cast<object>() ?? Enumerable.Empty<object>();
PaginationState = new CityProto.PaginationState { PageNumber = 1, PageSize = 20 },
Filter = new CityProto.GetAllCitiesByFilterFilter { Name = value }
}, cancellationToken: ct);
return response?.Models?.ToList() ?? new List<CityProto.GetAllCitiesByFilterResponseModel>();
}
catch
{
return Enumerable.Empty<object>();
return Enumerable.Empty<CityProto.GetAllCitiesByFilterResponseModel>();
}
}
@@ -52,11 +53,11 @@ public partial class AddAddressDialog : ComponentBase
return;
}
_request.CityId = (long)((dynamic)_selectedCity).Id;
_request.CityId = _selectedCity.Id;
_isSaving = true;
try
{
var response = await UserAddressContract.CreateCustomerAddressAsync(_request);
var response = await UserAddressContract.CreateNewUserAddressAsync(_request);
Snackbar.Add("آدرس با موفقیت اضافه شد.", Severity.Success);
MudDialog.Close(DialogResult.Ok(true));
}
@@ -1,10 +1,9 @@
@using CMSMicroservice.Protobuf.Protos.City
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h4" Align="Align.Center">ویرایش آدرس</MudText>
</TitleContent>
<DialogContent>
<MudForm @ref="_form" Model="_request">
<MudForm @ref="_form" Model="_request" Validation="@(_validator.ValidateValue)">
<MudStack Spacing="3">
<MudTextField @bind-Value="_request.Title"
For="@(() => _request.Title)"
@@ -29,12 +28,12 @@
Required="true"
RequiredError="کد پستی الزامی است." />
<MudAutocomplete T="object"
<MudAutocomplete T="CityProto.GetAllCitiesByFilterResponseModel"
@bind-Value="_selectedCity"
Label="شهر"
Variant="Variant.Outlined"
SearchFunc="SearchCities"
ToStringFunc="@(city => city != null ? $"{((CMSMicroservice.Protobuf.Protos.City.CityDto)city).Native} ({((CMSMicroservice.Protobuf.Protos.City.CityDto)city).StateName})" : string.Empty)"
ToStringFunc="@(city => city != null ? $"{city.Native} ({city.StateName})" : string.Empty)"
Required="true"
RequiredError="شهر الزامی است."
Clearable="true"
@@ -49,8 +48,8 @@
<ItemTemplate Context="city">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.LocationCity" Size="Size.Small" />
<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>
<MudText>@city.Native</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">(@city.StateName)</MudText>
</MudStack>
</ItemTemplate>
<NoItemsTemplate>
@@ -1,9 +1,10 @@
using CMSMicroservice.Protobuf.Protos.UserAddress;
using CMSMicroservice.Protobuf.Protos.City;
using FrontOffice.BFF.UserAddress.Protobuf.Protos.UserAddress;
using FrontOffice.BFF.UserAddress.Protobuf.Validator;
using Mapster;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using Severity = MudBlazor.Severity;
using CityProto = FrontOffice.BFF.City.Protobuf;
namespace FrontOffice.Main.Pages.Profile.Components;
@@ -11,21 +12,22 @@ public partial class EditAddressDialog : ComponentBase
{
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = default!;
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
[Inject] private CityContract.CityContractClient CityContract { get; set; } = default!;
[Inject] private CityProto.CityContract.CityContractClient CityContract { get; set; } = default!;
[Parameter] public CustomerAddressModel? Model { get; set; }
[Parameter] public GetAllUserAddressByFilterResponseModel? Model { get; set; }
private MudForm? _form;
private readonly UpdateUserAddressRequestValidator _validator = new();
private bool _isSaving;
private UpdateCustomerAddressRequest _request = new();
private object? _selectedCity;
private UpdateUserAddressRequest _request = new();
private CityProto.GetAllCitiesByFilterResponseModel? _selectedCity;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
if (Model != null)
{
_request = Model.Adapt<UpdateCustomerAddressRequest>();
_request = Model.Adapt<UpdateUserAddressRequest>();
// Load selected city if CityId exists
if (Model.CityId > 0)
@@ -39,13 +41,13 @@ public partial class EditAddressDialog : ComponentBase
{
try
{
var response = await CityContract.GetAllCitiesByFilterAsync(new GetAllCitiesByFilterRequest
var response = await CityContract.GetAllCitiesByFilterAsync(new CityProto.GetAllCitiesByFilterRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.City.PaginationState { PageNumber = 1, PageSize = 1 },
Filter = new GetAllCitiesByFilterFilter { Id = cityId }
PaginationState = new CityProto.PaginationState { PageNumber = 1, PageSize = 1 },
Filter = new CityProto.GetAllCitiesByFilterFilter { Id = cityId }
});
_selectedCity = response?.Cities?.FirstOrDefault();
_selectedCity = response?.Models?.FirstOrDefault();
}
catch
{
@@ -53,24 +55,24 @@ public partial class EditAddressDialog : ComponentBase
}
}
private async Task<IEnumerable<object>> SearchCities(string value, CancellationToken ct)
private async Task<IEnumerable<CityProto.GetAllCitiesByFilterResponseModel>> SearchCities(string value, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(value) || value.Length < 2)
return Enumerable.Empty<object>();
return Enumerable.Empty<CityProto.GetAllCitiesByFilterResponseModel>();
try
{
var response = await CityContract.GetAllCitiesByFilterAsync(new GetAllCitiesByFilterRequest
var response = await CityContract.GetAllCitiesByFilterAsync(new CityProto.GetAllCitiesByFilterRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.City.PaginationState { PageNumber = 1, PageSize = 20 },
Filter = new GetAllCitiesByFilterFilter { Name = value }
});
return response?.Cities?.Cast<object>() ?? Enumerable.Empty<object>();
PaginationState = new CityProto.PaginationState { PageNumber = 1, PageSize = 20 },
Filter = new CityProto.GetAllCitiesByFilterFilter { Native = value }
}, cancellationToken: ct);
return response?.Models?.ToList() ?? new List<CityProto.GetAllCitiesByFilterResponseModel>();
}
catch
{
return Enumerable.Empty<object>();
return Enumerable.Empty<CityProto.GetAllCitiesByFilterResponseModel>();
}
}
@@ -86,11 +88,11 @@ public partial class EditAddressDialog : ComponentBase
return;
}
_request.CityId = (long)((dynamic)_selectedCity).Id;
_request.CityId = _selectedCity.Id;
_isSaving = true;
try
{
await UserAddressContract.UpdateCustomerAddressAsync(_request);
await UserAddressContract.UpdateUserAddressAsync(_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 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>
<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>
</MudSelect>
<MudButtonGroup Variant="Variant.Outlined" Size="Size.Small" OverrideStyles="false">
@@ -61,11 +61,11 @@
<span class="stat-value">@_statistics.TotalMembers</span>
</div>
<div class="stat-item">
<span class="stat-label">سازمان چپ:</span>
<span class="stat-label">پای چپ:</span>
<span class="stat-value left">@_statistics.LeftLegCount</span>
</div>
<div class="stat-item">
<span class="stat-label">سازمان راست:</span>
<span class="stat-label">پای راست:</span>
<span class="stat-value right">@_statistics.RightLegCount</span>
</div>
<div class="stat-item">
@@ -1,4 +1,4 @@
using FrontOffice.Main.Utilities;
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
@@ -95,27 +95,7 @@ public partial class OrganizationChart : IAsyncDisposable
try
{
_dotNetHelper = DotNetObjectReference.Create(this);
// Explicit camelCase mapping (same pattern as BackOffice admin chart)
var flatData = _networkTree.ToFlatArray().Select(n => new
{
id = n.Id,
parentId = n.ParentId,
fullName = n.FullName,
mobile = n.Mobile,
avatar = n.Avatar,
position = n.Position,
level = n.Level,
isActive = n.IsActive,
isClubActive = n.IsClubActive,
activationWeekNumber = n.ActivationWeekNumber,
joinedAt = n.JoinedAt,
referralCode = n.ReferralCode,
packageName = n.PackageName ?? string.Empty,
goldLeftLegTotal = n.GoldLeftLegTotal,
goldRightLegTotal = n.GoldRightLegTotal,
silverLeftLegTotal = n.SilverLeftLegTotal,
silverRightLegTotal = n.SilverRightLegTotal
}).ToArray();
var flatData = _networkTree.ToFlatArray();
await JSRuntime.InvokeVoidAsync("OrgChart.init", "org-chart-container", flatData, _dotNetHelper);
}
@@ -9,9 +9,9 @@
@if (!string.IsNullOrWhiteSpace(node.Avatar))
{
<MudAvatar Size="@GetAvatarSize(Level)">
<AppImage Path="@node.Avatar"
ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center" />
<MudImage ObjectFit="ObjectFit.Cover"
ObjectPosition="ObjectPosition.Center"
Src="@node.Avatar" />
</MudAvatar>
}
else
@@ -1,64 +0,0 @@
@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>
@@ -1,34 +0,0 @@
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();
}
}
+287 -180
View File
@@ -5,194 +5,301 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudGrid Spacing="4">
@* ═══════════════════════════════════════════════
1. PROFILE HEADER — Modern gradient card
═══════════════════════════════════════════════ *@
<!-- Profile Header -->
<MudItem xs="12">
<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>
<MudButton Variant="Variant.Filled"
Class="rounded-pill dash-hero-btn"
Href="https://dayadiamond.ir/"
StartIcon="@Icons.Material.Filled.Diamond">
سامانه دایا
</MudButton>
</MudStack>
</MudPaper>
</MudItem>
@* ═══════════════════════════════════════════════
2. WALLET STRIP — Compact inline stats
═══════════════════════════════════════════════ *@
<MudItem xs="12">
<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>
@* ═══════════════════════════════════════════════
3. REFERRAL SECTION
═══════════════════════════════════════════════ *@
<MudItem xs="12">
@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" />
<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>
</MudPaper>
</MudLink>
</MudItem>
}
<MudSpacer/>
<MudButton Variant="Variant.Outlined" Href="https://dayadiamond.ir/" >سامانه دایا</MudButton>
</MudStack>
@* ═══════════════════════════════════════════════
4. QUICK ACCESS TILES — Modern grid
═══════════════════════════════════════════════ *@
<!-- 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>
}
</MudStack>
</MudPaper>
</MudItem>
<!-- Wallet Highlight Row -->
<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>
<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>
</MudItem>
<!-- Profile Content as tiles -->
<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>
}
</MudGrid>
<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>
</MudItem>
</MudGrid>
</MudContainer>
<!-- Purchase Options Bottom Sheet -->
<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">
پرداخت با کارت بانکی(بزودی)
</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">
تأمین اعتبار الماسی
</MudButton>
</MudStack>
</MudPaper>
<MudButton Variant="Variant.Text"
Color="Color.Default"
FullWidth="true"
OnClick="ClosePurchaseOptions">
بستن
</MudButton>
</MudStack>
</DialogContent>
</MudDialog>
+57 -121
View File
@@ -1,5 +1,9 @@
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;
@@ -7,11 +11,6 @@ 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;
@@ -22,11 +21,10 @@ 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 GetUserForCustomerResponse _userProfile = new();
private BlogPostCardDto? _latestPost;
private UpdateCustomerProfileRequest _updateUserRequest = new();
private GetUserResponse _userProfile = new();
private UpdateUserRequest _updateUserRequest = new();
private readonly UpdateUserRequestValidator _personalValidator = new();
private MudForm? _personalForm;
@@ -42,29 +40,12 @@ public partial class Index
private bool _isClubMemberActive;
private bool CanShowReferralLink => _hasPurchasedPackage && _isClubMemberActive;
// Purchase flow
// Purchase Options Bottom Sheet
private bool _showPurchaseBottomSheet;
private bool _isProcessingPayment;
private int _purchaseCycleCount; // تعداد دورهای خرید — اگر > 0 فقط IPG مجاز
// Dashboard Tiles
private readonly List<DashTile> _dashTiles = new()
{
new(RouteConstants.Profile.Personal, Icons.Material.Filled.Person, "اطلاعات شخصی", "نمایش و ویرایش", "background:rgba(99,102,241,.12); color:#6366f1;"),
new(RouteConstants.Profile.Addresses, Icons.Material.Filled.LocationOn, "آدرس‌ها", "مدیریت آدرس‌ها", "background:rgba(99,102,241,.12); color:#6366f1;"),
new(RouteConstants.Profile.Tree, Icons.Material.Filled.AccountTree, "سازمان فروش", "ساختار تیم", "background:rgba(99,102,241,.12); color:#6366f1;"),
new(RouteConstants.Gateway.StoreChooser, Icons.Material.Filled.Store, "فروشگاه‌ها", "ورود به فروشگاه", "background:rgba(16,185,129,.12); color:#10b981;"),
new(RouteConstants.Profile.Wallet, Icons.Material.Filled.AccountBalanceWallet, "کیف پول", "مدیریت و تاریخچه", "background:rgba(16,185,129,.12); color:#10b981;"),
new(RouteConstants.Profile.MagicWallet, Icons.Material.Filled.AutoAwesome, "کیف‌پول جادویی", "شارژ چند‌برابری", "background:rgba(168,85,247,.12); color:#a855f7;"),
new(RouteConstants.Profile.WithdrawalRequests, Icons.Material.Filled.RequestPage, "درخواست برداشت", "ثبت و پیگیری", "background:rgba(245,158,11,.12); color:#f59e0b;"),
new(RouteConstants.Club.Membership, Icons.Material.Filled.CardMembership, "باشگاه مشتریان", "عضویت و مزایا", "background:rgba(168,85,247,.12); color:#a855f7;"),
new(RouteConstants.Network.Statistics, Icons.Material.Filled.Groups, "آمار باشگاه", "جزئیات باشگاه", "background:rgba(59,130,246,.12); color:#3b82f6;"),
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<CustomerAddressModel> _addresses = new();
private List<GetAllUserAddressByFilterResponseModel> _addresses = new();
private bool _isLoadingAddresses;
private CreateNewUserAddressRequest _newAddressRequest = new();
private UpdateUserAddressRequest _editAddressRequest = new();
@@ -82,7 +63,6 @@ public partial class Index
await LoadUserProfile();
await LoadAddresses();
await LoadWallet();
await LoadLatestPost();
// نمایش Modal قرارداد باشگاه اگر پکیج خریده ولی باشگاه فعال نشده
await CheckAndShowClubContractModal();
@@ -148,57 +128,24 @@ public partial class Index
var discount = FormatPrice(b.DiscountBalance);
_walletNetwork = FormatPrice(b.NetworkBalance);
_walletDiscount = discount;
// بارگذاری تعداد دورهای خرید برای کنترل روش‌های پرداخت
var magicStatus = await WalletService.GetMagicWalletStatusAsync();
_purchaseCycleCount = magicStatus.PurchaseCycleCount;
StateHasChanged();
}
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.GetUserForCustomerAsync(new());
_updateUserRequest = new UpdateCustomerProfileRequest
{
FirstName = _userProfile.FirstName,
LastName = _userProfile.LastName,
Email = _userProfile.Email,
NationalCode = _userProfile.NationalCode,
};
_userProfile = await UserContract.GetUserAsync(request: new());
_updateUserRequest = _userProfile.Adapt<UpdateUserRequest>();
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 GetUserForCustomerResponse();
_userProfile = new GetUserResponse();
}
StateHasChanged();
}
@@ -220,7 +167,7 @@ public partial class Index
if (_date != null)
_updateUserRequest.BirthDate = _date.Value.DateTimeToTimestamp();
await UserContract.UpdateCustomerProfileAsync(request: _updateUserRequest);
await UserContract.UpdateUserAsync(request: _updateUserRequest);
await LoadUserProfile();
Snackbar.Add("اطلاعات شخصی با موفقیت ذخیره شد.", Severity.Success);
}
@@ -303,20 +250,20 @@ public partial class Index
_isLoadingAddresses = true;
try
{
var response = await UserAddressContract.GetCustomerAddressesAsync(request: new());
var response = await UserAddressContract.GetAllUserAddressByFilterAsync(request: new());
if (response?.Models?.Any() == true)
{
_addresses = response.Models.ToList();
}
else
{
_addresses = new List<CustomerAddressModel>();
_addresses = new List<GetAllUserAddressByFilterResponseModel>();
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری آدرس‌ها: {ex.Message}", Severity.Error);
_addresses = new List<CustomerAddressModel>();
_addresses = new List<GetAllUserAddressByFilterResponseModel>();
}
finally
{
@@ -333,11 +280,10 @@ public partial class Index
if (!result.Canceled)
{
await LoadAddresses();
await AuthService.RefreshTokenAsync();
}
}
private async Task OpenEditAddressDialog(CustomerAddressModel address)
private async Task OpenEditAddressDialog(GetAllUserAddressByFilterResponseModel address)
{
var dialog = await DialogService.ShowAsync<EditAddressDialog>("ویرایش آدرس", new DialogParameters<EditAddressDialog>
{
@@ -386,7 +332,6 @@ public partial class Index
});
Snackbar.Add("آدرس با موفقیت حذف شد.", Severity.Success);
await LoadAddresses();
await AuthService.RefreshTokenAsync();
}
catch (Exception ex)
{
@@ -395,62 +340,41 @@ public partial class Index
}
}
private async Task OpenPurchaseOptions()
private void OpenPurchaseOptions()
{
var options = new DialogOptions
{
MaxWidth = MaxWidth.Small,
FullWidth = true,
CloseOnEscapeKey = true,
BackdropClick = true
};
var parameters = new DialogParameters<Shared.PackagePurchaseDialog>
{
{ x => x.PurchaseCycleCount, _purchaseCycleCount }
};
var dialog = await DialogService.ShowAsync<Shared.PackagePurchaseDialog>(
string.Empty, parameters, options);
var result = await dialog.Result;
if (result.Canceled) return;
if (result.Data is Shared.PackagePurchaseDialog.PackagePurchaseResult purchase)
{
switch (purchase.Method)
{
case Shared.PackagePurchaseDialog.PaymentMethodType.DirectPayment:
await ProcessDirectPayment(purchase.Package.Id);
break;
case Shared.PackagePurchaseDialog.PaymentMethodType.DayaLoan:
await ProcessDayaLoanPayment();
break;
}
}
_showPurchaseBottomSheet = true;
}
private async Task ProcessDirectPayment(long packageId)
private void ClosePurchaseOptions()
{
_showPurchaseBottomSheet = false;
}
private async Task DirectPayment()
{
if (_isProcessingPayment) return;
_isProcessingPayment = true;
StateHasChanged();
_showPurchaseBottomSheet = false;
try
{
var response = await PackageContract.CustomerPurchasePackageAsync(new CustomerPurchasePackageRequest
// Create callback URL for payment verification
var callbackUrl = $"{Navigation.BaseUri}profile/payment-callback";
// Call BFF to initiate payment (UserId is taken from JWT token in BFF)
var response = await PackageContract.InitiateBasePackagePaymentAsync(new InitiateBasePackagePaymentRequest
{
PackageId = packageId,
PurchaseMethod = PurchaseMethodEnum.PurchaseMethodGateway
CallbackUrl = callbackUrl
});
if (!response.Success)
{
Snackbar.Add(response.Message ?? "خطا در آغاز فرآیند پرداخت", Severity.Error);
return;
}
// Redirect to payment gateway
if (!string.IsNullOrEmpty(response.PaymentGatewayUrl))
{
Navigation.NavigateTo(response.PaymentGatewayUrl, forceLoad: true);
@@ -471,29 +395,41 @@ public partial class Index
}
}
private async Task ProcessDayaLoanPayment()
private async Task DayaLoanPayment()
{
// Validate user info before redirecting
if (string.IsNullOrWhiteSpace(_updateUserRequest.FirstName))
{
Snackbar.Add("لطفا اطلاعات شخصی خود را تکمیل کنید. (نام وارد نشده)", Severity.Error);
_showPurchaseBottomSheet = false;
Snackbar.Add($"لطفا اطلاعات شخصی خود را تکمیل کنید. (نام وارد نشده)", Severity.Error);
return;
}
if (string.IsNullOrWhiteSpace(_updateUserRequest.LastName))
{
Snackbar.Add("لطفا اطلاعات شخصی خود را تکمیل کنید. (نام خانوادگی وارد نشده)", Severity.Error);
_showPurchaseBottomSheet = false;
Snackbar.Add($"لطفا اطلاعات شخصی خود را تکمیل کنید. (نام خانوادگی وارد نشده)", Severity.Error);
return;
}
if (string.IsNullOrWhiteSpace(_updateUserRequest.NationalCode))
{
Snackbar.Add("لطفا اطلاعات شخصی خود را تکمیل کنید. (کدملی وارد نشده)", Severity.Error);
_showPurchaseBottomSheet = false;
Snackbar.Add($"لطفا اطلاعات شخصی خود را تکمیل کنید. (کدملی وارد نشده)", Severity.Error);
return;
}
if (_updateUserRequest.BirthDate == null)
{
Snackbar.Add("لطفا اطلاعات شخصی خود را تکمیل کنید. (تاریخ تولد وارد نشده)", Severity.Error);
_showPurchaseBottomSheet = false;
Snackbar.Add($"لطفا اطلاعات شخصی خود را تکمیل کنید. (تاریخ تولد وارد نشده)", Severity.Error);
return;
}
if (!_addresses.Any())
{
_showPurchaseBottomSheet = false;
Snackbar.Add($"آدرس محل سکونت شما الزامی است!", Severity.Error);
return;
}
_showPurchaseBottomSheet = false;
var url = "https://dayadiamond.ir/profile/creditpurchase/?merchantcode=56146364";
await JSRuntime.InvokeVoidAsync("open", url, "_blank");
}
@@ -1,229 +0,0 @@
@attribute [Route(RouteConstants.Profile.MagicWallet)]
@attribute [Authorize]
<PageTitle>کیف‌پول جادویی</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="🪄 کیف‌پول جادویی" BackHref="@RouteConstants.Profile.Wallet" />
@if (_isLoading)
{
<MudPaper Elevation="2" Class="pa-6 rounded-lg text-center">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
<MudText Class="mt-3">در حال بارگذاری...</MudText>
</MudPaper>
}
else if (_status.WalletMode != 1)
{
@* ═══ حالت عادی — Magic فعال نیست ═══ *@
<MudPaper Elevation="2" Class="pa-6 rounded-lg text-center">
<MudIcon Icon="@Icons.Material.Filled.Lock" Color="Color.Default" Size="Size.Large" Style="font-size: 64px;" />
<MudText Typo="Typo.h5" Class="mt-4">کیف‌پول جادویی فعال نیست</MudText>
<MudText Typo="Typo.body1" Class="mt-2 mud-text-secondary">
برای فعال شدن کیف‌پول جادویی، ابتدا پکیج پایه را خریداری کنید و سپس
تمام موجودی اصلی خود را از فروشگاه خرج کنید. وقتی موجودی اصلی به صفر برسد،
کیف‌پول جادویی فعال می‌شود.
</MudText>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="mt-4 rounded-pill"
Href="@RouteConstants.Profile.Wallet"
StartIcon="@Icons.Material.Filled.AccountBalanceWallet">
بازگشت به کیف پول
</MudButton>
</MudPaper>
}
else
{
@* ═══ حالت جادویی فعال ═══ *@
@* ── G4: هشدار تنظیمات پکیج‌بیس (Q28) ── *@
<MudAlert Severity="Severity.Warning" Variant="Variant.Text" Dense="true" Icon="@Icons.Material.Filled.Warning">
سقف واریز و ضریب اعتبار کیف‌پول جادویی بر اساس پکیج شما تنظیم شده است. پکیج‌های بالاتر سقف و ضریب بیشتری دارند.
</MudAlert>
@* ─── وضعیت و پیشرفت ─── *@
<MudPaper Elevation="2" Class="pa-5 rounded-xl" Style="background: linear-gradient(135deg, #7c3aed 0%, #a855f7 100%); color: white;">
<MudStack Spacing="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.AutoAwesome" Size="Size.Large" />
<MudText Typo="Typo.h5">حالت جادویی فعال</MudText>
</MudStack>
<MudGrid Spacing="2">
<MudItem xs="6" sm="3">
<MudText Typo="Typo.caption" Style="opacity:0.8">موجودی فعلی</MudText>
<MudText Typo="Typo.h6">@FormatPrice(_status.Balance)</MudText>
</MudItem>
<MudItem xs="6" sm="3">
<MudText Typo="Typo.caption" Style="opacity:0.8">مجموع واریزی</MudText>
<MudText Typo="Typo.h6">@FormatPrice(_status.MagicTotalDeposited)</MudText>
</MudItem>
<MudItem xs="6" sm="3">
<MudText Typo="Typo.caption" Style="opacity:0.8">مجموع اعتبار</MudText>
<MudText Typo="Typo.h6">@FormatPrice(_status.MagicTotalCredited)</MudText>
</MudItem>
<MudItem xs="6" sm="3">
<MudText Typo="Typo.caption" Style="opacity:0.8">سقف باقیمانده</MudText>
<MudText Typo="Typo.h6">@FormatPrice(_status.MagicRemainingDeposit)</MudText>
</MudItem>
</MudGrid>
@* نوار پیشرفت *@
<MudStack Spacing="0" Class="mt-1">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.caption" Style="opacity:0.8">پیشرفت سقف واریز</MudText>
<MudText Typo="Typo.caption" Style="opacity:0.8">@(_progressPercent.ToString("0"))%</MudText>
</MudStack>
<MudProgressLinear Value="@_progressPercent" Color="Color.Warning" Size="Size.Medium"
Rounded="true" Class="mt-1" Style="background: rgba(255,255,255,0.3);" />
</MudStack>
@if (_status.MagicActivatedAt.HasValue)
{
<MudText Typo="Typo.caption" Style="opacity:0.7">
فعال از: @_status.MagicActivatedAt.Value.MiladiToJalaliWithTime()
</MudText>
}
</MudStack>
</MudPaper>
@* ─── فرم شارژ ─── *@
@if (_status.MagicRemainingDeposit > 0)
{
<MudPaper Elevation="2" Class="pa-5 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-3">
<MudIcon Icon="@Icons.Material.Filled.CreditCard" Class="ml-1" />
شارژ کیف‌پول جادویی
</MudText>
<MudAlert Severity="MudBlazor.Severity.Info" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info">
مبلغ واریزی شما <strong>×@_status.MagicMultiplier.ToString("0.#")</strong> به موجودی اضافه می‌شود.
مثلاً ۱۰ میلیون واریز = @FormatToman((long)(10_000_000 * (decimal)_status.MagicMultiplier)) اعتبار.
</MudAlert>
<MudStack Spacing="2">
<MudNumericField @bind-Value="_chargeAmount"
Label="مبلغ واریز (تومان)"
Variant="Variant.Outlined"
Min="10_000"
Max="@_status.MagicRemainingDeposit"
Format="N0"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Payments"
HelperText="@($"حداکثر: {FormatPrice(_status.MagicRemainingDeposit)} (تومان)")" />
@if (_chargeAmount > 0)
{
<MudPaper Outlined="true" Class="pa-3 rounded-lg" Style="background: #f0fdf4;">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">مبلغ واریز:</MudText>
<MudText Typo="Typo.body2">@FormatToman(_chargeAmount)</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Color="Color.Success"><strong>اعتبار دریافتی (×@_status.MagicMultiplier.ToString("0.#")):</strong></MudText>
<MudText Typo="Typo.body2" Color="Color.Success"><strong>@FormatToman((long)(_chargeAmount * (decimal)_status.MagicMultiplier))</strong></MudText>
</MudStack>
</MudPaper>
}
@* دکمه‌های مبلغ سریع *@
<MudStack Row="true" Spacing="1" Class="flex-wrap">
@foreach (var preset in _presetAmounts)
{
if (preset <= _status.MagicRemainingDeposit)
{
<MudButton Variant="Variant.Outlined" Size="Size.Small"
Color="@(_chargeAmount == preset ? Color.Primary : Color.Default)"
OnClick="() => _chargeAmount = preset"
Class="rounded-pill">
@FormatToman(preset)
</MudButton>
}
}
</MudStack>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
FullWidth="true"
Disabled="@(_isProcessing || _chargeAmount < 10_000)"
OnClick="StartMagicCharge"
StartIcon="@Icons.Material.Filled.Payment"
Class="rounded-lg mt-2">
@if (_isProcessing)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="me-2" />
<span>در حال انتقال به درگاه...</span>
}
else
{
<span>پرداخت با کارت بانکی</span>
}
</MudButton>
</MudStack>
</MudPaper>
}
else
{
<MudAlert Severity="MudBlazor.Severity.Warning" Class="rounded-lg">
<MudStack Spacing="2">
<MudText>
سقف شارژ جادویی در این دور پر شده است.
با خرج کردن موجودی و خرید مجدد پکیج، سقف ریست می‌شود.
</MudText>
<MudText Typo="Typo.body2">
اگر برای تکمیل خرید به موجودی بیشتری نیاز دارید، می‌توانید به‌صورت استثنایی کیف اصلی را بدون ضریب (۱:۱) شارژ کنید.
</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Small"
StartIcon="@Icons.Material.Filled.AccountBalanceWallet"
Href="@RouteConstants.Profile.ChargeCreditWallet"
Class="rounded-lg align-self-start">
شارژ عادی بدون ضریب
</MudButton>
</MudStack>
</MudAlert>
}
@* ─── قوانین ─── *@
<MudExpansionPanels Elevation="1" Class="rounded-lg">
<MudExpansionPanel Text="قوانین کیف‌پول جادویی" MaxHeight="500" IsInitiallyExpanded="false">
<MudList T="string" Dense="true">
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
هر مبلغ واریزی ×@_status.MagicMultiplier.ToString("0.#") به موجودی اضافه می‌شود
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
سقف واریز در هر دور: @FormatPrice(_status.MagicMaxDeposit)
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.CheckCircle" IconColor="Color.Success">
حداکثر اعتبار: @FormatPrice(_status.MagicMaxCredit)
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.Warning" IconColor="Color.Warning">
در حالت جادویی، کمیسیون و پاداش های دریافتی غیرفعال است
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.Info" IconColor="Color.Info">
بعد از اتمام سقف و خرج موجودی، با خرید مجدد پکیج دور جدید شروع می‌شود
</MudListItem>
</MudList>
</MudExpansionPanel>
</MudExpansionPanels>
}
@* ─── نتیجه پرداخت (اگه از callback برگشته) ─── *@
@if (_paymentResult != null)
{
<MudAlert Severity="@(_paymentResult == "success" ? MudBlazor.Severity.Success : MudBlazor.Severity.Error)"
Class="rounded-lg" Variant="Variant.Filled">
@if (_paymentResult == "success")
{
<span>شارژ جادویی با موفقیت انجام شد! ✅</span>
}
else
{
<span>پرداخت ناموفق بود. لطفاً دوباره تلاش کنید.</span>
}
</MudAlert>
}
</MudStack>
</MudContainer>
@@ -1,77 +0,0 @@
using DateTimeConverterCL;
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace FrontOffice.Main.Pages.Profile;
public partial class MagicWallet : ComponentBase
{
private MagicWalletStatus _status = new(0, 0, 0, 0, 0, 0, null, 0);
private bool _isLoading = true;
private bool _isProcessing;
private long _chargeAmount;
private double _progressPercent;
private string? _paymentResult;
private readonly long[] _presetAmounts = { 1_000_000, 5_000_000, 10_000_000, 20_000_000, 50_000_000, 100_000_000 };
[SupplyParameterFromQuery(Name = "payment")]
public string? PaymentQueryParam { get; set; }
protected override async Task OnInitializedAsync()
{
_paymentResult = PaymentQueryParam;
await LoadStatus();
}
private async Task LoadStatus()
{
_isLoading = true;
_status = await WalletService.GetMagicWalletStatusAsync();
_progressPercent = _status.MagicMaxDeposit > 0
? (double)_status.MagicTotalDeposited / _status.MagicMaxDeposit * 100
: 0;
_isLoading = false;
}
private async Task StartMagicCharge()
{
if (_chargeAmount <= 0 || _isProcessing) return;
_isProcessing = true;
StateHasChanged();
try
{
// مبلغ به تومان — CMS خودش موقع ارسال به درگاه ×۱۰ می‌کنه
var (success, gatewayUrl, error) = await WalletService.InitiateMagicChargeAsync(_chargeAmount);
if (success && !string.IsNullOrEmpty(gatewayUrl))
{
Navigation.NavigateTo(gatewayUrl, forceLoad: true);
}
else
{
Snackbar.Add(error ?? "خطا در ایجاد درخواست پرداخت", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_isProcessing = false;
StateHasChanged();
}
}
private static string FormatPrice(long price)
=> string.Format("{0:N0} تومان", price);
private static string FormatToman(long toman)
=> string.Format("{0:N0} تومان", toman);
}
@@ -1,10 +1,8 @@
@page "/profile/payment-callback"
@attribute [Authorize]
@using Blazored.LocalStorage
@using CMSMicroservice.Protobuf.Protos.Package
@using CMSMicroservice.Protobuf.Protos.User
@using FrontOffice.Main.Utilities
@using FrontOffice.BFF.Package.Protobuf.Protos.Package
@using FrontOffice.BFF.User.Protobuf.Protos.User
<PageTitle>نتیجه پرداخت | کارا بازار سلامت</PageTitle>
@@ -21,10 +19,10 @@
<MudText Typo="Typo.h5" Color="Color.Success" Class="mt-4">پرداخت موفق</MudText>
<MudText Typo="Typo.body1" Class="mt-2">@_message</MudText>
@if (_transactionId > 0)
@if (!string.IsNullOrEmpty(_refId))
{
<MudText Typo="Typo.body2" Class="mt-3">
<strong>کد ارجاعی:</strong> @_transactionId
<strong>کد پیگیری:</strong> @_refId
</MudText>
}
@@ -33,10 +31,13 @@
<MudText Typo="Typo.body2" Color="Color.Secondary">
موجودی کیف پول: @FormatPrice(_walletBalance)
</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">
موجودی اعتبار تخفیف: @FormatPrice(_discountBalance)
</MudText>
<MudButton Color="Color.Primary" Variant="Variant.Filled" Class="mt-6"
Href="@_successReturnUrl">
@_successReturnText
Href="/profile">
بازگشت به پروفایل
</MudButton>
}
else
@@ -46,8 +47,8 @@
<MudText Typo="Typo.body1" Class="mt-2">@_message</MudText>
<MudButton Color="Color.Primary" Variant="Variant.Filled" Class="mt-6"
Href="@_failReturnUrl">
@_failReturnText
Href="/profile">
بازگشت به پروفایل
</MudButton>
<MudButton Color="Color.Secondary" Variant="Variant.Outlined" Class="mt-3 mr-2"
@@ -61,19 +62,12 @@
@code {
[Inject] private PackageContract.PackageContractClient PackageContractClient { get; set; } = default!;
[Inject] private UserContract.UserContractClient UserContractClient { get; set; } = default!;
[Inject] private DiscountOrderService DiscountOrderService { get; set; } = default!;
[Inject] private ILocalStorageService LocalStorage { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!;
[Inject] private NavigationManager NavManager { get; set; } = default!;
private const string TokenStorageKey = "auth:token";
/// <summary>
/// نوع پرداخت: package (پیش‌فرض) | magic-wallet | discount-wallet | discount-order
/// </summary>
[SupplyParameterFromQuery(Name = "type")]
private string? PaymentType { get; set; }
[SupplyParameterFromQuery(Name = "orderId")]
private long OrderId { get; set; }
@@ -89,42 +83,15 @@
private bool _isLoading = true;
private bool _isSuccess;
private string _message = string.Empty;
private long _transactionId;
private string? _refId;
private long _walletBalance;
private bool _verifyCompleted;
private readonly SemaphoreSlim _verifyGate = new(1, 1);
// دکمه‌های بازگشت بسته به نوع پرداخت
private string _successReturnUrl = "/profile";
private string _successReturnText = "بازگشت به پروفایل";
private string _failReturnUrl = "/profile";
private string _failReturnText = "بازگشت به پروفایل";
private long _discountBalance;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && !_verifyCompleted)
if (firstRender)
{
await VerifyPaymentOnceAsync();
}
}
/// <summary>
/// Blazor Server may invoke render/verify twice in quick succession — gate with semaphore.
/// </summary>
private async Task VerifyPaymentOnceAsync()
{
if (_verifyCompleted) return;
await _verifyGate.WaitAsync();
try
{
if (_verifyCompleted) return;
await VerifyPayment();
_verifyCompleted = true;
}
finally
{
_verifyGate.Release();
}
}
@@ -132,26 +99,34 @@
{
try
{
// تعیین نوع پرداخت — اگر type نباشد، پیش‌فرض خرید پکیج
var type = PaymentType?.ToLowerInvariant() ?? "package";
switch (type)
if (OrderId <= 0 || TransactionId <= 0 || string.IsNullOrEmpty(Authority))
{
case "magic-wallet":
await VerifyMagicWalletCharge();
break;
case "discount-wallet":
await VerifyDiscountWalletCharge();
break;
case "credit-wallet":
await VerifyCreditWalletCharge();
break;
case "discount-order":
await VerifyDiscountOrderPayment();
break;
default: // "package" or missing
await VerifyPackagePurchase();
break;
_isSuccess = false;
_message = "پارامترهای پرداخت نامعتبر است";
_isLoading = false;
StateHasChanged();
return;
}
var response = await PackageContractClient.VerifyBasePackagePaymentAsync(new VerifyBasePackagePaymentRequest
{
OrderId = OrderId,
TransactionId = TransactionId,
Authority = Authority,
Status = Status ?? "NOK"
});
_isSuccess = response.Success;
_message = response.Message;
_refId = response.RefId;
_walletBalance = response.WalletBalance;
_discountBalance = response.DiscountBalance;
// اگر پرداخت موفق بود، توکن را refresh می‌کنیم
// چون PackagePurchaseMethod تغییر کرده و باید در claims جدید باشد
if (_isSuccess)
{
await RefreshTokenAsync();
}
}
catch (Exception ex)
@@ -165,186 +140,10 @@
StateHasChanged();
}
}
/// <summary>
/// تأیید خرید پکیج — رفتار فعلی
/// </summary>
private async Task VerifyPackagePurchase()
{
_successReturnUrl = "/profile";
_successReturnText = "بازگشت به پروفایل";
_failReturnUrl = "/profile";
_failReturnText = "بازگشت به پروفایل";
if (OrderId <= 0 || string.IsNullOrEmpty(Authority))
{
_isSuccess = false;
_message = "پارامترهای پرداخت نامعتبر است";
return;
}
var response = await PackageContractClient.CustomerVerifyPackagePurchaseAsync(new CustomerVerifyPackagePurchaseRequest
{
OrderId = OrderId,
Authority = Authority ?? string.Empty,
Status = Status ?? string.Empty
});
_isSuccess = response.Success;
_message = response.Message;
_transactionId = response.TransactionId;
if (_isSuccess)
{
await UpdateWalletBalanceAndRefreshToken();
}
}
/// <summary>
/// تأیید شارژ کیف‌پول جادویی
/// </summary>
private async Task VerifyMagicWalletCharge()
{
_successReturnUrl = "/profile/magic-wallet";
_successReturnText = "بازگشت به کیف پول جادویی";
_failReturnUrl = "/profile/magic-wallet";
_failReturnText = "بازگشت به کیف پول جادویی";
if (string.IsNullOrEmpty(Authority))
{
_isSuccess = false;
_message = "کد Authority نامعتبر است";
return;
}
var (success, message) = await WalletService.VerifyMagicChargeAsync(
Authority, Status ?? "NOK");
_isSuccess = success;
_message = message;
if (_isSuccess)
{
await UpdateWalletBalance();
}
}
/// <summary>
/// تأیید شارژ کیف پول تخفیفی
/// </summary>
private async Task VerifyDiscountWalletCharge()
{
_successReturnUrl = "/profile/charge-discount-wallet";
_successReturnText = "بازگشت به کیف پول تخفیفی";
_failReturnUrl = "/profile/charge-discount-wallet";
_failReturnText = "بازگشت به کیف پول تخفیفی";
if (string.IsNullOrEmpty(Authority))
{
_isSuccess = false;
_message = "کد Authority نامعتبر است";
return;
}
var (success, message) = await WalletService.VerifyDiscountChargeAsync(
Authority, Status ?? "NOK");
_isSuccess = success;
_message = message;
if (_isSuccess)
{
await UpdateWalletBalance();
}
}
/// <summary>
/// تأیید شارژ کیف پول اصلی
/// </summary>
private async Task VerifyCreditWalletCharge()
{
_successReturnUrl = "/profile/charge-credit-wallet?payment=success";
_successReturnText = "بازگشت به شارژ کیف پول اصلی";
_failReturnUrl = "/profile/charge-credit-wallet?payment=failed";
_failReturnText = "بازگشت به شارژ کیف پول اصلی";
if (string.IsNullOrEmpty(Authority))
{
_isSuccess = false;
_message = "کد Authority نامعتبر است";
return;
}
var (success, message) = await WalletService.VerifyCreditChargeAsync(
Authority, Status ?? "NOK");
_isSuccess = success;
_message = message;
if (_isSuccess)
{
await UpdateWalletBalance();
}
}
/// <summary>
/// تأیید پرداخت سفارش فروشگاه تخفیفی
/// </summary>
private async Task VerifyDiscountOrderPayment()
{
_successReturnUrl = $"/discount-store/order/{OrderId}";
_successReturnText = "مشاهده سفارش";
_failReturnUrl = $"/discount-store/order/{OrderId}";
_failReturnText = "مشاهده سفارش";
if (OrderId <= 0 || string.IsNullOrEmpty(Authority))
{
_isSuccess = false;
_message = "پارامترهای پرداخت نامعتبر است";
return;
}
var (success, message, _) = await DiscountOrderService.VerifyDiscountOrderPaymentAsync(
OrderId, Authority, Status ?? "NOK");
_isSuccess = success;
_message = message;
}
/// <summary>
/// بروزرسانی موجودی کیف پول
/// </summary>
private async Task UpdateWalletBalance()
{
try
{
var balances = await WalletService.GetBalancesAsync();
_walletBalance = balances.CreditBalance;
}
catch { /* اگر خطا شد، صفر نمایش بده */ }
}
/// <summary>
/// بروزرسانی موجودی + refresh توکن (فقط برای خرید پکیج)
/// </summary>
private async Task UpdateWalletBalanceAndRefreshToken()
{
await UpdateWalletBalance();
await RefreshTokenAsync();
}
private void RetryPayment()
{
var type = PaymentType?.ToLowerInvariant() ?? "package";
var url = type switch
{
"magic-wallet" => "/profile/magic-wallet",
"discount-wallet" => "/profile/charge-discount-wallet",
"credit-wallet" => "/profile/charge-credit-wallet",
"discount-order" => "/discount-store",
_ => "/profile"
};
NavManager.NavigateTo(url, forceLoad: true);
NavManager.NavigateTo("/profile");
}
/// <summary>
@@ -354,7 +153,7 @@
{
try
{
var userResponse = await UserContractClient.GetUserForCustomerAsync(new CMSMicroservice.Protobuf.Protos.User.GetUserForCustomerRequest());
var userResponse = await UserContractClient.GetUserAsync(new Google.Protobuf.WellKnownTypes.Empty());
if (!string.IsNullOrWhiteSpace(userResponse.Token))
{
// ذخیره توکن جدید در localStorage
@@ -4,10 +4,13 @@
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="اطلاعات شخصی" BackHref="@RouteConstants.Profile.Index" />
<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>
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudForm @ref="_personalForm" Model="_updateUserRequest">
<MudForm @ref="_personalForm" Model="_updateUserRequest" Validation="@(_personalValidator.ValidateValue)">
<MudGrid Spacing="3">
<MudItem xs="12" md="6">
<MudTextField @bind-Value="_updateUserRequest.FirstName"
@@ -1,6 +1,8 @@
using CMSMicroservice.Protobuf.Protos.User;
using FluentValidation;
using FrontOffice.BFF.User.Protobuf.Protos.User;
using FrontOffice.BFF.User.Protobuf.Validator;
using FrontOffice.Main.Utilities;
using Mapster;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using Severity = MudBlazor.Severity;
@@ -11,8 +13,9 @@ public partial class Personal : ComponentBase
{
[Inject] private UserContract.UserContractClient UserContract { get; set; } = default!;
private GetUserForCustomerResponse _userProfile = new();
private UpdateCustomerProfileRequest _updateUserRequest = new();
private GetUserResponse _userProfile = new();
private UpdateUserRequest _updateUserRequest = new();
private readonly UpdateUserRequestValidator _personalValidator = new();
private MudForm? _personalForm;
private DateTime? _date;
@@ -27,14 +30,8 @@ public partial class Personal : ComponentBase
{
try
{
_userProfile = await UserContract.GetUserForCustomerAsync(new());
_updateUserRequest = new UpdateCustomerProfileRequest
{
FirstName = _userProfile.FirstName,
LastName = _userProfile.LastName,
Email = _userProfile.Email,
NationalCode = _userProfile.NationalCode,
};
_userProfile = await UserContract.GetUserAsync(new());
_updateUserRequest = _userProfile.Adapt<UpdateUserRequest>();
if (_userProfile.BirthDate != null)
_date = _userProfile.BirthDate.ToDateTime();
}
@@ -59,7 +56,7 @@ public partial class Personal : ComponentBase
if (_date != null)
_updateUserRequest.BirthDate = _date.Value.DateTimeToTimestamp();
await UserContract.UpdateCustomerProfileAsync(_updateUserRequest);
await UserContract.UpdateUserAsync(_updateUserRequest);
Snackbar.Add("اطلاعات شخصی با موفقیت ذخیره شد.", Severity.Success);
}
catch (Exception ex)
@@ -4,7 +4,10 @@
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="تنظیمات حساب" BackHref="@RouteConstants.Profile.Index" />
<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>
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="4">
@@ -1,4 +1,5 @@
using CMSMicroservice.Protobuf.Protos.User;
using FrontOffice.BFF.User.Protobuf.Protos.User;
using Mapster;
using Microsoft.AspNetCore.Components;
using MudBlazor;
@@ -15,18 +16,8 @@ public partial class Settings : ComponentBase
{
try
{
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,
};
var user = await UserContract.GetUserAsync(new());
_request = user.Adapt<UpdateUserRequest>();
}
catch
{
@@ -1,11 +1,14 @@
@attribute [Route(RouteConstants.Profile.Tree)]
@using FrontOffice.Main.Pages.Profile.Components
<PageTitle>سازمان فروش</PageTitle>
<PageTitle>شجره‌نامه</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="سازمان فروش" BackHref="@RouteConstants.Profile.Index" />
<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>
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<OrganizationChart />
+21 -180
View File
@@ -4,25 +4,28 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="کیف پول" BackHref="@RouteConstants.Profile.Index" />
<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>
<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.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>
<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>
</MudGrid>
@@ -38,104 +41,6 @@
درخواست‌های برداشت
</MudButton>
<!-- دکمه شارژ کیف‌پول اصلی / جادویی -->
@if (_isClubMemberActive)
{
@if (_isMagicWallet)
{
<MudButton Variant="Variant.Filled"
Color="Color.Secondary"
Size="Size.Large"
FullWidth="true"
StartIcon="@Icons.Material.Filled.AutoAwesome"
Href="@RouteConstants.Profile.MagicWallet"
Class="rounded-lg">
شارژ کیف‌پول جادویی
</MudButton>
@if (_magicCeilingFull)
{
<MudAlert Severity="Severity.Warning" Dense="true" Variant="Variant.Outlined">
سقف شارژ جادویی این دور پر است. در صورت نیاز می‌توانید به‌صورت استثنایی بدون ضریب شارژ کنید.
</MudAlert>
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Large"
FullWidth="true"
StartIcon="@Icons.Material.Filled.AccountBalanceWallet"
Href="@RouteConstants.Profile.ChargeCreditWallet"
Class="rounded-lg">
شارژ عادی بدون ضریب
</MudButton>
}
else
{
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined">
کیف پول شما در حالت جادویی است؛ شارژ از مسیر جادویی با ضریب چندبرابر اعمال می‌شود.
</MudAlert>
}
}
else
{
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
FullWidth="true"
StartIcon="@Icons.Material.Filled.AccountBalanceWallet"
Href="@RouteConstants.Profile.ChargeCreditWallet"
Class="rounded-lg">
شارژ کیف‌پول اصلی
</MudButton>
}
}
else
{
<MudPaper Elevation="0" Class="pa-3 rounded-lg" Outlined="true">
<MudStack Spacing="2">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
FullWidth="true"
StartIcon="@Icons.Material.Filled.AccountBalanceWallet"
Disabled="true"
Class="rounded-lg">
شارژ کیف‌پول اصلی
</MudButton>
<MudAlert Severity="Severity.Warning" Dense="true" Variant="Variant.Outlined">
برای شارژ کیف پول اصلی ابتدا باید پکیج را خریداری کرده و قرارداد باشگاه مشتریان را امضا کنید.
</MudAlert>
<MudStack Row="true" Spacing="1" Class="flex-wrap">
@if (!_hasPurchasedPackage)
{
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
Href="@RouteConstants.Package.List"
StartIcon="@Icons.Material.Filled.CardGiftcard">
مشاهده پکیج‌ها
</MudButton>
}
else
{
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
Href="@RouteConstants.Club.Membership"
StartIcon="@Icons.Material.Filled.Handshake">
امضای قرارداد باشگاه
</MudButton>
}
</MudStack>
</MudStack>
</MudPaper>
}
<!-- دکمه شارژ کیف‌پول اعتباری -->
<MudButton Variant="Variant.Filled"
Color="Color.Info"
Size="Size.Large"
FullWidth="true"
StartIcon="@Icons.Material.Filled.CreditCard"
Href="@RouteConstants.Profile.ChargeDiscountWallet"
Class="rounded-lg">
شارژ کیف‌پول اعتباری
</MudButton>
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-2">تراکنش‌ها و مسیرهای شارژ</MudText>
@@ -166,45 +71,18 @@
</MudGrid>
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudTable Items="_txs" Dense="true" Hover="true" Striped="true">
<MudTable Items="_txs" Dense="true">
<HeaderContent>
<MudTh>تاریخ</MudTh>
<MudTh>اصلی (تغییرات / مانده)</MudTh>
<MudTh>پاداش های دریافتی (تغییرات / مانده)</MudTh>
<MudTh>اعتباری (تغییرات / مانده)</MudTh>
<MudTh>شناسه ارجاع</MudTh>
<MudTh>مبلغ</MudTh>
<MudTh>مسیر</MudTh>
<MudTh>توضیحات</MudTh>
</HeaderContent>
<RowTemplate>
<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>
<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>
</RowTemplate>
</MudTable>
</MudHidden>
@@ -213,50 +91,13 @@
<MudStack Spacing="2">
@foreach (var tx in _txs)
{
<MudPaper Class="pa-3 rounded-lg" Outlined="true" Elevation="1">
<MudStack Spacing="2">
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.caption" Class="mud-text-secondary">@tx.Date</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">شناسه: @tx.Channel</MudText>
<MudText>@tx.Date</MudText>
<MudText Color="@(tx.Amount > 0 ? Color.Success : Color.Error)">@FormatPrice(tx.Amount)</MudText>
</MudStack>
<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.caption" Class="mud-text-secondary">@tx.Channel</MudText>
<MudText Typo="Typo.body2">@tx.Description</MudText>
</MudStack>
</MudPaper>
@@ -6,43 +6,13 @@ namespace FrontOffice.Main.Pages.Profile;
public partial class Wallet : ComponentBase
{
[Inject] private AuthService AuthService { get; set; } = default!;
private (string Credit, string Discount, string Network) _balances = ("-", "-", "-");
private List<WalletTransaction> _txs = new();
private string? _filterReferenceId;
private string _filterType = "all";
private bool _isClubMemberActive;
private bool _hasPurchasedPackage;
private bool _isMagicWallet;
private bool _magicCeilingFull;
protected override async Task OnInitializedAsync()
{
try
{
var userInfo = await AuthService.GetUserAuthInfo();
_isClubMemberActive = userInfo.IsClubMemberActive;
_hasPurchasedPackage = userInfo.HasPurchasedPackage;
}
catch
{
_isClubMemberActive = false;
_hasPurchasedPackage = false;
}
try
{
var magicStatus = await WalletService.GetMagicWalletStatusAsync();
_isMagicWallet = magicStatus.WalletMode == 1;
_magicCeilingFull = _isMagicWallet && magicStatus.MagicRemainingDeposit <= 0;
}
catch
{
_isMagicWallet = false;
_magicCeilingFull = false;
}
var b = await WalletService.GetBalancesAsync();
_balances = (FormatPrice(b.CreditBalance), FormatPrice(b.DiscountBalance), FormatPrice(b.NetworkBalance));
_txs = await WalletService.GetTransactionsAsync();
@@ -4,7 +4,10 @@
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="درخواست‌های برداشت" BackHref="@RouteConstants.Profile.Wallet" />
<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>
<!-- فرم درخواست برداشت جدید -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
@@ -97,7 +100,7 @@
@if (_isLoading)
{
<LoadingState />
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
}
else if (!_withdrawals.Any())
{
@@ -58,19 +58,8 @@ public partial class WithdrawalRequests : ComponentBase
try
{
_isSubmittingWithdrawal = true;
string? normalizedIban = null;
if (_withdrawMethod == WithdrawalMethodClient.Cash)
{
normalizedIban = NormalizeIranianIban(_withdrawIban);
if (normalizedIban is null)
{
Snackbar.Add("فرمت شماره شبا معتبر نیست. باید IR و ۲۴ رقم باشد (خط تیره اختیاری است).", Severity.Warning);
return;
}
}
await WalletService.RequestWithdrawalAsync(_selectedPayout.Id, _withdrawMethod, normalizedIban);
_withdrawIban=_withdrawIban!.Trim().ToUpper().Replace("IR", "").Replace(" ", "");
await WalletService.RequestWithdrawalAsync(_selectedPayout.Id, _withdrawMethod, "IR"+_withdrawIban);
Snackbar.Add("درخواست برداشت ثبت شد.", Severity.Success);
// بروزرسانی لیست
@@ -136,26 +125,4 @@ public partial class WithdrawalRequests : ComponentBase
1 => "الماس",
_ => "-"
};
/// <summary>
/// فاصله و خط تیره را حذف می‌کند و شبا را به شکل IR + ۲۴ رقم برمی‌گرداند.
/// </summary>
private static string? NormalizeIranianIban(string? iban)
{
if (string.IsNullOrWhiteSpace(iban))
return null;
var normalized = iban.Trim().ToUpperInvariant()
.Replace(" ", "", StringComparison.Ordinal)
.Replace("-", "", StringComparison.Ordinal);
if (normalized.StartsWith("IR", StringComparison.Ordinal))
normalized = normalized[2..];
normalized = "IR" + normalized;
return System.Text.RegularExpressions.Regex.IsMatch(normalized, @"^IR\d{24}$")
? normalized
: null;
}
}
+55 -68
View File
@@ -6,11 +6,10 @@
<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="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">
<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">
فقط در چند دقیقه حساب خود را فعال کنید
</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
@@ -19,47 +18,34 @@
</MudText>
</MudStack>
<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>
<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" />
<MudText Typo="Typo.subtitle1">مزایای ثبت‌نام آنلاین</MudText>
</MudStack>
<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>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
• تایید سریع و آنلاین شماره موبایل<br />
• تکمیل اطلاعات هویتی بدون مراجعه حضوری<br />
• دریافت نسخه‌ی دیجیتال قرارداد برای بررسی دقیق
</MudText>
</MudStack>
</MudPaper>
</MudItem>
<MudItem xs="12" md="7">
<MudPaper Elevation="1" Class="pa-5 pa-md-6 wizard-card">
<MudPaper Elevation="3" Class="pa-6 wizard-card">
@if (_completed)
{
<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>
<MudStack Spacing="3" AlignItems="AlignItems.Center" Class="text-center">
<MudAvatar Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Large" />
<MudText Typo="Typo.h4">درخواست شما ثبت شد</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary" Style="max-width:400px;">
<MudText Typo="Typo.body1" Class="mud-text-secondary">
تیم ما پس از بررسی اطلاعات با شما تماس خواهد گرفت. می‌توانید از طریق داشبورد وضعیت ثبت‌نام
را دنبال کنید.
</MudText>
<MudStack Row="true" Spacing="2" Justify="Justify.Center">
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="rounded-lg"
<MudButton Variant="Variant.Filled" Color="Color.Primary"
OnClick="@(() => Navigation.NavigateTo(RouteConstants.Main.MainPage))">بازگشت به صفحه
اصلی</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
@@ -70,45 +56,43 @@
}
else
{
<MudStack Spacing="3">
<MudStack Spacing="3" >
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">مراحل ثبت‌نام</MudText>
<MudChip T="string" Color="Color.Primary" Variant="Variant.Outlined" Size="Size.Small" Class="rounded-pill">
مرحله @(_activeStep + 1) از ۳
<MudText Typo="Typo.h4">مراحل ثبت‌نام</MudText>
<MudChip T="string" Color="Color.Info" Variant="Variant.Outlined" Size="Size.Small">۳ مرحله
</MudChip>
</MudStack>
@if (_isSubmitting)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="rounded" />
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
}
<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.Outlined.PhoneAndroid">
<MudStep Label="تأیید موبایل" Icon="@Icons.Material.Filled.Smartphone">
@* Inline AuthDialog with captcha enabled *@
<AuthDialog @ref="_authDialog" InlineMode="true" HideCancelButton="true" OnLoginSuccess="@(async () => { OnPhoneVerified(); })" />
</MudStep>
<MudStep Label="اطلاعات هویتی" Icon="@Icons.Material.Outlined.Badge">
<MudStep Label="اطلاعات هویتی" Icon="@Icons.Material.Filled.Badge">
<MudForm @ref="_stepTwoForm">
<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>
<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" />
</MudForm>
</MudStep>
<MudStep Label="قوانین و قرارداد" Icon="@Icons.Material.Outlined.Rule">
<MudStep Label="قوانین و قرارداد" Icon="@Icons.Material.Filled.Rule">
<MudForm @ref="_stepThreeForm">
<MudAlert Variant="Variant.Outlined" Severity="Severity.Info" Class="mb-3 rounded-lg">
<MudAlert Variant="Variant.Outlined" Severity="Severity.Info" Class="mb-3">
لطفاً قوانین و شرایط همکاری را با دقت مطالعه کنید و در صورت موافقت، تیک
تایید را فعال نمایید.
تایید را فعال نمایید. همچنین می‌توانید نسخه‌ی قرارداد را دانلود و ذخیره
کنید.
</MudAlert>
<MudPaper Elevation="0" Class="terms-box pa-4 mb-3">
<MudText Typo="Typo.subtitle2" Class="mb-2">بخشی از قوانین:</MudText>
@@ -122,44 +106,47 @@
</MudList>
</MudPaper>
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Class="rounded-lg mb-3"
StartIcon="@Icons.Material.Outlined.Download" Disabled="_isSubmitting"
OnClick="DownloadContract">
دانلود قرارداد نمونه (PDF)
</MudButton>
<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>
<MudCheckBox @bind-Checked="_model.AcceptTerms" Color="Color.Primary"
<MudCheckBox @bind-Checked="_model.AcceptTerms" Color="Color.Success"
For="@(() => _model.AcceptTerms)"
Label="قوانین و مقررات را مطالعه کرده‌ام و می‌پذیرم" />
</MudForm>
</MudStep>
</ChildContent>
<ActionContent Context="stepper">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mt-2">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
@if (_isAuthenticated && _activeStep == 1)
{
<MudButton Variant="Variant.Text" Color="Color.Error" Size="Size.Small"
<MudButton Variant="Variant.Text" Color="Color.Secondary"
OnClick="@AuthService.LogoutAsync">خروج از حساب</MudButton>
}
else
{
<MudButton Variant="Variant.Text" Color="Color.Secondary" Size="Size.Small"
Disabled="_activeStep == 0 || _isSubmitting"
OnClick="@(async () => { await GoBack(); })">مرحله قبل</MudButton>
<MudButton Variant="Variant.Text" Color="Color.Secondary"
Disabled="_activeStep == 0 || _isSubmitting"
OnClick="@(async () => { await GoBack(); })">مرحله قبل</MudButton>
}
<MudSpacer/>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="rounded-lg"
<MudButton Variant="Variant.Filled" Color="Color.Primary"
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 CMSMicroservice.Protobuf.Protos.User;
using FrontOffice.BFF.User.Protobuf.Protos.User;
using FrontOffice.Main.Shared;
using FrontOffice.Main.Utilities;
using Google.Protobuf.WellKnownTypes;
@@ -23,14 +23,13 @@ public partial class RegisterWizard
private bool _completed;
private AuthDialog? _authDialog;
private MudStepper? _mudStepper;
private UpdateCustomerProfileRequest _updateUserRequest = new();
private UpdateUserRequest _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!;
@@ -56,7 +55,6 @@ public partial class RegisterWizard
if (_userAuthInfo.IsSignMainContract)
{
Navigation.NavigateTo(RouteConstants.Profile.Index);
return;
}
if (_activeStep == 0)
@@ -65,12 +63,11 @@ public partial class RegisterWizard
_activeStep = 1;
await InvokeAsync(StateHasChanged);
}
else if (_activeStep == 1 && !_initialDataLoaded)
else if (_activeStep == 1)
{
_initialDataLoaded = true;
try
{
var existUser = await UserContract.GetUserForCustomerAsync(new GetUserForCustomerRequest());
var existUser = await UserContract.GetUserAsync(new Empty());
if (existUser != null && !string.IsNullOrEmpty(existUser.FirstName) &&
string.IsNullOrWhiteSpace(_model.FirstName))
{
@@ -98,6 +95,7 @@ public partial class RegisterWizard
}
}
await base.OnAfterRenderAsync(firstRender);
}
@@ -357,7 +355,7 @@ public partial class RegisterWizard
_updateUserRequest.NationalCode = _model.NationalCode.PersianToEnglish();
await UserContract.UpdateCustomerProfileAsync(request: _updateUserRequest);
await UserContract.UpdateUserAsync(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">
<PageHeader Title="سبد خرید" BackHref="@RouteConstants.Store.Products" />
<MudText Typo="Typo.h4">سبد خرید</MudText>
@if (CartData.Items.Count == 0)
{
@@ -30,8 +30,8 @@
<MudTd>
<MudStack Spacing="1">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<AppImage Path="@GetProductImageUrl(context.ImageUrl)" Alt="@context.Title"
ImgWidth="64" ImgHeight="64" Class="rounded-lg" ObjectFit="ObjectFit.Cover" />
<MudImage Src="@GetProductImageUrl(context.ImageUrl)" Alt="@context.Title"
Width="64" Height="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">
<AppImage Path="@GetProductImageUrl(item.ImageUrl)" Alt="@item.Title"
ImgWidth="50" ImgHeight="50" Class="rounded-lg" ObjectFit="ObjectFit.Cover" />
<MudImage Src="@GetProductImageUrl(item.ImageUrl)" Alt="@item.Title"
Width="50" Height="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" Class="fw-bold">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.h6" Color="Color.Primary" Class="fw-bold">@FormatPrice(TotalWithVAT) تومان</MudText>
<MudText Typo="Typo.h6" Style="font-weight:bold;">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.h6" Color="Color.Primary" Style="font-weight:bold;">@FormatPrice(TotalWithVAT) تومان</MudText>
</MudStack>
</MudStack>
@@ -185,8 +185,8 @@
}
<MudDivider Class="my-1" />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1" Class="fw-bold">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="fw-bold">@FormatPrice(TotalWithVAT) تومان</MudText>
<MudText Typo="Typo.subtitle1" Style="font-weight:bold;">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Style="font-weight:bold;">@FormatPrice(TotalWithVAT) تومان</MudText>
</MudStack>
</MudStack>
</MudPaper>
@@ -8,17 +8,11 @@ public partial class Cart : ComponentBase, IDisposable
{
[Inject] private CartService CartService { get; set; } = default!;
[Inject] private VATService VAT { get; set; } = default!;
[Inject] private AuthDialogService AuthDialogService { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!;
// Navigation and Snackbar are available via _Imports.razor
private CartService CartData => CartService;
protected override async Task OnInitializedAsync()
{
if (!await AuthService.IsAuthenticatedAsync())
{
await AuthDialogService.ShowAuthDialogAsync();
}
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
await CartService.EnsureInitializedAsync();
CartService.OnChange += StateHasChanged;
@@ -72,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.TrimStart('/');
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl;
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,11 +1,9 @@
@using CMSMicroservice.Protobuf.Protos.UserOrder
@using CMSMicroservice.Protobuf.Protos
@using FrontOffice.BFF.UserOrder.Protobuf.Protos.UserOrder
@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">
@@ -21,11 +19,9 @@
else if (_addresses.Count == 0)
{
<MudAlert Severity="Severity.Warning">
هیچ آدرسی ثبت نشده است. می‌توانید همین‌جا آدرس جدید اضافه کنید.
هیچ آدرسی ثبت نشده است. لطفاً از بخش پروفایل آدرس خود را اضافه کنید.
</MudAlert>
<MudButton Class="mt-2" Variant="Variant.Outlined" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="OpenAddAddressDialog">افزودن آدرس</MudButton>
<MudButton Class="mt-2" Variant="Variant.Outlined" Href="/profile/addresses">افزودن آدرس</MudButton>
}
else
{
@@ -33,7 +29,7 @@
@foreach (var address in _addresses)
{
<MudPaper Outlined="@(_selectedAddress?.Id != address.Id)"
Elevation="@(_selectedAddress?.Id == address.Id ? 2 : 0)"
Elevation="@(_selectedAddress?.Id == address.Id ? 4 : 0)"
Class="pa-3 rounded-xl cursor-pointer"
Style="@(_selectedAddress?.Id == address.Id ? "border: 2px solid var(--mud-palette-primary);" : "")"
@onclick="() => _selectedAddress = address">
@@ -42,25 +38,13 @@
<MudText Typo="Typo.subtitle2">@address.Title</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@address.Address</MudText>
</MudStack>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
@if (address.IsDefault)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Outlined" Size="Size.Small">پیش‌فرض</MudChip>
}
<span @onclick:stopPropagation="true">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Size="Size.Small"
Color="Color.Primary"
aria-label="ویرایش آدرس"
OnClick="@(() => OpenEditAddressDialog(address))" />
</span>
</MudStack>
@if (address.IsDefault)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Outlined">پیش‌فرض</MudChip>
}
</MudStack>
</MudPaper>
}
<MudButton Class="mt-2" Variant="Variant.Text" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="OpenAddAddressDialog">افزودن آدرس جدید</MudButton>
</MudStack>
}
</MudPaper>
@@ -99,8 +83,8 @@
<MudListItemText>
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<AppImage Path="@GetProductImageUrl(item.ImageUrl)" Alt="@item.Title"
ImgWidth="48" ImgHeight="48" Class="rounded-lg" ObjectFit="ObjectFit.Cover" />
<MudImage Src="@GetProductImageUrl(item.ImageUrl)" Alt="@item.Title"
Width="48" Height="48" Class="rounded-circle" />
<MudText Typo="Typo.subtitle2">@item.Title</MudText>
<MudText Typo="Typo.subtitle2">@FormatPrice(item.LineTotal)</MudText>
</MudStack>
@@ -133,8 +117,8 @@
}
<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(VAT.AddVAT(Cart.Total))</MudText>
<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>
</MudStack>
</MudStack>
@@ -1,11 +1,8 @@
using CMSMicroservice.Protobuf.Protos.UserAddress;
using CMSMicroservice.Protobuf.Protos.UserOrder;
using FrontOffice.Main.Pages.Profile.Components;
using FrontOffice.Main.Shared;
using FrontOffice.BFF.UserAddress.Protobuf.Protos.UserAddress;
using FrontOffice.BFF.UserOrder.Protobuf.Protos.UserOrder;
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using Messages = CMSMicroservice.Protobuf.Protos;
namespace FrontOffice.Main.Pages.Store;
@@ -16,31 +13,22 @@ public partial class CheckoutSummary : ComponentBase
[Inject] private VATService VAT { get; set; } = default!;
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
[Inject] private UserOrderContract.UserOrderContractClient UserOrderContract { get; set; } = default!;
[Inject] private AuthDialogService AuthDialogService { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!;
// Snackbar and Navigation are injected via _Imports.razor
private List<CustomerAddressModel> _addresses = new();
private CustomerAddressModel? _selectedAddress;
private List<GetAllUserAddressByFilterResponseModel> _addresses = new();
private GetAllUserAddressByFilterResponseModel? _selectedAddress;
private bool _loadingAddresses;
private long walletBalance;
private Messages.PaymentMethod _payment = Messages.PaymentMethod.Wallet;
private PaymentMethod _payment = PaymentMethod.Wallet;
private bool CanPlaceOrder => Cart.Items.Count > 0 && _selectedAddress != null;
protected override async Task OnInitializedAsync()
{
if (!await AuthService.IsAuthenticatedAsync())
{
await AuthDialogService.ShowAuthDialogAsync();
}
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
await Cart.EnsureInitializedAsync();
var userInfo = await AuthService.GetUserAuthInfo();
if (userInfo.HasAddress)
await LoadAddresses();
else
_addresses = new();
await LoadAddresses();
await LoadWalletBalance();
}
@@ -48,6 +36,7 @@ public partial class CheckoutSummary : ComponentBase
{
if (firstRender)
{
// بارگذاری نرخ VAT
await VAT.LoadAsync();
}
await base.OnAfterRenderAsync(firstRender);
@@ -56,7 +45,9 @@ public partial class CheckoutSummary : ComponentBase
private async Task LoadWalletBalance()
{
var walletResult = await WalletService.GetBalancesAsync();
walletBalance = walletResult.CreditBalance;
walletBalance = walletResult.CreditBalance
// + walletResult.NetworkBalance
;
}
private async Task LoadAddresses()
@@ -64,7 +55,7 @@ public partial class CheckoutSummary : ComponentBase
_loadingAddresses = true;
try
{
var response = await UserAddressContract.GetCustomerAddressesAsync(new());
var response = await UserAddressContract.GetAllUserAddressByFilterAsync(new());
if (response?.Models?.Any() == true)
{
_addresses = response.Models.ToList();
@@ -73,14 +64,12 @@ public partial class CheckoutSummary : ComponentBase
else
{
_addresses = new();
_selectedAddress = null;
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری آدرس‌ها: {ex.Message}", Severity.Error);
_addresses = new();
_selectedAddress = null;
}
finally
{
@@ -89,30 +78,6 @@ public partial class CheckoutSummary : ComponentBase
}
}
private async Task OpenAddAddressDialog()
{
var dialog = await DialogService.ShowAsync<AddAddressDialog>("افزودن آدرس جدید");
var result = await dialog.Result;
if (result is { Canceled: false })
{
await AuthService.RefreshTokenAsync();
await LoadAddresses();
}
}
private async Task OpenEditAddressDialog(CustomerAddressModel address)
{
var dialog = await DialogService.ShowAsync<EditAddressDialog>("ویرایش آدرس", new DialogParameters<EditAddressDialog>
{
{ x => x.Model, address }
});
var result = await dialog.Result;
if (result is { Canceled: false })
{
await LoadAddresses();
}
}
private async Task PlaceOrder()
{
if (!CanPlaceOrder || _selectedAddress is null)
@@ -121,15 +86,11 @@ public partial class CheckoutSummary : ComponentBase
return;
}
var totalRequired = VAT.AddVAT(Cart.Total);
if (await TryHandleInsufficientBalanceAsync(totalRequired))
return;
try
{
var request = new SubmitShopBuyOrderRequest
{
TotalAmount = totalRequired
TotalAmount = VAT.AddVAT(Cart.Total)
};
var response = await UserOrderContract.SubmitShopBuyOrderAsync(request);
@@ -139,87 +100,17 @@ public partial class CheckoutSummary : ComponentBase
}
catch (Exception ex)
{
if (CreditChargeNavigation.IsInsufficientWalletBalance(ex))
{
await LoadWalletBalance();
await TryHandleInsufficientBalanceAsync(totalRequired);
return;
}
Snackbar.Add($"خطا در ثبت سفارش: {ex.Message}", Severity.Error);
}
}
private async Task<bool> TryHandleInsufficientBalanceAsync(long totalRequired)
{
if (walletBalance >= totalRequired)
return false;
var shortfall = totalRequired - walletBalance;
var allowCharge = false;
var hasPurchasedPackage = false;
var isMagicWallet = false;
var magicCeilingFull = false;
try
{
var userInfo = await AuthService.GetUserAuthInfo();
allowCharge = userInfo.IsClubMemberActive;
hasPurchasedPackage = userInfo.HasPurchasedPackage;
}
catch
{
allowCharge = false;
}
try
{
var magicStatus = await WalletService.GetMagicWalletStatusAsync();
isMagicWallet = magicStatus.WalletMode == 1;
magicCeilingFull = isMagicWallet && magicStatus.MagicRemainingDeposit <= 0;
}
catch
{
isMagicWallet = false;
magicCeilingFull = false;
}
var parameters = new DialogParameters<InsufficientCreditDialog>
{
{ x => x.CurrentBalance, walletBalance },
{ x => x.RequiredAmount, totalRequired },
{ x => x.ShortfallAmount, shortfall },
{ x => x.AllowChargeCredit, allowCharge },
{ x => x.HasPurchasedPackage, hasPurchasedPackage },
{ x => x.IsMagicWallet, isMagicWallet },
{ x => x.MagicCeilingFull, magicCeilingFull }
};
var dialog = await DialogService.ShowAsync<InsufficientCreditDialog>(
"موجودی کافی نیست",
parameters,
new DialogOptions
{
CloseOnEscapeKey = true,
MaxWidth = MaxWidth.Small,
FullWidth = true
});
var result = await dialog.Result;
if (allowCharge && result is { Canceled: false } && result.Data is long chargeShortfall)
{
var chargeAmount = CreditChargeNavigation.NormalizeChargeAmount(chargeShortfall);
Navigation.NavigateTo(CreditChargeNavigation.BuildChargeUrl(
chargeAmount,
RouteConstants.Store.CheckoutSummary));
}
return true;
}
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
/// <summary>
/// محاسبه مالیات بر ارزش افزوده
/// </summary>
private long CalculateVAT() => VAT.CalculateVAT(Cart.Total);
private static string GetProductImageUrl(string? imageUrl)
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/');
}
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl;
}
@@ -6,33 +6,27 @@
@if (_loading)
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<LoadingState />
<MudStack AlignItems="AlignItems.Center">
<MudProgressCircular Indeterminate="true" Color="Color.Primary" />
</MudStack>
</MudContainer>
}
else if (_order is null)
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<EmptyState Icon="@Icons.Material.Filled.SearchOff"
Title="سفارش یافت نشد."
ActionText="بازگشت"
ActionHref="@RouteConstants.Store.Orders" />
<MudAlert Severity="Severity.Warning">سفارش یافت نشد.</MudAlert>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" OnClick="() => Navigation.NavigateTo(RouteConstants.Store.Orders)">بازگشت</MudButton>
</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">
@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.h5">سفارش #@_order.Id</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">تاریخ: @_order.PaymentDate.ToDateTime().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">
@@ -46,14 +40,14 @@ else
<RowTemplate>
<MudTd>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<AppImage Path="@GetProductImageUrl(context.ProductThumbnailPath)" Alt="@context.ProductTitle"
ImgWidth="60" ImgHeight="60" Class="rounded-lg" ObjectFit="ObjectFit.Cover" />
<MudText>@context.ProductTitle</MudText>
<MudImage Src="@GetProductImageUrl(context.ProductThumbnailPath)" Alt="@context.ProductTitle"
Width="60" Height="60" Class="rounded-circle" />
<MudText>@context.ProductThumbnailPath</MudText>
</MudStack>
</MudTd>
<MudTd>@FormatPrice(context.UnitPrice ?? 0)</MudTd>
<MudTd>@FormatPrice(context.UnitPrice.Value)</MudTd>
<MudTd>@context.Count</MudTd>
<MudTd>@FormatPrice((context.UnitPrice ?? 0) * (context.Count ?? 0))</MudTd>
<MudTd>@FormatPrice(context.UnitPrice.Value*context.Count.Value)</MudTd>
</RowTemplate>
</MudTable>
</MudHidden>
@@ -65,15 +59,15 @@ else
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
<MudStack Spacing="1">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<AppImage Path="@GetProductImageUrl(it.ProductThumbnailPath)" Alt="@it.ProductTitle"
ImgWidth="60" ImgHeight="60" Class="rounded-lg" ObjectFit="ObjectFit.Cover" />
<MudImage Src="@GetProductImageUrl(it.ProductThumbnailPath)" Alt="@it.ProductTitle"
Width="60" Height="60" Class="rounded-circle" />
<MudText>@it.ProductTitle</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Class="mud-text-secondary">@FormatPrice(it.UnitPrice ?? 0) واحد</MudText>
<MudText Class="mud-text-secondary">@FormatPrice(it.UnitPrice.Value) واحد</MudText>
<MudText>تعداد: @it.Count</MudText>
</MudStack>
<MudText>جمع: @FormatPrice((it.UnitPrice ?? 0) * (it.Count ?? 0))</MudText>
<MudText>جمع: @FormatPrice(it.UnitPrice.Value*it.Count.Value)</MudText>
</MudStack>
</MudPaper>
}
@@ -83,7 +77,7 @@ else
@* نمایش جزئیات مالی *@
@{
var subtotal = _order.FactorDetails.Sum(s => (s.UnitPrice ?? 0) * (s.Count ?? 0));
var subtotal = _order.FactorDetails.Sum(s => s.UnitPrice.Value * s.Count.Value);
}
<MudStack Spacing="1" Class="pa-2" Style="background-color: var(--mud-palette-background-grey); border-radius: 8px;">
<MudStack Row="true" Justify="Justify.SpaceBetween">
@@ -100,15 +94,15 @@ else
</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.VatInfo.TotalAmount)</MudText>
<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>
</MudStack>
}
else
{
<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(subtotal)</MudText>
<MudText Typo="Typo.subtitle1" Style="font-weight: bold;">مبلغ قابل پرداخت:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Style="font-weight: bold;">@FormatPrice(subtotal)</MudText>
</MudStack>
}
</MudStack>
@@ -1,7 +1,6 @@
using CMSMicroservice.Protobuf.Protos.UserOrder;
using FrontOffice.BFF.UserOrder.Protobuf.Protos.UserOrder;
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using Messages = CMSMicroservice.Protobuf.Protos;
namespace FrontOffice.Main.Pages.Store;
@@ -23,27 +22,28 @@ public partial class OrderDetail : ComponentBase
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
private string GetStatusText(Messages.PaymentStatus orderPaymentStatus)
private string GetStatusText(PaymentStatus orderPaymentStatus)
{
return orderPaymentStatus switch
{
Messages.PaymentStatus.Pending => "در انتظار پرداخت",
Messages.PaymentStatus.Success => "پرداخت شده",
Messages.PaymentStatus.Reject => "پرداخت ناموفق",
PaymentStatus.Pending => "در انتظار پرداخت",
PaymentStatus.Success => "پرداخت شده",
PaymentStatus.Reject => "پرداخت ناموفق",
_ => "نامشخص",
};
}
private string GetPaymentMethodText(Messages.PaymentMethod orderPaymentMethod)
private string GetPaymentMethodText(PaymentMethod orderPaymentMethod)
{
return orderPaymentMethod switch
{
Messages.PaymentMethod.Wallet => "کیف پول",
Messages.PaymentMethod.Ipg => "درگاه پرداخت اینترنتی",
PaymentMethod.Wallet => "کیف پول",
PaymentMethod.Ipg => "درگاه پرداخت اینترنتی",
_ => "نامشخص",
};
}
private static string GetProductImageUrl(string? imageUrl)
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/');
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : UrlUtility.DownloadUrl+imageUrl;
}

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