Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 58037f0b49 | |||
| aae4b2693d | |||
| 61fd805c75 | |||
| ef6d233bbc | |||
| 3d84e726db | |||
| 13091849cf | |||
| 8e98c50bdf | |||
| 6123e47968 | |||
| acad1a719f | |||
| c7e6d01c11 | |||
| c764614d26 | |||
| 9203dc7924 | |||
| b64d99c5b3 | |||
| 4989cd00e0 | |||
| a708727241 | |||
| bed35935ad | |||
| 2339ca99b4 | |||
| ceaaac3e23 | |||
| 7f5a4f2c37 | |||
| db35814ce3 | |||
| 06100ca6dd | |||
| 7a5a2110c6 | |||
| 8dbc64cd86 | |||
| 2573729db5 | |||
| deadd197e6 | |||
| 5d58d354d7 | |||
| 671c8407e5 | |||
| 454d27f37e | |||
| 1956dcc5f4 | |||
| 37c7cebf92 | |||
| d59d829d5f | |||
| 81d139cdab | |||
| d44e99f4ab | |||
| cd835ae6f9 | |||
| 3c216feb32 | |||
| 7f8f99d4eb | |||
| 8766c29a5a | |||
| 8080b75f05 | |||
| 34c5c508e0 | |||
| 032b8eb448 | |||
| c2fd8790b6 | |||
| 088ea8dc7f | |||
| d8289237e9 | |||
| cd92a2f7b0 | |||
| 2874b931df | |||
| 0c9eb5f9ef | |||
| e5b1e11895 | |||
| 94fbf49fb9 | |||
| 0b1cc1c1c8 | |||
| eef13d1471 | |||
| 4330a5eaa5 | |||
| 77a835311a | |||
| 738119cf6d | |||
| 28a55246df | |||
| ceaf6de226 | |||
| f19711eabc | |||
| 310400b9e7 | |||
| 3600fc7709 | |||
| ae5ab1492e | |||
| 231da2cbaa | |||
| 1e5e6bcf3d | |||
| a1d67c5865 | |||
| d751c06a67 | |||
| b00af9e326 |
@@ -0,0 +1,82 @@
|
|||||||
|
name: Build and Deploy to Kubernetes
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- kub-stage
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: 194.5.195.53:30080
|
||||||
|
IMAGE_NAME: admin/frontoffice
|
||||||
|
K8S_SERVER: 194.5.195.53
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: 194.5.195.53:32082/docker-sshpass:latest
|
||||||
|
options: --privileged
|
||||||
|
steps:
|
||||||
|
- name: Start Docker daemon
|
||||||
|
run: |
|
||||||
|
mkdir -p /etc/docker
|
||||||
|
cat > /etc/docker/daemon.json << 'DAEMON'
|
||||||
|
{
|
||||||
|
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
|
||||||
|
}
|
||||||
|
DAEMON
|
||||||
|
echo "🚀 Starting Docker daemon..."
|
||||||
|
dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs &
|
||||||
|
|
||||||
|
# Wait up to 3 minutes for Docker to be ready
|
||||||
|
for i in $(seq 1 90); do
|
||||||
|
if docker info >/dev/null 2>&1; then
|
||||||
|
echo "✅ Docker daemon is ready (attempt $i)"
|
||||||
|
docker version
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "⏳ Waiting for Docker daemon... (attempt $i/90)"
|
||||||
|
sleep 2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Final check
|
||||||
|
if ! docker info >/dev/null 2>&1; then
|
||||||
|
echo "❌ Docker daemon failed to start after 3 minutes"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
run: |
|
||||||
|
git clone --depth 1 --branch kub-stage http://gitea-svc:3000/admin/FrontOffice.git .
|
||||||
|
|
||||||
|
- name: Login to Docker registries
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 194.5.195.53:32082 -u admin --password-stdin
|
||||||
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
|
||||||
|
|
||||||
|
- name: Build Docker Image
|
||||||
|
run: |
|
||||||
|
cd src
|
||||||
|
DOCKER_BUILDKIT=0 docker build --network host -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest -f FrontOffice.Main/Dockerfile .
|
||||||
|
|
||||||
|
- name: Push to Registry
|
||||||
|
run: |
|
||||||
|
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||||
|
|
||||||
|
- name: Deploy to Kubernetes
|
||||||
|
run: |
|
||||||
|
export SSHPASS="${{ secrets.SERVER_PASSWORD }}"
|
||||||
|
|
||||||
|
# Copy K8s manifests to server
|
||||||
|
sshpass -e scp -o StrictHostKeyChecking=no k8s/staging/frontoffice-deployment.yaml root@${{ env.K8S_SERVER }}:/tmp/frontoffice-deployment.yaml
|
||||||
|
|
||||||
|
# Apply manifests and restart
|
||||||
|
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
|
||||||
|
kubectl apply -f /tmp/frontoffice-deployment.yaml &&
|
||||||
|
crictl rmi ${REGISTRY}/${IMAGE_NAME}:latest 2>/dev/null || true &&
|
||||||
|
kubectl rollout restart deployment/frontoffice &&
|
||||||
|
kubectl rollout status deployment/frontoffice --timeout=180s &&
|
||||||
|
rm -f /tmp/frontoffice-deployment.yaml
|
||||||
|
"
|
||||||
|
echo "✅ Deployed!"
|
||||||
@@ -69,8 +69,15 @@ jobs:
|
|||||||
- name: Deploy to Production
|
- name: Deploy to Production
|
||||||
run: |
|
run: |
|
||||||
export SSHPASS="${{ secrets.SERVER_PASSWORD }}"
|
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 }} "
|
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
|
||||||
kubectl set image deployment/frontoffice frontoffice=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
|
kubectl apply -f /tmp/frontoffice-deployment.yaml &&
|
||||||
kubectl rollout status deployment/frontoffice --timeout=300s
|
kubectl rollout restart deployment/frontoffice &&
|
||||||
|
kubectl rollout status deployment/frontoffice --timeout=300s &&
|
||||||
|
rm -f /tmp/frontoffice-deployment.yaml
|
||||||
"
|
"
|
||||||
echo "✅ Deployed to Production!"
|
echo "✅ Deployed to Production!"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
# Docs moved to totalDoc
|
# Docs moved to totalDoc
|
||||||
|
|
||||||
See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation.
|
See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation.
|
||||||
|
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
# سرویسهای 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.9 | `OrderService.CalculateOrderPVAsync` | `UserOrderContract.CalculateOrderPV` | `/order/{id}` | محاسبه PV سفارش |
|
||||||
|
| 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` |
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
# 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
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
# 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
|
||||||
@@ -30,6 +30,7 @@ using CMSMicroservice.Protobuf.Protos.DiscountCategory;
|
|||||||
using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
|
using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
|
||||||
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
|
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
|
||||||
using FrontOffice.Main.Utilities;
|
using FrontOffice.Main.Utilities;
|
||||||
|
using FrontOffice.Main.Utilities.Seo;
|
||||||
|
|
||||||
namespace Microsoft.Extensions.DependencyInjection;
|
namespace Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
@@ -56,6 +57,7 @@ public static class ConfigureServices
|
|||||||
services.AddSingleton<UserAuthInfo>();
|
services.AddSingleton<UserAuthInfo>();
|
||||||
services.AddScoped<AuthService>();
|
services.AddScoped<AuthService>();
|
||||||
services.AddScoped<AuthDialogService>();
|
services.AddScoped<AuthDialogService>();
|
||||||
|
services.AddScoped<GuestActionGate>();
|
||||||
// Storefront services
|
// Storefront services
|
||||||
services.AddScoped<CartService>();
|
services.AddScoped<CartService>();
|
||||||
services.AddScoped<ProductService>();
|
services.AddScoped<ProductService>();
|
||||||
@@ -94,26 +96,45 @@ public static class ConfigureServices
|
|||||||
// SignalR Token Notification Service
|
// SignalR Token Notification Service
|
||||||
services.AddScoped<TokenNotificationService>();
|
services.AddScoped<TokenNotificationService>();
|
||||||
|
|
||||||
|
// SEO
|
||||||
|
services.AddScoped<SeoMetadataProvider>();
|
||||||
|
services.AddScoped<SitemapGenerator>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IServiceCollection AddGrpcServices(this IServiceCollection services, IConfiguration configuration)
|
public static IServiceCollection AddGrpcServices(this IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
var baseUrl = configuration["GwUrl"];
|
var baseUrl = ResolveGatewayUrl(configuration)
|
||||||
|
?? throw new InvalidOperationException("Gateway URL is missing. Set GW_URL or GwUrl.");
|
||||||
|
|
||||||
// Register optimized HttpClient for gRPC
|
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.
|
||||||
services.AddScoped(sp =>
|
services.AddScoped(sp =>
|
||||||
{
|
{
|
||||||
var handler = new HttpClientHandler
|
HttpMessageHandler inner = isHttp
|
||||||
{
|
? new SocketsHttpHandler
|
||||||
MaxConnectionsPerServer = 10,
|
{
|
||||||
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate
|
MaxConnectionsPerServer = 10,
|
||||||
};
|
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate
|
||||||
|
}
|
||||||
|
: new HttpClientHandler
|
||||||
|
{
|
||||||
|
MaxConnectionsPerServer = 10,
|
||||||
|
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate
|
||||||
|
};
|
||||||
|
|
||||||
return new HttpClient(new GrpcWebHandler(GrpcWebMode.GrpcWeb, handler))
|
return new HttpClient(new GrpcWebHandler(GrpcWebMode.GrpcWeb, inner))
|
||||||
{
|
{
|
||||||
Timeout = TimeSpan.FromMinutes(10),
|
Timeout = TimeSpan.FromMinutes(10),
|
||||||
BaseAddress = new Uri(baseUrl)
|
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
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -151,12 +172,22 @@ public static class ConfigureServices
|
|||||||
return services;
|
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)
|
private static TClient CreateAuthenticatedClient<TClient>(IServiceProvider sp)
|
||||||
where TClient : class
|
where TClient : class
|
||||||
{
|
{
|
||||||
var httpClient = sp.GetRequiredService<HttpClient>();
|
var httpClient = sp.GetRequiredService<HttpClient>();
|
||||||
var localStorage = sp.GetRequiredService<ILocalStorageService>();
|
var localStorage = sp.GetRequiredService<ILocalStorageService>();
|
||||||
var baseUrl = httpClient.BaseAddress?.ToString() ?? throw new InvalidOperationException("Base URL not configured");
|
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) =>
|
var credentials = CallCredentials.FromInterceptor(async (context, metadata) =>
|
||||||
{
|
{
|
||||||
@@ -176,10 +207,14 @@ public static class ConfigureServices
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var channelCredentials = isHttps
|
||||||
|
? ChannelCredentials.Create(new SslCredentials(), credentials)
|
||||||
|
: ChannelCredentials.Create(ChannelCredentials.Insecure, credentials);
|
||||||
|
|
||||||
var channel = GrpcChannel.ForAddress(baseUrl, new GrpcChannelOptions
|
var channel = GrpcChannel.ForAddress(baseUrl, new GrpcChannelOptions
|
||||||
{
|
{
|
||||||
UnsafeUseInsecureChannelCallCredentials = true,
|
UnsafeUseInsecureChannelCallCredentials = !isHttps,
|
||||||
Credentials = ChannelCredentials.Create(new SslCredentials(), credentials),
|
Credentials = channelCredentials,
|
||||||
HttpClient = httpClient,
|
HttpClient = httpClient,
|
||||||
MaxReceiveMessageSize = 1000 * 1024 * 1024, // 1 GB
|
MaxReceiveMessageSize = 1000 * 1024 * 1024, // 1 GB
|
||||||
MaxSendMessageSize = 1000 * 1024 * 1024 // 1 GB
|
MaxSendMessageSize = 1000 * 1024 * 1024 // 1 GB
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ FROM 194.5.195.53:32082/dotnet/aspnet:9.0 AS runtime
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=build /app/publish .
|
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
|
ENV ASPNETCORE_URLS=http://+:80
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
|
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
|
||||||
<!-- Replace all FrontOffice.BFF protobuf packages with CMS protobuf -->
|
<!-- Replace all FrontOffice.BFF protobuf packages with CMS protobuf -->
|
||||||
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.192" />
|
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.204" />
|
||||||
<!-- <ProjectReference Include="../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj" />-->
|
<!-- <ProjectReference Include="../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj" />-->
|
||||||
<PackageReference Include="MudBlazor" Version="8.14.0" />
|
<PackageReference Include="MudBlazor" Version="8.14.0" />
|
||||||
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
|
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
|
||||||
|
|||||||
@@ -71,80 +71,6 @@
|
|||||||
</MudStack>
|
</MudStack>
|
||||||
}
|
}
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
<!-- Address Selection -->
|
|
||||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
|
||||||
<MudText Typo="Typo.h5" Class="mb-4">انتخاب آدرس</MudText>
|
|
||||||
|
|
||||||
@if (_isLoadingAddresses)
|
|
||||||
{
|
|
||||||
<MudStack AlignItems="AlignItems.Center" Class="py-4">
|
|
||||||
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Medium" />
|
|
||||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-2">بارگذاری آدرسها...</MudText>
|
|
||||||
</MudStack>
|
|
||||||
}
|
|
||||||
else if (_addresses.Any())
|
|
||||||
{
|
|
||||||
<MudStack Spacing="2">
|
|
||||||
@foreach (var address in _addresses)
|
|
||||||
{
|
|
||||||
<MudPaper Outlined="@(_selectedAddress?.Id != address.Id)"
|
|
||||||
Elevation="@(_selectedAddress?.Id == address.Id ? 4 : 0)"
|
|
||||||
Class="pa-3 rounded-xl cursor-pointer"
|
|
||||||
Style="@(_selectedAddress?.Id == address.Id ? "border: 2px solid var(--mud-palette-primary);" : "")"
|
|
||||||
@onclick="() => SetAddressAsDefault(address.Id)">
|
|
||||||
<MudStack Spacing="1">
|
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
|
||||||
<MudCheckBox @bind-Value="address.IsDefault"
|
|
||||||
Disabled="true"
|
|
||||||
Color="Color.Primary" />
|
|
||||||
<MudText Typo="Typo.subtitle2">@(address.Title)</MudText>
|
|
||||||
@if (address.IsDefault)
|
|
||||||
{
|
|
||||||
<MudChip T="string" Color="Color.Success" Variant="Variant.Filled" Size="Size.Small">پیشفرض</MudChip>
|
|
||||||
}
|
|
||||||
@if (!address.IsDefault)
|
|
||||||
{
|
|
||||||
@if (_isSettingDefaultAddress && _settingDefaultAddressId == address.Id)
|
|
||||||
{
|
|
||||||
<MudProgressCircular Size="Size.Small" Color="Color.Primary" Indeterminate="true" />
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<MudButton Variant="Variant.Text"
|
|
||||||
Color="Color.Primary"
|
|
||||||
Size="Size.Small"
|
|
||||||
OnClick="() => SetAddressAsDefault(address.Id)">
|
|
||||||
تنظیم به عنوان پیشفرض
|
|
||||||
</MudButton>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</MudStack>
|
|
||||||
<MudStack Row="true">
|
|
||||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">@(address.Address)</MudText>
|
|
||||||
<MudSpacer />
|
|
||||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">کد پستی: @(address.PostalCode)</MudText>
|
|
||||||
</MudStack>
|
|
||||||
</MudStack>
|
|
||||||
</MudPaper>
|
|
||||||
}
|
|
||||||
</MudStack>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<MudStack AlignItems="AlignItems.Center" Class="py-4">
|
|
||||||
<MudIcon Icon="@Icons.Material.Filled.LocationOff" Size="Size.Large" Color="Color.Default" />
|
|
||||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-2">آدرسی ثبت نشده است.</MudText>
|
|
||||||
<MudButton Variant="Variant.Outlined"
|
|
||||||
Color="Color.Primary"
|
|
||||||
StartIcon="@Icons.Material.Filled.Add"
|
|
||||||
OnClick="() => Navigation.NavigateTo(RouteConstants.Profile.Index)"
|
|
||||||
Class="mt-2">
|
|
||||||
افزودن آدرس
|
|
||||||
</MudButton>
|
|
||||||
</MudStack>
|
|
||||||
}
|
|
||||||
</MudPaper>
|
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
|
||||||
@@ -241,7 +167,7 @@
|
|||||||
@if (!CanProceedToPayment)
|
@if (!CanProceedToPayment)
|
||||||
{
|
{
|
||||||
<MudText Typo="Typo.caption" Color="Color.Error" Align="Align.Center">
|
<MudText Typo="Typo.caption" Color="Color.Error" Align="Align.Center">
|
||||||
لطفاً پکیج و آدرس را انتخاب کنید.
|
لطفاً پکیج را انتخاب کنید.
|
||||||
</MudText>
|
</MudText>
|
||||||
}
|
}
|
||||||
</MudStack>
|
</MudStack>
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
|
|
||||||
using CMSMicroservice.Protobuf.Protos.Package;
|
using CMSMicroservice.Protobuf.Protos.Package;
|
||||||
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
|
||||||
using FrontOffice.Main.Utilities;
|
using FrontOffice.Main.Utilities;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
@@ -13,14 +11,10 @@ public partial class Checkout
|
|||||||
{
|
{
|
||||||
[Inject] private PackageService PackageService { get; set; } = default!;
|
[Inject] private PackageService PackageService { get; set; } = default!;
|
||||||
[Inject] private PackageContract.PackageContractClient PackageClient { get; set; } = default!;
|
[Inject] private PackageContract.PackageContractClient PackageClient { get; set; } = default!;
|
||||||
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
|
||||||
|
|
||||||
[Parameter] public long? PackageId { get; set; }
|
[Parameter] public long? PackageId { get; set; }
|
||||||
|
|
||||||
private Pack? _selectedPackage;
|
private Pack? _selectedPackage;
|
||||||
private List<CustomerAddressModel> _addresses = new();
|
|
||||||
private CustomerAddressModel? _selectedAddress;
|
|
||||||
private bool _isLoadingAddresses;
|
|
||||||
private bool _isProcessingPayment;
|
private bool _isProcessingPayment;
|
||||||
|
|
||||||
// Discount code
|
// Discount code
|
||||||
@@ -31,16 +25,11 @@ public partial class Checkout
|
|||||||
private long _discountAmount;
|
private long _discountAmount;
|
||||||
private long _finalPrice;
|
private long _finalPrice;
|
||||||
|
|
||||||
// Address management
|
private bool CanProceedToPayment => _selectedPackage != null;
|
||||||
private bool _isSettingDefaultAddress;
|
|
||||||
private long? _settingDefaultAddressId;
|
|
||||||
|
|
||||||
private bool CanProceedToPayment => _selectedPackage != null && _selectedAddress != null;
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
await LoadPackageDetails();
|
await LoadPackageDetails();
|
||||||
await LoadAddresses();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadPackageDetails()
|
private async Task LoadPackageDetails()
|
||||||
@@ -72,60 +61,6 @@ public partial class Checkout
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadAddresses()
|
|
||||||
{
|
|
||||||
_isLoadingAddresses = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var response = await UserAddressContract.GetCustomerAddressesAsync(request: new());
|
|
||||||
if (response?.Models?.Any() == true)
|
|
||||||
{
|
|
||||||
_addresses = response.Models.ToList();
|
|
||||||
// Select default address if available
|
|
||||||
_selectedAddress = _addresses.FirstOrDefault(a => a.IsDefault) ?? _addresses.First();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_addresses = new List<CustomerAddressModel>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Snackbar.Add($"خطا در بارگذاری آدرسها: {ex.Message}", Severity.Error);
|
|
||||||
_addresses = new List<CustomerAddressModel>();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_isLoadingAddresses = false;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SetAddressAsDefault(long addressId)
|
|
||||||
{
|
|
||||||
if (_isSettingDefaultAddress) return;
|
|
||||||
|
|
||||||
_isSettingDefaultAddress = true;
|
|
||||||
_settingDefaultAddressId = addressId;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await UserAddressContract.SetAddressAsDefaultAsync(new() { Id = addressId });
|
|
||||||
await LoadAddresses(); // Reload addresses to reflect the change
|
|
||||||
Snackbar.Add("آدرس پیشفرض با موفقیت تغییر یافت.", Severity.Success);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Snackbar.Add($"خطا در تغییر آدرس پیشفرض: {ex.Message}", Severity.Error);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_isSettingDefaultAddress = false;
|
|
||||||
_settingDefaultAddressId = null;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ApplyDiscountCode()
|
private async Task ApplyDiscountCode()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(_discountCode))
|
if (string.IsNullOrWhiteSpace(_discountCode))
|
||||||
@@ -157,7 +92,7 @@ public partial class Checkout
|
|||||||
_finalPrice = _selectedPackage!.Price;
|
_finalPrice = _selectedPackage!.Price;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
_discountMessage = "خطا در اعمال کد تخفیف.";
|
_discountMessage = "خطا در اعمال کد تخفیف.";
|
||||||
_discountApplied = false;
|
_discountApplied = false;
|
||||||
@@ -173,9 +108,11 @@ public partial class Checkout
|
|||||||
|
|
||||||
private async Task ProcessPayment()
|
private async Task ProcessPayment()
|
||||||
{
|
{
|
||||||
if (!CanProceedToPayment || _selectedPackage == null || _selectedAddress == null)
|
if (_isProcessingPayment) return;
|
||||||
|
|
||||||
|
if (!CanProceedToPayment || _selectedPackage == null)
|
||||||
{
|
{
|
||||||
Snackbar.Add("لطفاً پکیج و آدرس را انتخاب کنید.", Severity.Warning);
|
Snackbar.Add("لطفاً پکیج را انتخاب کنید.", Severity.Warning);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,9 +154,9 @@ public partial class Checkout
|
|||||||
|
|
||||||
private async Task DayaLoanPayment()
|
private async Task DayaLoanPayment()
|
||||||
{
|
{
|
||||||
if (_selectedAddress == null)
|
if (_selectedPackage == null)
|
||||||
{
|
{
|
||||||
Snackbar.Add("لطفاً آدرس را انتخاب کنید.", Severity.Warning);
|
Snackbar.Add("لطفاً پکیج را انتخاب کنید.", Severity.Warning);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Color="Color.Success" Size="Size.Large" />
|
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Color="Color.Success" Size="Size.Large" />
|
||||||
<MudStack Spacing="0">
|
<MudStack Spacing="0">
|
||||||
<MudText Typo="Typo.subtitle1">شارژ کیف پول فروشگاه اعتباری</MudText>
|
<MudText Typo="Typo.subtitle1">شارژ کیف پول فروشگاه اعتباری</MudText>
|
||||||
<MudText Typo="Typo.caption" Color="Color.Default">شارژ ۵۶ میلیون تومان کیف پول فروشگاه اعتباری</MudText>
|
<MudText Typo="Typo.caption" Color="Color.Default">شارژ برابر ارزش پکیج فعال در کیف پول فروشگاه اعتباری</MudText>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
<MudDivider />
|
<MudDivider />
|
||||||
|
|||||||
@@ -7,9 +7,15 @@ public partial class Cart : IDisposable
|
|||||||
{
|
{
|
||||||
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
|
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
|
||||||
[Inject] private VATService VAT { 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()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
if (!await AuthService.IsAuthenticatedAsync())
|
||||||
|
{
|
||||||
|
await AuthDialogService.ShowAuthDialogAsync();
|
||||||
|
}
|
||||||
await DiscountCart.EnsureInitializedAsync();
|
await DiscountCart.EnsureInitializedAsync();
|
||||||
DiscountCart.OnChange += StateHasChanged;
|
DiscountCart.OnChange += StateHasChanged;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ public partial class Checkout
|
|||||||
[Inject] private DiscountOrderService DiscountOrderService { get; set; } = default!;
|
[Inject] private DiscountOrderService DiscountOrderService { get; set; } = default!;
|
||||||
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
||||||
[Inject] private VATService VAT { 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 List<CustomerAddressModel> _addresses = new();
|
||||||
private CustomerAddressModel? _selectedAddress;
|
private CustomerAddressModel? _selectedAddress;
|
||||||
@@ -32,9 +34,17 @@ public partial class Checkout
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
if (!await AuthService.IsAuthenticatedAsync())
|
||||||
|
{
|
||||||
|
await AuthDialogService.ShowAuthDialogAsync();
|
||||||
|
}
|
||||||
await VAT.LoadAsync();
|
await VAT.LoadAsync();
|
||||||
await DiscountCart.EnsureInitializedAsync();
|
await DiscountCart.EnsureInitializedAsync();
|
||||||
await LoadAddresses();
|
var userInfo = await AuthService.GetUserAuthInfo();
|
||||||
|
if (userInfo.HasAddress)
|
||||||
|
await LoadAddresses();
|
||||||
|
else
|
||||||
|
_addresses = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadAddresses()
|
private async Task LoadAddresses()
|
||||||
|
|||||||
@@ -63,49 +63,58 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<!-- Price & Discount -->
|
<!-- Price & Discount -->
|
||||||
<MudPaper Class="pa-4 rounded-lg discount-price-box" Elevation="0">
|
@if (_isAuthenticated)
|
||||||
<MudStack Spacing="2">
|
{
|
||||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
<MudPaper Class="pa-4 rounded-lg discount-price-box" Elevation="0">
|
||||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">قیمت محصول:</MudText>
|
<MudStack Spacing="2">
|
||||||
<MudText Typo="Typo.body1" Class="fw-bold">
|
|
||||||
@($"{_product.Price:N0}") تومان
|
|
||||||
</MudText>
|
|
||||||
</MudStack>
|
|
||||||
@if (VAT.IsEnabled)
|
|
||||||
{
|
|
||||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
<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">قیمت محصول:</MudText>
|
||||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">
|
<MudText Typo="Typo.body1" Class="fw-bold">
|
||||||
@($"{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}") تومان
|
@($"{_product.Price:N0}") تومان
|
||||||
</MudText>
|
</MudText>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
}
|
@if (VAT.IsEnabled)
|
||||||
@if (_product.MaxDiscountPercent > 0)
|
{
|
||||||
{
|
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||||
<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">اعتبار از کیف اعتباری:</MudText>
|
<MudText Typo="Typo.body2" Class="mud-text-secondary">
|
||||||
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">
|
@($"{VAT.CalculateVAT(_product.Price):N0}") تومان
|
||||||
@_product.MaxDiscountPercent% (@($"{_product.Price * _product.MaxDiscountPercent / 100:N0}") تومان)
|
</MudText>
|
||||||
</MudChip>
|
</MudStack>
|
||||||
</MudStack>
|
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||||
}
|
<MudText Typo="Typo.body2" Class="mud-text-secondary">قیمت با مالیات:</MudText>
|
||||||
</MudStack>
|
<MudText Typo="Typo.h5" Color="Color.Primary" Class="fw-bold">
|
||||||
</MudPaper>
|
@($"{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 -->
|
<!-- Stock & Stats -->
|
||||||
<MudStack Row="true" Spacing="3" Class="flex-wrap">
|
<MudStack Row="true" Spacing="3" Class="flex-wrap">
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ public partial class ProductDetail
|
|||||||
[Inject] private DiscountProductService DiscountProductService { get; set; } = default!;
|
[Inject] private DiscountProductService DiscountProductService { get; set; } = default!;
|
||||||
[Inject] private DiscountCartService DiscountCartService { get; set; } = default!;
|
[Inject] private DiscountCartService DiscountCartService { get; set; } = default!;
|
||||||
[Inject] private VATService VAT { 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 DiscountProductDetail? _product;
|
||||||
private string _selectedImage = string.Empty;
|
private string _selectedImage = string.Empty;
|
||||||
@@ -19,6 +23,7 @@ public partial class ProductDetail
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
|
||||||
await LoadProductAsync();
|
await LoadProductAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,8 +64,14 @@ public partial class ProductDetail
|
|||||||
_addingToCart = true;
|
_addingToCart = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await DiscountCartService.AddAsync(_product.Id, _quantity);
|
var productId = _product.Id;
|
||||||
Snackbar.Add($"{_product.Title} به سبد خرید اضافه شد", MudBlazor.Severity.Success);
|
var qty = _quantity;
|
||||||
|
var title = _product.Title;
|
||||||
|
await GuestGate.RunAsync(async () =>
|
||||||
|
{
|
||||||
|
await DiscountCartService.AddAsync(productId, qty);
|
||||||
|
Snackbar.Add($"{title} به سبد خرید اضافه شد", MudBlazor.Severity.Success);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
{
|
{
|
||||||
<MudItem xs="6" sm="6" md="3"
|
<MudItem xs="6" sm="6" md="3"
|
||||||
onclick="@(() => NavigateToProduct(p.Id))">
|
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"
|
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden"
|
||||||
Style="cursor:pointer;">
|
Style="cursor:pointer;">
|
||||||
<MudCardContent Class="d-flex flex-column pa-1 h-100">
|
<MudCardContent Class="d-flex flex-column pa-1 h-100">
|
||||||
@@ -103,10 +104,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="pa-1 flex-grow-1 d-flex flex-column justify-space-between">
|
<div class="pa-1 flex-grow-1 d-flex flex-column justify-space-between">
|
||||||
<MudText Typo="Typo.subtitle1">@p.Title</MudText>
|
<MudText Typo="Typo.subtitle1">@p.Title</MudText>
|
||||||
<div>
|
@if (_isAuthenticated)
|
||||||
<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>
|
||||||
</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>
|
</div>
|
||||||
</MudCardContent>
|
</MudCardContent>
|
||||||
<MudCardActions Class="mt-auto d-flex justify-space-between pa-2">
|
<MudCardActions Class="mt-auto d-flex justify-space-between pa-2">
|
||||||
@@ -118,6 +128,7 @@
|
|||||||
</MudButton>
|
</MudButton>
|
||||||
</MudCardActions>
|
</MudCardActions>
|
||||||
</MudCard>
|
</MudCard>
|
||||||
|
</div>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
}
|
}
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.AspNetCore.Components.Routing;
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
using FrontOffice.Main.Utilities;
|
using FrontOffice.Main.Utilities;
|
||||||
|
|
||||||
namespace FrontOffice.Main.Pages.DiscountStore;
|
namespace FrontOffice.Main.Pages.DiscountStore;
|
||||||
@@ -8,7 +10,11 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
{
|
{
|
||||||
[Inject] private DiscountProductService ProductService { get; set; } = default!;
|
[Inject] private DiscountProductService ProductService { get; set; } = default!;
|
||||||
[Inject] private DiscountCartService DiscountCart { 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 string _search = string.Empty;
|
||||||
private long? _selectedCategoryId;
|
private long? _selectedCategoryId;
|
||||||
private int _currentPage = 1;
|
private int _currentPage = 1;
|
||||||
@@ -17,41 +23,107 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
private bool _hasMore = true;
|
private bool _hasMore = true;
|
||||||
private int _totalCount;
|
private int _totalCount;
|
||||||
private const int PageSize = 12;
|
private const int PageSize = 12;
|
||||||
|
private const string DefaultSortBy = "price desc";
|
||||||
|
|
||||||
private List<DiscountProductCard> _products = new();
|
private List<DiscountProductCard> _products = new();
|
||||||
private List<DiscountCategoryNode> _categories = new();
|
private List<DiscountCategoryNode> _categories = new();
|
||||||
|
private bool _ignoreNextLocationChange;
|
||||||
|
private bool _pendingScrollRestore;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
_loading = true;
|
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
|
||||||
await DiscountCart.EnsureInitializedAsync();
|
await DiscountCart.EnsureInitializedAsync();
|
||||||
DiscountCart.OnChange += StateHasChanged;
|
DiscountCart.OnChange += StateHasChanged;
|
||||||
var categoriesTask = ProductService.GetCategoriesAsync();
|
Navigation.LocationChanged += HandleLocationChanged;
|
||||||
var productsTask = ProductService.GetProductsAsync(page: 1, pageSize: PageSize);
|
|
||||||
await Task.WhenAll(categoriesTask, productsTask);
|
_categories = await ProductService.GetCategoriesAsync();
|
||||||
_categories = categoriesTask.Result;
|
ApplyStateFromUri();
|
||||||
var result = productsTask.Result;
|
_loading = true;
|
||||||
_products = result.Products;
|
await LoadPages(_currentPage);
|
||||||
_totalCount = result.TotalCount;
|
|
||||||
_hasMore = result.CurrentPage < result.TotalPages;
|
|
||||||
_loading = false;
|
_loading = false;
|
||||||
|
_pendingScrollRestore = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadInitial()
|
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;
|
_loading = true;
|
||||||
_currentPage = 1;
|
_currentPage = 1;
|
||||||
_products.Clear();
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
|
await LoadPages(1);
|
||||||
var result = await ProductService.GetProductsAsync(
|
SyncUrl();
|
||||||
page: 1,
|
|
||||||
pageSize: PageSize,
|
|
||||||
search: string.IsNullOrWhiteSpace(_search) ? null : _search,
|
|
||||||
categoryId: _selectedCategoryId);
|
|
||||||
_products = result.Products;
|
|
||||||
_totalCount = result.TotalCount;
|
|
||||||
_hasMore = result.CurrentPage < result.TotalPages;
|
|
||||||
_loading = false;
|
_loading = false;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
@@ -68,44 +140,73 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
page: _currentPage,
|
page: _currentPage,
|
||||||
pageSize: PageSize,
|
pageSize: PageSize,
|
||||||
search: string.IsNullOrWhiteSpace(_search) ? null : _search,
|
search: string.IsNullOrWhiteSpace(_search) ? null : _search,
|
||||||
categoryId: _selectedCategoryId);
|
categoryId: _selectedCategoryId,
|
||||||
|
sortBy: DefaultSortBy);
|
||||||
_products.AddRange(result.Products);
|
_products.AddRange(result.Products);
|
||||||
_hasMore = result.CurrentPage < result.TotalPages;
|
_hasMore = result.CurrentPage < result.TotalPages;
|
||||||
|
SyncUrl();
|
||||||
|
|
||||||
_loadingMore = false;
|
_loadingMore = false;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SearchProducts() => await LoadInitial();
|
private async Task SearchProducts() => await ReloadFromFilters();
|
||||||
|
|
||||||
private async Task OnSearchKeyUp(KeyboardEventArgs e)
|
private async Task OnSearchKeyUp(KeyboardEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Key == "Enter")
|
if (e.Key == "Enter")
|
||||||
{
|
await ReloadFromFilters();
|
||||||
await LoadInitial();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnCategoryChanged(long? value)
|
private async Task OnCategoryChanged(long? value)
|
||||||
{
|
{
|
||||||
_selectedCategoryId = value;
|
_selectedCategoryId = value;
|
||||||
await LoadInitial();
|
await ReloadFromFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task AddToCart(DiscountProductCard p)
|
private async Task AddToCart(DiscountProductCard p)
|
||||||
{
|
{
|
||||||
await DiscountCart.AddAsync(p.Id);
|
await GuestGate.RunAsync(() => DiscountCart.AddAsync(p.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void NavigateToProduct(long id)
|
private async Task NavigateToProduct(long id)
|
||||||
{
|
{
|
||||||
|
await ShopListScrollRestore.SaveAsync(Js, ShopListScrollRestore.DiscountKey, id);
|
||||||
Navigation.NavigateTo($"{RouteConstants.DiscountStore.ProductDetail}{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} تومان";
|
private static string FormatPrice(long price) => $"{price:N0} تومان";
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
DiscountCart.OnChange -= StateHasChanged;
|
DiscountCart.OnChange -= StateHasChanged;
|
||||||
|
Navigation.LocationChanged -= HandleLocationChanged;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ public partial class FAQ
|
|||||||
Icon = Icons.Material.Filled.Build,
|
Icon = Icons.Material.Filled.Build,
|
||||||
Questions = new List<FAQQuestion>
|
Questions = new List<FAQQuestion>
|
||||||
{
|
{
|
||||||
new() { Question = "شجرهنامه چگونه کار میکند؟", Answer = "شجرهنامه بصری نمایش سلسله مراتبی تیم شما را نشان میدهد و امکان ردیابی روابط ارجاعی را فراهم میکند." },
|
new() { Question = "سازمان فروش چگونه کار میکند؟", Answer = "سازمان فروش بصری نمایش سلسله مراتبی تیم شما را نشان میدهد و امکان ردیابی روابط ارجاعی را فراهم میکند." },
|
||||||
new() { Question = "گزارشگیری به چه صورت است؟", Answer = "سیستم گزارشهای جامع مالی، عملکردی و آماری ارائه میدهد که قابل فیلتر و دانلود به فرمت Excel است." },
|
new() { Question = "گزارشگیری به چه صورت است؟", Answer = "سیستم گزارشهای جامع مالی، عملکردی و آماری ارائه میدهد که قابل فیلتر و دانلود به فرمت Excel است." },
|
||||||
new() { Question = "آیا از موبایل قابل استفاده است؟", Answer = "بله، اپلیکیشن کاملاً responsive است و تجربه کاربری عالی در موبایل و تبلت ارائه میدهد." }
|
new() { Question = "آیا از موبایل قابل استفاده است؟", Answer = "بله، اپلیکیشن کاملاً responsive است و تجربه کاربری عالی در موبایل و تبلت ارائه میدهد." }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<MudStack AlignItems="AlignItems.Center" Spacing="4" Class="text-center">
|
<MudStack AlignItems="AlignItems.Center" Spacing="4" Class="text-center">
|
||||||
<MudChip T="string" Color="Color.Default" Variant="Variant.Filled"
|
<MudChip T="string" Color="Color.Default" Variant="Variant.Filled"
|
||||||
Class="pulse-chip hero-chip-glass" Size="Size.Small">
|
Class="pulse-chip hero-chip-glass" Size="Size.Small">
|
||||||
🌿 پلتفرم سلامتمحور فروش و تیمسازی
|
باشگاه مشتریان KBS کارا بازار سلامت
|
||||||
</MudChip>
|
</MudChip>
|
||||||
|
|
||||||
<MudText Typo="Typo.h1" Class="hero-title" Style="max-width:680px;">
|
<MudText Typo="Typo.h1" Class="hero-title" Style="max-width:680px;">
|
||||||
@@ -172,6 +172,179 @@
|
|||||||
</MudContainer>
|
</MudContainer>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
@* ═══════════════════════════════════════════════
|
||||||
|
1c. TOP-SELLING REGULAR PRODUCTS
|
||||||
|
═══════════════════════════════════════════════ *@
|
||||||
|
@if (_loadingTopProducts)
|
||||||
|
{
|
||||||
|
<section class="section-landing" style="background:var(--mud-palette-background-gray);">
|
||||||
|
<MudContainer MaxWidth="MaxWidth.Large">
|
||||||
|
<MudStack AlignItems="AlignItems.Center" Class="py-4">
|
||||||
|
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Small" />
|
||||||
|
</MudStack>
|
||||||
|
</MudContainer>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@if (_topRegularProducts.Any())
|
||||||
|
{
|
||||||
|
<section class="section-landing" style="background:var(--mud-palette-background-gray);">
|
||||||
|
<MudContainer MaxWidth="MaxWidth.Large">
|
||||||
|
<div class="text-center mb-6 fade-in-up">
|
||||||
|
<MudText Typo="Typo.h3">محصولات پرفروش فروشگاه</MudText>
|
||||||
|
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
|
||||||
|
محبوبترین محصولات کارا بازار سلامت
|
||||||
|
</MudText>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MudGrid Spacing="2" Justify="Justify.FlexStart">
|
||||||
|
@foreach (var p in _topRegularProducts)
|
||||||
|
{
|
||||||
|
<MudItem xs="6" sm="6" md="4">
|
||||||
|
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden landing-product-card"
|
||||||
|
Style="cursor:pointer;"
|
||||||
|
@onclick="() => NavigateToRegularProduct(p.Id)">
|
||||||
|
<MudCardContent Class="d-flex flex-column pa-1 h-100">
|
||||||
|
<div style="aspect-ratio:1/1;width:100%;background-image:url('@p.ImageUrl');background-size:cover;background-position:center;border-radius:0.5rem;position:relative;">
|
||||||
|
@if (p.RemainingCount <= 0)
|
||||||
|
{
|
||||||
|
<div style="position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;border-radius:0.5rem;">
|
||||||
|
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">ناموجود</MudChip>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (p.RemainingCount <= 5)
|
||||||
|
{
|
||||||
|
<MudChip T="string" Color="Color.Warning" Variant="Variant.Filled" Size="Size.Small"
|
||||||
|
Style="position:absolute;top:8px;right:8px;">
|
||||||
|
فقط @p.RemainingCount عدد
|
||||||
|
</MudChip>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="pa-1 flex-grow-1 d-flex flex-column justify-space-between">
|
||||||
|
<MudText Typo="Typo.subtitle1" Style="display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">@p.Title</MudText>
|
||||||
|
@if (_isAuthenticated)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatPrice(p.Price)</MudText>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Small" Class="me-1"/>برای مشاهده قیمت وارد شوید
|
||||||
|
</MudText>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</MudCardContent>
|
||||||
|
<MudCardActions Class="mt-auto pa-2" @onclick:stopPropagation="true">
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
|
||||||
|
StartIcon="@Icons.Material.Filled.AddShoppingCart"
|
||||||
|
Disabled="@(p.RemainingCount <= 0)"
|
||||||
|
OnClick="@(() => AddRegularToCart(p))">
|
||||||
|
@(p.RemainingCount <= 0 ? "ناموجود" : "افزودن به سبد")
|
||||||
|
</MudButton>
|
||||||
|
</MudCardActions>
|
||||||
|
</MudCard>
|
||||||
|
</MudItem>
|
||||||
|
}
|
||||||
|
</MudGrid>
|
||||||
|
|
||||||
|
<div class="text-center mt-6">
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Class="rounded-pill"
|
||||||
|
Href="@RouteConstants.Store.Products"
|
||||||
|
EndIcon="@Icons.Material.Filled.ArrowBack">
|
||||||
|
مشاهده همه محصولات
|
||||||
|
</MudButton>
|
||||||
|
</div>
|
||||||
|
</MudContainer>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
|
@* ═══════════════════════════════════════════════
|
||||||
|
1d. TOP-SELLING DISCOUNT STORE PRODUCTS
|
||||||
|
═══════════════════════════════════════════════ *@
|
||||||
|
@if (_topDiscountProducts.Any())
|
||||||
|
{
|
||||||
|
<section class="section-landing">
|
||||||
|
<MudContainer MaxWidth="MaxWidth.Large">
|
||||||
|
<div class="text-center mb-6 fade-in-up">
|
||||||
|
<MudText Typo="Typo.h3">محصولات پرفروش فروشگاه اعتباری</MudText>
|
||||||
|
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
|
||||||
|
محصولات ویژه اعضای باشگاه با پرداخت اعتباری
|
||||||
|
</MudText>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MudGrid Spacing="2" Justify="Justify.FlexStart">
|
||||||
|
@foreach (var p in _topDiscountProducts)
|
||||||
|
{
|
||||||
|
<MudItem xs="6" sm="6" md="4">
|
||||||
|
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden landing-product-card"
|
||||||
|
Style="cursor:pointer;"
|
||||||
|
@onclick="() => NavigateToDiscountProduct(p.Id)">
|
||||||
|
<MudCardContent Class="d-flex flex-column pa-1 h-100">
|
||||||
|
<div style="aspect-ratio:1/1;width:100%;background-image:url('@(string.IsNullOrWhiteSpace(p.ThumbnailUrl) ? p.ImageUrl : p.ThumbnailUrl)');background-size:cover;background-position:center;border-radius:0.5rem;position:relative;">
|
||||||
|
@if (p.RemainingCount <= 0)
|
||||||
|
{
|
||||||
|
<div style="position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;border-radius:0.5rem;">
|
||||||
|
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">ناموجود</MudChip>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (p.RemainingCount <= 5)
|
||||||
|
{
|
||||||
|
<MudChip T="string" Color="Color.Warning" Variant="Variant.Filled" Size="Size.Small"
|
||||||
|
Style="position:absolute;top:8px;right:8px;">
|
||||||
|
فقط @p.RemainingCount عدد
|
||||||
|
</MudChip>
|
||||||
|
}
|
||||||
|
@if (p.MaxDiscountPercent > 0)
|
||||||
|
{
|
||||||
|
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled" Size="Size.Small"
|
||||||
|
Style="position:absolute;top:8px;left:8px;">
|
||||||
|
@p.MaxDiscountPercent% اعتبار
|
||||||
|
</MudChip>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="pa-1 flex-grow-1 d-flex flex-column justify-space-between">
|
||||||
|
<MudText Typo="Typo.subtitle1" Style="display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">@p.Title</MudText>
|
||||||
|
@if (_isAuthenticated)
|
||||||
|
{
|
||||||
|
<div>
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatDiscountPrice(p.Price)</MudText>
|
||||||
|
<MudText Typo="Typo.overline" Class="mud-text-secondary" Style="font-size:0.6rem;line-height:1;">(+ ارزش افزوده)</MudText>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Small" Class="me-1"/>برای مشاهده قیمت وارد شوید
|
||||||
|
</MudText>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</MudCardContent>
|
||||||
|
<MudCardActions Class="mt-auto pa-2" @onclick:stopPropagation="true">
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Secondary" FullWidth="true"
|
||||||
|
StartIcon="@Icons.Material.Filled.AddShoppingCart"
|
||||||
|
Disabled="@(p.RemainingCount <= 0)"
|
||||||
|
OnClick="@(() => AddDiscountToCart(p))">
|
||||||
|
@(p.RemainingCount <= 0 ? "ناموجود" : "افزودن به سبد")
|
||||||
|
</MudButton>
|
||||||
|
</MudCardActions>
|
||||||
|
</MudCard>
|
||||||
|
</MudItem>
|
||||||
|
}
|
||||||
|
</MudGrid>
|
||||||
|
|
||||||
|
<div class="text-center mt-6">
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" Class="rounded-pill"
|
||||||
|
Href="@RouteConstants.DiscountStore.Products"
|
||||||
|
EndIcon="@Icons.Material.Filled.ArrowBack">
|
||||||
|
مشاهده همه محصولات اعتباری
|
||||||
|
</MudButton>
|
||||||
|
</div>
|
||||||
|
</MudContainer>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@* ═══════════════════════════════════════════════
|
@* ═══════════════════════════════════════════════
|
||||||
5. LATEST BLOG POSTS
|
5. LATEST BLOG POSTS
|
||||||
═══════════════════════════════════════════════ *@
|
═══════════════════════════════════════════════ *@
|
||||||
@@ -256,6 +429,8 @@
|
|||||||
@* ═══════════════════════════════════════════════
|
@* ═══════════════════════════════════════════════
|
||||||
6. TESTIMONIALS
|
6. TESTIMONIALS
|
||||||
═══════════════════════════════════════════════ *@
|
═══════════════════════════════════════════════ *@
|
||||||
|
@if (_testimonials.Count > 0)
|
||||||
|
{
|
||||||
<section class="section-landing">
|
<section class="section-landing">
|
||||||
<MudContainer MaxWidth="MaxWidth.Large">
|
<MudContainer MaxWidth="MaxWidth.Large">
|
||||||
<div class="text-center mb-8 fade-in-up">
|
<div class="text-center mb-8 fade-in-up">
|
||||||
@@ -292,6 +467,7 @@
|
|||||||
</MudGrid>
|
</MudGrid>
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
</section>
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
@* ═══════════════════════════════════════════════
|
@* ═══════════════════════════════════════════════
|
||||||
7. FAQ
|
7. FAQ
|
||||||
|
|||||||
@@ -10,11 +10,26 @@ public partial class Index : IDisposable
|
|||||||
[Inject] private BlogPostService BlogPostService { get; set; } = default!;
|
[Inject] private BlogPostService BlogPostService { get; set; } = default!;
|
||||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||||
[Inject] private SitePageSettingsService PageSettingsService { 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 ──
|
// ── CMS page data ──
|
||||||
private PageSettingsDto? _pageData;
|
private PageSettingsDto? _pageData;
|
||||||
private LandingSettings? _settings;
|
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) ──
|
// ── Latest blog posts (loaded from CMS) ──
|
||||||
private List<BlogPostCardDto> _latestPosts = new();
|
private List<BlogPostCardDto> _latestPosts = new();
|
||||||
|
|
||||||
@@ -33,6 +48,7 @@ public partial class Index : IDisposable
|
|||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
MainService.OnChangeHandler += OnStateChanged;
|
MainService.OnChangeHandler += OnStateChanged;
|
||||||
|
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
|
||||||
|
|
||||||
// Load landing page settings from CMS
|
// Load landing page settings from CMS
|
||||||
try
|
try
|
||||||
@@ -51,10 +67,29 @@ public partial class Index : IDisposable
|
|||||||
PopulateFromSettings();
|
PopulateFromSettings();
|
||||||
_dataLoaded = true;
|
_dataLoaded = true;
|
||||||
|
|
||||||
|
// Load top-selling in-stock products and blog posts in parallel
|
||||||
|
var topRegTask = ProductService.GetTopSellingAsync(LandingTopProductCount, inStock: true);
|
||||||
|
var topDiscTask = DiscountProductService.GetTopSellingAsync(LandingTopProductCount, inStock: true);
|
||||||
|
var featuredPostsTask = BlogPostService.GetFeaturedPostsAsync(2);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.WhenAll(topRegTask, topDiscTask, featuredPostsTask);
|
||||||
|
_topRegularProducts = topRegTask.Result.Products;
|
||||||
|
_topDiscountProducts = topDiscTask.Result.Products;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Fallback: sections remain empty
|
||||||
|
}
|
||||||
|
_loadingTopProducts = false;
|
||||||
|
|
||||||
// Load latest published blog posts
|
// Load latest published blog posts
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_latestPosts = await BlogPostService.GetFeaturedPostsAsync(2);
|
_latestPosts = featuredPostsTask.IsCompletedSuccessfully
|
||||||
|
? featuredPostsTask.Result
|
||||||
|
: await BlogPostService.GetFeaturedPostsAsync(2);
|
||||||
|
|
||||||
if (_latestPosts.Count < 2)
|
if (_latestPosts.Count < 2)
|
||||||
{
|
{
|
||||||
@@ -158,9 +193,10 @@ public partial class Index : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Testimonials ──
|
// ── Testimonials ──
|
||||||
if (_settings?.Testimonials?.Any() == true)
|
// اگر تنظیمات از CMS لود شده، لیست خالی یعنی ادمین عمداً حذف کرده — بدون fallback hardcode
|
||||||
|
if (_settings is not null)
|
||||||
{
|
{
|
||||||
_testimonials = _settings.Testimonials
|
_testimonials = (_settings.Testimonials ?? [])
|
||||||
.Select((t, i) => new TestimonialItem(t.Quote ?? "", t.Name ?? "", t.Role ?? "", i * 150))
|
.Select((t, i) => new TestimonialItem(t.Quote ?? "", t.Name ?? "", t.Role ?? "", i * 150))
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
@@ -169,7 +205,7 @@ public partial class Index : IDisposable
|
|||||||
_testimonials = new()
|
_testimonials = new()
|
||||||
{
|
{
|
||||||
new("با کارا بازار سلامت، محاسبه کارمزدها و پایش تیمها بدون اکسل و دردسر انجام میشود.", "شرکت سینا نت", "مدیر عملیات", 0),
|
new("با کارا بازار سلامت، محاسبه کارمزدها و پایش تیمها بدون اکسل و دردسر انجام میشود.", "شرکت سینا نت", "مدیر عملیات", 0),
|
||||||
new("شجرهنامه بصری و گزارشهای دقیق باعث شد رشد تیم را لحظهای ببینیم.", "هولدینگ آریانا", "مدیر فروش", 150),
|
new("سازمان فروش بصری و گزارشهای دقیق باعث شد رشد تیم را لحظهای ببینیم.", "هولدینگ آریانا", "مدیر فروش", 150),
|
||||||
new("سادگی ثبتنام و شفافیت پاداشها مهمترین مزیت این پلتفرم است.", "گروه بهداشتی نوین", "مدیر توسعه", 300),
|
new("سادگی ثبتنام و شفافیت پاداشها مهمترین مزیت این پلتفرم است.", "گروه بهداشتی نوین", "مدیر توسعه", 300),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -267,6 +303,21 @@ public partial class Index : IDisposable
|
|||||||
Navigation.NavigateTo($"/blog/{slug}");
|
Navigation.NavigateTo($"/blog/{slug}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void NavigateToRegularProduct(long id)
|
||||||
|
=> Navigation.NavigateTo(RouteConstants.Store.ProductDetail + id);
|
||||||
|
|
||||||
|
private void NavigateToDiscountProduct(long id)
|
||||||
|
=> Navigation.NavigateTo(RouteConstants.DiscountStore.ProductDetail + id);
|
||||||
|
|
||||||
|
private async Task AddRegularToCart(Product p)
|
||||||
|
=> await GuestGate.RunAsync(() => Cart.Add(p, 1));
|
||||||
|
|
||||||
|
private async Task AddDiscountToCart(DiscountProductCard p)
|
||||||
|
=> await GuestGate.RunAsync(() => DiscountCart.AddAsync(p.Id));
|
||||||
|
|
||||||
|
private string FormatPrice(long price) => $"{VAT.AddVAT(price):N0} تومان";
|
||||||
|
private string FormatDiscountPrice(long price) => $"{price:N0} تومان";
|
||||||
|
|
||||||
private async void OnStateChanged()
|
private async void OnStateChanged()
|
||||||
{
|
{
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
using FrontOffice.Main.Utilities;
|
||||||
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
||||||
using FrontOffice.Main.Pages.Profile.Components;
|
using FrontOffice.Main.Pages.Profile.Components;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
@@ -9,6 +9,7 @@ namespace FrontOffice.Main.Pages.Profile;
|
|||||||
public partial class Addresses : ComponentBase
|
public partial class Addresses : ComponentBase
|
||||||
{
|
{
|
||||||
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
||||||
|
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||||
|
|
||||||
private List<CustomerAddressModel> _addresses = new();
|
private List<CustomerAddressModel> _addresses = new();
|
||||||
private bool _isLoadingAddresses;
|
private bool _isLoadingAddresses;
|
||||||
@@ -42,7 +43,10 @@ public partial class Addresses : ComponentBase
|
|||||||
var dialog = await DialogService.ShowAsync<AddAddressDialog>("افزودن آدرس جدید");
|
var dialog = await DialogService.ShowAsync<AddAddressDialog>("افزودن آدرس جدید");
|
||||||
var result = await dialog.Result;
|
var result = await dialog.Result;
|
||||||
if (result is not null && !result.Canceled)
|
if (result is not null && !result.Canceled)
|
||||||
|
{
|
||||||
await LoadAddresses();
|
await LoadAddresses();
|
||||||
|
await AuthService.RefreshTokenAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OpenEditAddressDialog(CustomerAddressModel address)
|
private async Task OpenEditAddressDialog(CustomerAddressModel address)
|
||||||
@@ -80,6 +84,7 @@ public partial class Addresses : ComponentBase
|
|||||||
await UserAddressContract.DeleteCustomerAddressAsync(new() { Id = id });
|
await UserAddressContract.DeleteCustomerAddressAsync(new() { Id = id });
|
||||||
Snackbar.Add("آدرس حذف شد.", Severity.Success);
|
Snackbar.Add("آدرس حذف شد.", Severity.Success);
|
||||||
await LoadAddresses();
|
await LoadAddresses();
|
||||||
|
await AuthService.RefreshTokenAsync();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
@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>
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
@@ -61,11 +61,11 @@
|
|||||||
<span class="stat-value">@_statistics.TotalMembers</span>
|
<span class="stat-value">@_statistics.TotalMembers</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<span class="stat-label">پای چپ:</span>
|
<span class="stat-label">سازمان چپ:</span>
|
||||||
<span class="stat-value left">@_statistics.LeftLegCount</span>
|
<span class="stat-value left">@_statistics.LeftLegCount</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<span class="stat-label">پای راست:</span>
|
<span class="stat-label">سازمان راست:</span>
|
||||||
<span class="stat-value right">@_statistics.RightLegCount</span>
|
<span class="stat-value right">@_statistics.RightLegCount</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using FrontOffice.Main.Utilities;
|
using FrontOffice.Main.Utilities;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
@@ -95,7 +95,23 @@ public partial class OrganizationChart : IAsyncDisposable
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_dotNetHelper = DotNetObjectReference.Create(this);
|
_dotNetHelper = DotNetObjectReference.Create(this);
|
||||||
var flatData = _networkTree.ToFlatArray();
|
// 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
|
||||||
|
}).ToArray();
|
||||||
|
|
||||||
await JSRuntime.InvokeVoidAsync("OrgChart.init", "org-chart-container", flatData, _dotNetHelper);
|
await JSRuntime.InvokeVoidAsync("OrgChart.init", "org-chart-container", flatData, _dotNetHelper);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
<div class="wallet-strip-item">
|
<div class="wallet-strip-item">
|
||||||
<div class="wallet-strip-label">
|
<div class="wallet-strip-label">
|
||||||
<MudIcon Icon="@Icons.Material.Outlined.Groups" Size="Size.Small" Color="Color.Warning" />
|
<MudIcon Icon="@Icons.Material.Outlined.Groups" Size="Size.Small" Color="Color.Warning" />
|
||||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">پاداش تیمی</MudText>
|
<MudText Typo="Typo.caption" Class="mud-text-secondary">پاداش های دریافتی</MudText>
|
||||||
</div>
|
</div>
|
||||||
<MudText Typo="Typo.subtitle2" Color="Color.Warning">@_walletNetwork</MudText>
|
<MudText Typo="Typo.subtitle2" Color="Color.Warning">@_walletNetwork</MudText>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public partial class Index
|
|||||||
{
|
{
|
||||||
new(RouteConstants.Profile.Personal, Icons.Material.Filled.Person, "اطلاعات شخصی", "نمایش و ویرایش", "background:rgba(99,102,241,.12); color:#6366f1;"),
|
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.Addresses, Icons.Material.Filled.LocationOn, "آدرسها", "مدیریت آدرسها", "background:rgba(99,102,241,.12); color:#6366f1;"),
|
||||||
new(RouteConstants.Profile.Tree, Icons.Material.Filled.AccountTree, "شجرهنامه", "ساختار تیم", "background:rgba(99,102,241,.12); color:#6366f1;"),
|
new(RouteConstants.Profile.Tree, Icons.Material.Filled.AccountTree, "سازمان فروش", "ساختار تیم", "background:rgba(99,102,241,.12); color:#6366f1;"),
|
||||||
new(RouteConstants.Gateway.StoreChooser, Icons.Material.Filled.Store, "فروشگاهها", "ورود به فروشگاه", "background:rgba(16,185,129,.12); color:#10b981;"),
|
new(RouteConstants.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.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.MagicWallet, Icons.Material.Filled.AutoAwesome, "کیفپول جادویی", "شارژ چندبرابری", "background:rgba(168,85,247,.12); color:#a855f7;"),
|
||||||
@@ -333,6 +333,7 @@ public partial class Index
|
|||||||
if (!result.Canceled)
|
if (!result.Canceled)
|
||||||
{
|
{
|
||||||
await LoadAddresses();
|
await LoadAddresses();
|
||||||
|
await AuthService.RefreshTokenAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,6 +386,7 @@ public partial class Index
|
|||||||
});
|
});
|
||||||
Snackbar.Add("آدرس با موفقیت حذف شد.", Severity.Success);
|
Snackbar.Add("آدرس با موفقیت حذف شد.", Severity.Success);
|
||||||
await LoadAddresses();
|
await LoadAddresses();
|
||||||
|
await AuthService.RefreshTokenAsync();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -491,11 +493,6 @@ public partial class Index
|
|||||||
Snackbar.Add("لطفا اطلاعات شخصی خود را تکمیل کنید. (تاریخ تولد وارد نشده)", Severity.Error);
|
Snackbar.Add("لطفا اطلاعات شخصی خود را تکمیل کنید. (تاریخ تولد وارد نشده)", Severity.Error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!_addresses.Any())
|
|
||||||
{
|
|
||||||
Snackbar.Add("آدرس محل سکونت شما الزامی است!", Severity.Error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var url = "https://dayadiamond.ir/profile/creditpurchase/?merchantcode=56146364";
|
var url = "https://dayadiamond.ir/profile/creditpurchase/?merchantcode=56146364";
|
||||||
await JSRuntime.InvokeVoidAsync("open", url, "_blank");
|
await JSRuntime.InvokeVoidAsync("open", url, "_blank");
|
||||||
|
|||||||
@@ -166,8 +166,23 @@
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<MudAlert Severity="MudBlazor.Severity.Warning" Class="rounded-lg">
|
<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>
|
</MudAlert>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +200,7 @@
|
|||||||
حداکثر اعتبار: @FormatPrice(_status.MagicMaxCredit)
|
حداکثر اعتبار: @FormatPrice(_status.MagicMaxCredit)
|
||||||
</MudListItem>
|
</MudListItem>
|
||||||
<MudListItem T="string" Icon="@Icons.Material.Filled.Warning" IconColor="Color.Warning">
|
<MudListItem T="string" Icon="@Icons.Material.Filled.Warning" IconColor="Color.Warning">
|
||||||
در حالت جادویی، کمیسیون و پاداش تیمی غیرفعال است
|
در حالت جادویی، کمیسیون و پاداش های دریافتی غیرفعال است
|
||||||
</MudListItem>
|
</MudListItem>
|
||||||
<MudListItem T="string" Icon="@Icons.Material.Filled.Info" IconColor="Color.Info">
|
<MudListItem T="string" Icon="@Icons.Material.Filled.Info" IconColor="Color.Info">
|
||||||
بعد از اتمام سقف و خرج موجودی، با خرید مجدد پکیج دور جدید شروع میشود
|
بعد از اتمام سقف و خرج موجودی، با خرید مجدد پکیج دور جدید شروع میشود
|
||||||
|
|||||||
@@ -91,6 +91,8 @@
|
|||||||
private string _message = string.Empty;
|
private string _message = string.Empty;
|
||||||
private long _transactionId;
|
private long _transactionId;
|
||||||
private long _walletBalance;
|
private long _walletBalance;
|
||||||
|
private bool _verifyCompleted;
|
||||||
|
private readonly SemaphoreSlim _verifyGate = new(1, 1);
|
||||||
|
|
||||||
// دکمههای بازگشت بسته به نوع پرداخت
|
// دکمههای بازگشت بسته به نوع پرداخت
|
||||||
private string _successReturnUrl = "/profile";
|
private string _successReturnUrl = "/profile";
|
||||||
@@ -100,9 +102,29 @@
|
|||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (firstRender && !_verifyCompleted)
|
||||||
{
|
{
|
||||||
|
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();
|
await VerifyPayment();
|
||||||
|
_verifyCompleted = true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_verifyGate.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +143,9 @@
|
|||||||
case "discount-wallet":
|
case "discount-wallet":
|
||||||
await VerifyDiscountWalletCharge();
|
await VerifyDiscountWalletCharge();
|
||||||
break;
|
break;
|
||||||
|
case "credit-wallet":
|
||||||
|
await VerifyCreditWalletCharge();
|
||||||
|
break;
|
||||||
case "discount-order":
|
case "discount-order":
|
||||||
await VerifyDiscountOrderPayment();
|
await VerifyDiscountOrderPayment();
|
||||||
break;
|
break;
|
||||||
@@ -233,6 +258,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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>
|
||||||
/// تأیید پرداخت سفارش فروشگاه تخفیفی
|
/// تأیید پرداخت سفارش فروشگاه تخفیفی
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -286,6 +340,7 @@
|
|||||||
{
|
{
|
||||||
"magic-wallet" => "/profile/magic-wallet",
|
"magic-wallet" => "/profile/magic-wallet",
|
||||||
"discount-wallet" => "/profile/charge-discount-wallet",
|
"discount-wallet" => "/profile/charge-discount-wallet",
|
||||||
|
"credit-wallet" => "/profile/charge-credit-wallet",
|
||||||
"discount-order" => "/discount-store",
|
"discount-order" => "/discount-store",
|
||||||
_ => "/profile"
|
_ => "/profile"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
@attribute [Route(RouteConstants.Profile.Tree)]
|
@attribute [Route(RouteConstants.Profile.Tree)]
|
||||||
@using FrontOffice.Main.Pages.Profile.Components
|
@using FrontOffice.Main.Pages.Profile.Components
|
||||||
|
|
||||||
<PageTitle>شجرهنامه</PageTitle>
|
<PageTitle>سازمان فروش</PageTitle>
|
||||||
|
|
||||||
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
|
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
|
||||||
<MudStack Spacing="3">
|
<MudStack Spacing="3">
|
||||||
<PageHeader Title="شجرهنامه" BackHref="@RouteConstants.Profile.Index" />
|
<PageHeader Title="سازمان فروش" BackHref="@RouteConstants.Profile.Index" />
|
||||||
|
|
||||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
||||||
<OrganizationChart />
|
<OrganizationChart />
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12" sm="6" md="4">
|
<MudItem xs="12" sm="6" md="4">
|
||||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
||||||
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">پاداش تیمی</MudText>
|
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">پاداش های دریافتی</MudText>
|
||||||
<MudText Typo="Typo.h4" Color="Color.Success">@_balances.Network</MudText>
|
<MudText Typo="Typo.h4" Color="Color.Success">@_balances.Network</MudText>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
@@ -38,6 +38,93 @@
|
|||||||
درخواستهای برداشت
|
درخواستهای برداشت
|
||||||
</MudButton>
|
</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"
|
<MudButton Variant="Variant.Filled"
|
||||||
Color="Color.Info"
|
Color="Color.Info"
|
||||||
@@ -83,7 +170,7 @@
|
|||||||
<HeaderContent>
|
<HeaderContent>
|
||||||
<MudTh>تاریخ</MudTh>
|
<MudTh>تاریخ</MudTh>
|
||||||
<MudTh>اصلی (تغییرات / مانده)</MudTh>
|
<MudTh>اصلی (تغییرات / مانده)</MudTh>
|
||||||
<MudTh>پاداش تیمی (تغییرات / مانده)</MudTh>
|
<MudTh>پاداش های دریافتی (تغییرات / مانده)</MudTh>
|
||||||
<MudTh>اعتباری (تغییرات / مانده)</MudTh>
|
<MudTh>اعتباری (تغییرات / مانده)</MudTh>
|
||||||
<MudTh>شناسه ارجاع</MudTh>
|
<MudTh>شناسه ارجاع</MudTh>
|
||||||
<MudTh>توضیحات</MudTh>
|
<MudTh>توضیحات</MudTh>
|
||||||
@@ -100,7 +187,7 @@
|
|||||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">@FormatPrice(context.CreditBalance)</MudText>
|
<MudText Typo="Typo.caption" Class="mud-text-secondary">@FormatPrice(context.CreditBalance)</MudText>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudTd>
|
</MudTd>
|
||||||
<MudTd DataLabel="پاداش تیمی">
|
<MudTd DataLabel="پاداش های دریافتی">
|
||||||
<MudStack Spacing="0">
|
<MudStack Spacing="0">
|
||||||
<MudText Color="@(context.NetworkChange > 0 ? Color.Success : context.NetworkChange < 0 ? Color.Error : Color.Default)" Typo="Typo.body2">
|
<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) : "-")
|
@(context.NetworkChange != 0 ? (context.NetworkChange > 0 ? "+" : "") + FormatPrice(context.NetworkChange) : "-")
|
||||||
@@ -147,10 +234,10 @@
|
|||||||
</MudStack>
|
</MudStack>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
<!-- پاداش تیمی -->
|
<!-- پاداش های دریافتی -->
|
||||||
<MudPaper Class="pa-2 flex-grow-1" Outlined="true">
|
<MudPaper Class="pa-2 flex-grow-1" Outlined="true">
|
||||||
<MudStack Spacing="1" AlignItems="AlignItems.Center">
|
<MudStack Spacing="1" AlignItems="AlignItems.Center">
|
||||||
<MudText Typo="Typo.caption" Color="Color.Success">پاداش تیمی</MudText>
|
<MudText Typo="Typo.caption" Color="Color.Success">پاداش های دریافتی</MudText>
|
||||||
<MudText Color="@(tx.NetworkChange > 0 ? Color.Success : tx.NetworkChange < 0 ? Color.Error : Color.Default)" Style="font-weight: 600; font-size: 0.75rem;">
|
<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) : "-")
|
@(tx.NetworkChange != 0 ? (tx.NetworkChange > 0 ? "+" : "") + FormatPrice(tx.NetworkChange) : "-")
|
||||||
</MudText>
|
</MudText>
|
||||||
|
|||||||
@@ -6,13 +6,43 @@ namespace FrontOffice.Main.Pages.Profile;
|
|||||||
|
|
||||||
public partial class Wallet : ComponentBase
|
public partial class Wallet : ComponentBase
|
||||||
{
|
{
|
||||||
|
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||||
|
|
||||||
private (string Credit, string Discount, string Network) _balances = ("-", "-", "-");
|
private (string Credit, string Discount, string Network) _balances = ("-", "-", "-");
|
||||||
private List<WalletTransaction> _txs = new();
|
private List<WalletTransaction> _txs = new();
|
||||||
private string? _filterReferenceId;
|
private string? _filterReferenceId;
|
||||||
private string _filterType = "all";
|
private string _filterType = "all";
|
||||||
|
private bool _isClubMemberActive;
|
||||||
|
private bool _hasPurchasedPackage;
|
||||||
|
private bool _isMagicWallet;
|
||||||
|
private bool _magicCeilingFull;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
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();
|
var b = await WalletService.GetBalancesAsync();
|
||||||
_balances = (FormatPrice(b.CreditBalance), FormatPrice(b.DiscountBalance), FormatPrice(b.NetworkBalance));
|
_balances = (FormatPrice(b.CreditBalance), FormatPrice(b.DiscountBalance), FormatPrice(b.NetworkBalance));
|
||||||
_txs = await WalletService.GetTransactionsAsync();
|
_txs = await WalletService.GetTransactionsAsync();
|
||||||
|
|||||||
@@ -58,8 +58,19 @@ public partial class WithdrawalRequests : ComponentBase
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_isSubmittingWithdrawal = true;
|
_isSubmittingWithdrawal = true;
|
||||||
_withdrawIban=_withdrawIban!.Trim().ToUpper().Replace("IR", "").Replace(" ", "");
|
|
||||||
await WalletService.RequestWithdrawalAsync(_selectedPayout.Id, _withdrawMethod, "IR"+_withdrawIban);
|
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);
|
||||||
Snackbar.Add("درخواست برداشت ثبت شد.", Severity.Success);
|
Snackbar.Add("درخواست برداشت ثبت شد.", Severity.Success);
|
||||||
|
|
||||||
// بروزرسانی لیست
|
// بروزرسانی لیست
|
||||||
@@ -125,4 +136,26 @@ public partial class WithdrawalRequests : ComponentBase
|
|||||||
1 => "الماس",
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,17 @@ public partial class Cart : ComponentBase, IDisposable
|
|||||||
{
|
{
|
||||||
[Inject] private CartService CartService { get; set; } = default!;
|
[Inject] private CartService CartService { get; set; } = default!;
|
||||||
[Inject] private VATService VAT { 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
|
// Navigation and Snackbar are available via _Imports.razor
|
||||||
private CartService CartData => CartService;
|
private CartService CartData => CartService;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
if (!await AuthService.IsAuthenticatedAsync())
|
||||||
|
{
|
||||||
|
await AuthDialogService.ShowAuthDialogAsync();
|
||||||
|
}
|
||||||
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
|
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
|
||||||
await CartService.EnsureInitializedAsync();
|
await CartService.EnsureInitializedAsync();
|
||||||
CartService.OnChange += StateHasChanged;
|
CartService.OnChange += StateHasChanged;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
||||||
using CMSMicroservice.Protobuf.Protos.UserOrder;
|
using CMSMicroservice.Protobuf.Protos.UserOrder;
|
||||||
|
using FrontOffice.Main.Shared;
|
||||||
using FrontOffice.Main.Utilities;
|
using FrontOffice.Main.Utilities;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
@@ -14,6 +15,8 @@ public partial class CheckoutSummary : ComponentBase
|
|||||||
[Inject] private VATService VAT { get; set; } = default!;
|
[Inject] private VATService VAT { get; set; } = default!;
|
||||||
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
||||||
[Inject] private UserOrderContract.UserOrderContractClient UserOrderContract { 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
|
// Snackbar and Navigation are injected via _Imports.razor
|
||||||
|
|
||||||
private List<CustomerAddressModel> _addresses = new();
|
private List<CustomerAddressModel> _addresses = new();
|
||||||
@@ -27,9 +30,16 @@ public partial class CheckoutSummary : ComponentBase
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
|
if (!await AuthService.IsAuthenticatedAsync())
|
||||||
|
{
|
||||||
|
await AuthDialogService.ShowAuthDialogAsync();
|
||||||
|
}
|
||||||
await Cart.EnsureInitializedAsync();
|
await Cart.EnsureInitializedAsync();
|
||||||
await LoadAddresses();
|
var userInfo = await AuthService.GetUserAuthInfo();
|
||||||
|
if (userInfo.HasAddress)
|
||||||
|
await LoadAddresses();
|
||||||
|
else
|
||||||
|
_addresses = new();
|
||||||
await LoadWalletBalance();
|
await LoadWalletBalance();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +47,6 @@ public partial class CheckoutSummary : ComponentBase
|
|||||||
{
|
{
|
||||||
if (firstRender)
|
if (firstRender)
|
||||||
{
|
{
|
||||||
// بارگذاری نرخ VAT
|
|
||||||
await VAT.LoadAsync();
|
await VAT.LoadAsync();
|
||||||
}
|
}
|
||||||
await base.OnAfterRenderAsync(firstRender);
|
await base.OnAfterRenderAsync(firstRender);
|
||||||
@@ -46,9 +55,7 @@ public partial class CheckoutSummary : ComponentBase
|
|||||||
private async Task LoadWalletBalance()
|
private async Task LoadWalletBalance()
|
||||||
{
|
{
|
||||||
var walletResult = await WalletService.GetBalancesAsync();
|
var walletResult = await WalletService.GetBalancesAsync();
|
||||||
walletBalance = walletResult.CreditBalance
|
walletBalance = walletResult.CreditBalance;
|
||||||
// + walletResult.NetworkBalance
|
|
||||||
;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadAddresses()
|
private async Task LoadAddresses()
|
||||||
@@ -87,11 +94,15 @@ public partial class CheckoutSummary : ComponentBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var totalRequired = VAT.AddVAT(Cart.Total);
|
||||||
|
if (await TryHandleInsufficientBalanceAsync(totalRequired))
|
||||||
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var request = new SubmitShopBuyOrderRequest
|
var request = new SubmitShopBuyOrderRequest
|
||||||
{
|
{
|
||||||
TotalAmount = VAT.AddVAT(Cart.Total)
|
TotalAmount = totalRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
var response = await UserOrderContract.SubmitShopBuyOrderAsync(request);
|
var response = await UserOrderContract.SubmitShopBuyOrderAsync(request);
|
||||||
@@ -101,15 +112,85 @@ public partial class CheckoutSummary : ComponentBase
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
if (CreditChargeNavigation.IsInsufficientWalletBalance(ex))
|
||||||
|
{
|
||||||
|
await LoadWalletBalance();
|
||||||
|
await TryHandleInsufficientBalanceAsync(totalRequired);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Snackbar.Add($"خطا در ثبت سفارش: {ex.Message}", Severity.Error);
|
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);
|
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// محاسبه مالیات بر ارزش افزوده
|
|
||||||
/// </summary>
|
|
||||||
private long CalculateVAT() => VAT.CalculateVAT(Cart.Total);
|
private long CalculateVAT() => VAT.CalculateVAT(Cart.Total);
|
||||||
|
|
||||||
private static string GetProductImageUrl(string? imageUrl)
|
private static string GetProductImageUrl(string? imageUrl)
|
||||||
|
|||||||
@@ -47,11 +47,18 @@ public partial class OrderTracking : ComponentBase
|
|||||||
{
|
{
|
||||||
if (_order is null) return;
|
if (_order is null) return;
|
||||||
|
|
||||||
// Build tracking steps based on PaymentStatus
|
|
||||||
var isPaid = _order.PaymentStatus == Messages.PaymentStatus.Success;
|
var isPaid = _order.PaymentStatus == Messages.PaymentStatus.Success;
|
||||||
|
// Domain/public_messages: None=0, Pending=1, InTransit=2, Delivered=3, Returned=4, Cancelled=5, ReadyForOfficePickup=6
|
||||||
|
var delivery = (int)_order.DeliveryStatus;
|
||||||
|
var isCancelled = delivery == 5;
|
||||||
|
var isReturned = delivery == 4;
|
||||||
|
var isOfficePickup = delivery == 6;
|
||||||
|
var isInTransit = delivery == 2;
|
||||||
|
var isDelivered = delivery == 3;
|
||||||
|
var isPendingDelivery = delivery is 0 or 1;
|
||||||
|
|
||||||
_trackingSteps = new List<TrackingStep>
|
_trackingSteps =
|
||||||
{
|
[
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Title = "ثبت سفارش",
|
Title = "ثبت سفارش",
|
||||||
@@ -61,45 +68,77 @@ public partial class OrderTracking : ComponentBase
|
|||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Title = "در انتظار پرداخت",
|
Title = "پرداخت",
|
||||||
Description = "منتظر تایید پرداخت هستیم",
|
Description = isPaid ? "پرداخت شما تایید شد" : "منتظر تایید پرداخت هستیم",
|
||||||
IsCompleted = isPaid,
|
IsCompleted = isPaid,
|
||||||
IsCurrent = !isPaid,
|
IsCurrent = !isPaid && !isCancelled,
|
||||||
Date = isPaid ? FormatDate(_order.PaymentDate) : ""
|
Date = isPaid ? FormatDate(_order.PaymentDate) : ""
|
||||||
},
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Title = "تایید پرداخت",
|
|
||||||
Description = "پرداخت شما تایید شد",
|
|
||||||
IsCompleted = isPaid,
|
|
||||||
IsCurrent = false,
|
|
||||||
Date = isPaid ? FormatDate(_order.PaymentDate) : ""
|
|
||||||
},
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Title = "در حال پردازش",
|
|
||||||
Description = "سفارش شما در حال آمادهسازی است",
|
|
||||||
IsCompleted = false,
|
|
||||||
IsCurrent = isPaid,
|
|
||||||
Date = ""
|
|
||||||
},
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Title = "ارسال شده",
|
|
||||||
Description = "سفارش شما به پست تحویل داده شد",
|
|
||||||
IsCompleted = false,
|
|
||||||
IsCurrent = false,
|
|
||||||
Date = ""
|
|
||||||
},
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Title = "تحویل داده شده",
|
|
||||||
Description = "سفارش به دست شما رسید",
|
|
||||||
IsCompleted = false,
|
|
||||||
IsCurrent = false,
|
|
||||||
Date = ""
|
|
||||||
}
|
}
|
||||||
};
|
];
|
||||||
|
|
||||||
|
if (isCancelled)
|
||||||
|
{
|
||||||
|
_trackingSteps.Add(new()
|
||||||
|
{
|
||||||
|
Title = "لغو شده",
|
||||||
|
Description = "ارسال این سفارش لغو شده است",
|
||||||
|
IsCompleted = true,
|
||||||
|
IsCurrent = true,
|
||||||
|
Date = ""
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isReturned)
|
||||||
|
{
|
||||||
|
_trackingSteps.Add(new()
|
||||||
|
{
|
||||||
|
Title = "مرجوع شده",
|
||||||
|
Description = "سفارش مرجوع شده است",
|
||||||
|
IsCompleted = true,
|
||||||
|
IsCurrent = true,
|
||||||
|
Date = ""
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOfficePickup)
|
||||||
|
{
|
||||||
|
_trackingSteps.Add(new()
|
||||||
|
{
|
||||||
|
Title = "آماده تحویل در دفتر",
|
||||||
|
Description = "سفارش برای تحویل حضوری در دفتر آماده است",
|
||||||
|
IsCompleted = true,
|
||||||
|
IsCurrent = true,
|
||||||
|
Date = ""
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_trackingSteps.Add(new()
|
||||||
|
{
|
||||||
|
Title = "در حال آمادهسازی",
|
||||||
|
Description = "سفارش شما در حال آمادهسازی است",
|
||||||
|
IsCompleted = isInTransit || isDelivered,
|
||||||
|
IsCurrent = isPaid && isPendingDelivery,
|
||||||
|
Date = ""
|
||||||
|
});
|
||||||
|
_trackingSteps.Add(new()
|
||||||
|
{
|
||||||
|
Title = "ارسال شده",
|
||||||
|
Description = "سفارش شما به پست تحویل داده شد",
|
||||||
|
IsCompleted = isDelivered,
|
||||||
|
IsCurrent = isInTransit,
|
||||||
|
Date = ""
|
||||||
|
});
|
||||||
|
_trackingSteps.Add(new()
|
||||||
|
{
|
||||||
|
Title = "تحویل داده شده",
|
||||||
|
Description = "سفارش به دست شما رسید",
|
||||||
|
IsCompleted = isDelivered,
|
||||||
|
IsCurrent = false,
|
||||||
|
Date = ""
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string FormatDate(Google.Protobuf.WellKnownTypes.Timestamp? timestamp)
|
private static string FormatDate(Google.Protobuf.WellKnownTypes.Timestamp? timestamp)
|
||||||
|
|||||||
@@ -1,80 +1,102 @@
|
|||||||
@attribute [Route(RouteConstants.Store.Orders)]
|
@attribute [Route(RouteConstants.Store.Orders)]
|
||||||
@* Injection is handled in code-behind *@
|
|
||||||
|
|
||||||
<PageTitle>سفارشات من</PageTitle>
|
<PageTitle>سفارشات من</PageTitle>
|
||||||
|
|
||||||
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
|
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
|
||||||
<PageHeader Title="سفارشات من" BackHref="@RouteConstants.Store.Products" />
|
<MudStack Spacing="3">
|
||||||
@if (_loading)
|
<PageHeader Title="سفارشات من" BackHref="@RouteConstants.Store.Products" />
|
||||||
{
|
|
||||||
<LoadingState />
|
|
||||||
}
|
|
||||||
else if (_orders.Count == 0)
|
|
||||||
{
|
|
||||||
<MudAlert Severity="Severity.Info">هنوز سفارشی ثبت نکردهاید.</MudAlert>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
|
|
||||||
<MudTable Items="_orders" Dense="true">
|
|
||||||
<HeaderContent>
|
|
||||||
<MudTh>شناسه</MudTh>
|
|
||||||
<MudTh>تاریخ</MudTh>
|
|
||||||
<MudTh>وضعیت</MudTh>
|
|
||||||
<MudTh>مبلغ</MudTh>
|
|
||||||
<MudTh></MudTh>
|
|
||||||
</HeaderContent>
|
|
||||||
<RowTemplate>
|
|
||||||
<MudTd>@context.Id</MudTd>
|
|
||||||
<MudTd>
|
|
||||||
@if (context.PaymentDate != null)
|
|
||||||
{
|
|
||||||
@context.PaymentDate.ToDateTime().MiladiToJalaliWithTime()
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<text>در انتظار پرداخت</text>
|
|
||||||
}
|
|
||||||
</MudTd>
|
|
||||||
<MudTd>@GetStatusText(context.PaymentStatus)</MudTd>
|
|
||||||
<MudTd>@FormatPrice(context.FactorDetails.Sum(s=>(s.UnitPrice ?? 0) * (s.Count ?? 0)))</MudTd>
|
|
||||||
<MudTd>
|
|
||||||
<MudButton Variant="Variant.Text" Href="@($"{RouteConstants.Store.OrderDetail}{context.Id}")" StartIcon="@Icons.Material.Filled.Receipt">جزئیات</MudButton>
|
|
||||||
</MudTd>
|
|
||||||
</RowTemplate>
|
|
||||||
</MudTable>
|
|
||||||
</MudHidden>
|
|
||||||
|
|
||||||
<MudHidden Breakpoint="Breakpoint.MdAndUp">
|
@if (_loading)
|
||||||
<MudStack Spacing="2">
|
{
|
||||||
@foreach (var o in _orders)
|
<LoadingState />
|
||||||
{
|
}
|
||||||
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
|
else if (_orders.Count == 0)
|
||||||
<MudStack Spacing="1">
|
{
|
||||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
<MudAlert Severity="Severity.Info">هنوز سفارشی ثبت نشده است.</MudAlert>
|
||||||
<MudText>سفارش #@o.Id</MudText>
|
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Store"
|
||||||
<MudText Class="mud-text-secondary">
|
Href="@RouteConstants.Store.Products">
|
||||||
@if (o.PaymentDate != null)
|
مشاهده فروشگاه
|
||||||
{
|
</MudButton>
|
||||||
@o.PaymentDate.ToDateTime().MiladiToJalaliWithTime()
|
}
|
||||||
}
|
else
|
||||||
else
|
{
|
||||||
{
|
<!-- Desktop Table -->
|
||||||
<text>در انتظار پرداخت</text>
|
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
|
||||||
}
|
<MudPaper Elevation="1" Class="pa-4 rounded-lg">
|
||||||
</MudText>
|
<MudTable Items="_orders">
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>شماره سفارش</MudTh>
|
||||||
|
<MudTh>تعداد اقلام</MudTh>
|
||||||
|
<MudTh>مبلغ کل</MudTh>
|
||||||
|
<MudTh>وضعیت پرداخت</MudTh>
|
||||||
|
<MudTh>وضعیت ارسال</MudTh>
|
||||||
|
<MudTh>تاریخ</MudTh>
|
||||||
|
<MudTh></MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd>@context.Id</MudTd>
|
||||||
|
<MudTd>@GetItemsCount(context)</MudTd>
|
||||||
|
<MudTd>@FormatPrice(GetTotalAmount(context))</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudChip T="string" Size="Size.Small"
|
||||||
|
Color="@GetPaymentStatusColor(context.PaymentStatus)"
|
||||||
|
Variant="Variant.Filled">
|
||||||
|
@GetPaymentStatusText(context.PaymentStatus)
|
||||||
|
</MudChip>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudChip T="string" Size="Size.Small"
|
||||||
|
Color="@GetDeliveryStatusColor((int)context.DeliveryStatus)"
|
||||||
|
Variant="Variant.Outlined">
|
||||||
|
@GetDeliveryStatusText((int)context.DeliveryStatus)
|
||||||
|
</MudChip>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>@FormatDate(context)</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
|
||||||
|
OnClick="() => ViewOrder(context.Id)">
|
||||||
|
جزئیات
|
||||||
|
</MudButton>
|
||||||
|
</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
</MudTable>
|
||||||
|
</MudPaper>
|
||||||
|
</MudHidden>
|
||||||
|
|
||||||
|
<!-- Mobile Cards -->
|
||||||
|
<MudHidden Breakpoint="Breakpoint.MdAndUp">
|
||||||
|
<MudStack Spacing="2">
|
||||||
|
@foreach (var order in _orders)
|
||||||
|
{
|
||||||
|
<MudPaper Class="pa-3 rounded-lg cursor-pointer" Outlined="true"
|
||||||
|
@onclick="() => ViewOrder(order.Id)">
|
||||||
|
<MudStack Spacing="1">
|
||||||
|
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||||
|
<MudText Typo="Typo.subtitle2">سفارش #@order.Id</MudText>
|
||||||
|
<MudChip T="string" Size="Size.Small"
|
||||||
|
Color="@GetPaymentStatusColor(order.PaymentStatus)"
|
||||||
|
Variant="Variant.Filled">
|
||||||
|
@GetPaymentStatusText(order.PaymentStatus)
|
||||||
|
</MudChip>
|
||||||
|
</MudStack>
|
||||||
|
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||||
|
<MudText Typo="Typo.caption" Class="mud-text-secondary">@FormatDate(order)</MudText>
|
||||||
|
<MudChip T="string" Size="Size.Small"
|
||||||
|
Color="@GetDeliveryStatusColor((int)order.DeliveryStatus)"
|
||||||
|
Variant="Variant.Outlined">
|
||||||
|
@GetDeliveryStatusText((int)order.DeliveryStatus)
|
||||||
|
</MudChip>
|
||||||
|
</MudStack>
|
||||||
|
<MudDivider />
|
||||||
|
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||||
|
<MudText Typo="Typo.body2">@GetItemsCount(order) قلم</MudText>
|
||||||
|
<MudText Typo="Typo.body2" Class="fw-semibold">@FormatPrice(GetTotalAmount(order)) تومان</MudText>
|
||||||
|
</MudStack>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
</MudPaper>
|
||||||
<MudText>وضعیت: @GetStatusText(o.PaymentStatus)</MudText>
|
}
|
||||||
<MudText Color="Color.Primary">@FormatPrice(o.FactorDetails.Sum(s=>(s.UnitPrice ?? 0) * (s.Count ?? 0)))</MudText>
|
</MudStack>
|
||||||
</MudStack>
|
</MudHidden>
|
||||||
<MudStack Row="true" Justify="Justify.FlexEnd">
|
}
|
||||||
<MudButton Size="Size.Small" Variant="Variant.Outlined" Href="@($"{RouteConstants.Store.OrderDetail}{o.Id}")" StartIcon="@Icons.Material.Filled.Receipt">جزئیات</MudButton>
|
</MudStack>
|
||||||
</MudStack>
|
|
||||||
</MudStack>
|
|
||||||
</MudPaper>
|
|
||||||
}
|
|
||||||
</MudStack>
|
|
||||||
</MudHidden>
|
|
||||||
}
|
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System;
|
|
||||||
using CMSMicroservice.Protobuf.Protos.UserOrder;
|
using CMSMicroservice.Protobuf.Protos.UserOrder;
|
||||||
|
using DateTimeConverterCL;
|
||||||
using FrontOffice.Main.Utilities;
|
using FrontOffice.Main.Utilities;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Messages = CMSMicroservice.Protobuf.Protos;
|
using Messages = CMSMicroservice.Protobuf.Protos;
|
||||||
@@ -10,7 +10,6 @@ namespace FrontOffice.Main.Pages.Store;
|
|||||||
public partial class Orders : ComponentBase
|
public partial class Orders : ComponentBase
|
||||||
{
|
{
|
||||||
[Inject] private OrderService OrderService { get; set; } = default!;
|
[Inject] private OrderService OrderService { get; set; } = default!;
|
||||||
[Inject] private VATService VAT { get; set; } = default!;
|
|
||||||
|
|
||||||
private List<GetUserOrderResponse> _orders = new();
|
private List<GetUserOrderResponse> _orders = new();
|
||||||
private bool _loading;
|
private bool _loading;
|
||||||
@@ -33,28 +32,81 @@ public partial class Orders : ComponentBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
private void ViewOrder(long orderId)
|
||||||
{
|
{
|
||||||
if (firstRender)
|
Navigation.NavigateTo($"{RouteConstants.Store.OrderDetail}{orderId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatPrice(long price) => $"{price:N0}";
|
||||||
|
|
||||||
|
private static string FormatDate(GetUserOrderResponse order)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
// بارگذاری نرخ VAT
|
var timestamp = order.PaymentDate;
|
||||||
await VAT.LoadAsync();
|
if (timestamp is null)
|
||||||
|
return "در انتظار پرداخت";
|
||||||
|
|
||||||
|
return timestamp.ToDateTime().ToLocalTime().MiladiToJalaliWithTime();
|
||||||
}
|
}
|
||||||
await base.OnAfterRenderAsync(firstRender);
|
catch
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
|
|
||||||
|
|
||||||
|
|
||||||
private string GetStatusText(Messages.PaymentStatus contextPaymentStatus)
|
|
||||||
{
|
|
||||||
return contextPaymentStatus switch
|
|
||||||
{
|
{
|
||||||
Messages.PaymentStatus.Pending => "در انتظار پرداخت",
|
return "—";
|
||||||
Messages.PaymentStatus.Success => "پرداخت شده",
|
}
|
||||||
Messages.PaymentStatus.Reject => "پرداخت ناموفق",
|
|
||||||
_ => "نامشخص",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
private static int GetItemsCount(GetUserOrderResponse order) =>
|
||||||
|
order.FactorDetails.Sum(f => f.Count ?? 0);
|
||||||
|
|
||||||
|
private static long GetTotalAmount(GetUserOrderResponse order)
|
||||||
|
{
|
||||||
|
if (order.VatInfo is not null && order.VatInfo.TotalAmount > 0)
|
||||||
|
return order.VatInfo.TotalAmount;
|
||||||
|
|
||||||
|
if (order.Amount > 0)
|
||||||
|
return order.Amount;
|
||||||
|
|
||||||
|
return order.FactorDetails.Sum(f => (f.UnitPrice ?? 0) * (f.Count ?? 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetPaymentStatusText(Messages.PaymentStatus status) => status switch
|
||||||
|
{
|
||||||
|
Messages.PaymentStatus.Pending => "در انتظار پرداخت",
|
||||||
|
Messages.PaymentStatus.Success => "پرداخت شده",
|
||||||
|
Messages.PaymentStatus.Reject => "پرداخت ناموفق",
|
||||||
|
_ => "نامشخص"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Color GetPaymentStatusColor(Messages.PaymentStatus status) => status switch
|
||||||
|
{
|
||||||
|
Messages.PaymentStatus.Pending => Color.Warning,
|
||||||
|
Messages.PaymentStatus.Success => Color.Success,
|
||||||
|
Messages.PaymentStatus.Reject => Color.Error,
|
||||||
|
_ => Color.Default
|
||||||
|
};
|
||||||
|
|
||||||
|
// Domain/public_messages: None=0, Pending=1, InTransit=2, Delivered=3, Returned=4, Cancelled=5, ReadyForOfficePickup=6
|
||||||
|
private static string GetDeliveryStatusText(int status) => status switch
|
||||||
|
{
|
||||||
|
0 => "بدون ارسال",
|
||||||
|
1 => "در انتظار ارسال",
|
||||||
|
2 => "ارسال شده",
|
||||||
|
3 => "تحویل داده شده",
|
||||||
|
4 => "مرجوع شده",
|
||||||
|
5 => "لغو شده",
|
||||||
|
6 => "آماده تحویل در دفتر",
|
||||||
|
_ => "نامشخص"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Color GetDeliveryStatusColor(int status) => status switch
|
||||||
|
{
|
||||||
|
0 => Color.Default,
|
||||||
|
1 => Color.Warning,
|
||||||
|
2 => Color.Primary,
|
||||||
|
3 => Color.Success,
|
||||||
|
4 => Color.Error,
|
||||||
|
5 => Color.Dark,
|
||||||
|
6 => Color.Primary,
|
||||||
|
_ => Color.Default
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -78,15 +78,24 @@ else
|
|||||||
@* </MudStack> *@
|
@* </MudStack> *@
|
||||||
@* } *@
|
@* } *@
|
||||||
<MudDivider Class="my-2"/>
|
<MudDivider Class="my-2"/>
|
||||||
<MudStack Spacing="1">
|
@if (_isAuthenticated)
|
||||||
<MudText Typo="Typo.h5" Color="Color.Primary">@FormatPrice(_product.Price)</MudText>
|
{
|
||||||
@if (VAT.IsEnabled)
|
<MudStack Spacing="1">
|
||||||
{
|
<MudText Typo="Typo.h5" Color="Color.Primary">@FormatPrice(_product.Price)</MudText>
|
||||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
@if (VAT.IsEnabled)
|
||||||
(شامل @VAT.VatPercentage% مالیات بر ارزش افزوده)
|
{
|
||||||
</MudText>
|
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
||||||
}
|
(شامل @VAT.VatPercentage% مالیات بر ارزش افزوده)
|
||||||
</MudStack>
|
</MudText>
|
||||||
|
}
|
||||||
|
</MudStack>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined" Icon="@Icons.Material.Filled.Lock">
|
||||||
|
برای مشاهده قیمت ابتدا وارد شوید
|
||||||
|
</MudAlert>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- نمایش وضعیت موجودی -->
|
<!-- نمایش وضعیت موجودی -->
|
||||||
@if (IsInStock)
|
@if (IsInStock)
|
||||||
@@ -141,18 +150,27 @@ else
|
|||||||
{
|
{
|
||||||
<MudGrid Class="align-center" Justify="Justify.SpaceBetween">
|
<MudGrid Class="align-center" Justify="Justify.SpaceBetween">
|
||||||
<MudItem xs="6">
|
<MudItem xs="6">
|
||||||
<MudStack Spacing="1">
|
@if (_isAuthenticated)
|
||||||
@if (HasDiscount && OriginalPrice is not null)
|
{
|
||||||
{
|
<MudStack Spacing="1">
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
@if (HasDiscount && OriginalPrice is not null)
|
||||||
<MudText Typo="Typo.caption"
|
{
|
||||||
Class="mud-text-secondary mud-line-through">@FormatPrice(OriginalPrice.Value)</MudText>
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||||
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small"
|
<MudText Typo="Typo.caption"
|
||||||
Label="true">@($"٪{_product!.Discount}")</MudChip>
|
Class="mud-text-secondary mud-line-through">@FormatPrice(OriginalPrice.Value)</MudText>
|
||||||
</MudStack>
|
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small"
|
||||||
}
|
Label="true">@($"٪{_product!.Discount}")</MudChip>
|
||||||
<MudText Typo="Typo.h6" Color="Color.Primary">@FormatPrice(TotalPrice)</MudText>
|
</MudStack>
|
||||||
</MudStack>
|
}
|
||||||
|
<MudText Typo="Typo.h6" Color="Color.Primary">@FormatPrice(TotalPrice)</MudText>
|
||||||
|
</MudStack>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Small" Class="me-1"/>برای مشاهده قیمت وارد شوید
|
||||||
|
</MudText>
|
||||||
|
}
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
|
||||||
@if (IsInCart)
|
@if (IsInCart)
|
||||||
|
|||||||
@@ -12,11 +12,16 @@ public partial class ProductDetail : ComponentBase, IDisposable
|
|||||||
[Inject] private ProductService ProductService { get; set; } = default!;
|
[Inject] private ProductService ProductService { get; set; } = default!;
|
||||||
[Inject] private CartService Cart { get; set; } = default!;
|
[Inject] private CartService Cart { get; set; } = default!;
|
||||||
[Inject] private VATService VAT { get; set; } = default!;
|
[Inject] private VATService VAT { get; set; } = default!;
|
||||||
|
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
||||||
|
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||||
|
|
||||||
|
private bool _isAuthenticated;
|
||||||
|
|
||||||
[Parameter] public long id { get; set; }
|
[Parameter] public long id { get; set; }
|
||||||
|
|
||||||
private Product? _product;
|
private Product? _product;
|
||||||
private bool _loading;
|
private bool _loading;
|
||||||
|
private bool _initialized;
|
||||||
private int _qty = 1;
|
private int _qty = 1;
|
||||||
private const int MinQty = 1;
|
private const int MinQty = 1;
|
||||||
|
|
||||||
@@ -44,29 +49,35 @@ public partial class ProductDetail : ComponentBase, IDisposable
|
|||||||
private bool IsInCart => CurrentCartItem is not null;
|
private bool IsInCart => CurrentCartItem is not null;
|
||||||
private int CurrentCartQuantity => CurrentCartItem?.Quantity ?? 0;
|
private int CurrentCartQuantity => CurrentCartItem?.Quantity ?? 0;
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
|
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
|
||||||
await Cart.EnsureInitializedAsync();
|
await Cart.EnsureInitializedAsync();
|
||||||
Cart.OnChange += HandleCartChanged;
|
Cart.OnChange += HandleCartChanged;
|
||||||
|
_initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnParametersSetAsync()
|
||||||
|
{
|
||||||
|
if (!_initialized || id <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await LoadProductAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadProductAsync()
|
||||||
|
{
|
||||||
_loading = true;
|
_loading = true;
|
||||||
_product = await ProductService.GetByIdAsync(id);
|
_product = await ProductService.GetByIdAsync(id);
|
||||||
_loading = false;
|
_loading = false;
|
||||||
|
|
||||||
if (_product is not null)
|
if (_product is not null)
|
||||||
{
|
{
|
||||||
_galleryItems = BuildGalleryItems(_product);
|
_galleryItems = BuildGalleryItems(_product);
|
||||||
_selectedGalleryImage = _galleryItems.FirstOrDefault();
|
_selectedGalleryImage = _galleryItems.FirstOrDefault();
|
||||||
_categoryPaths = _product.Categories;
|
_categoryPaths = _product.Categories;
|
||||||
UpdateBreadcrumb();
|
UpdateBreadcrumb();
|
||||||
_qty = Math.Clamp(CurrentCartItem?.Quantity ?? _qty, MinQty, MaxQty);
|
_qty = Math.Clamp(CurrentCartItem?.Quantity ?? MinQty, MinQty, Math.Max(MinQty, MaxQty));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -74,32 +85,33 @@ public partial class ProductDetail : ComponentBase, IDisposable
|
|||||||
_categoryPaths = Array.Empty<ProductCategoryPathInfo>();
|
_categoryPaths = Array.Empty<ProductCategoryPathInfo>();
|
||||||
_breadcrumbItems.Clear();
|
_breadcrumbItems.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
StateHasChanged();
|
|
||||||
await base.OnInitializedAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
await base.OnAfterRenderAsync(firstRender);
|
|
||||||
if (firstRender)
|
if (firstRender)
|
||||||
{
|
{
|
||||||
// بارگذاری نرخ VAT
|
|
||||||
await VAT.LoadAsync();
|
await VAT.LoadAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await base.OnAfterRenderAsync(firstRender);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task AddToCart()
|
private async Task AddToCart()
|
||||||
{
|
{
|
||||||
if (_product is null) return;
|
if (_product is null) return;
|
||||||
await Cart.Add(_product, 1);
|
var product = _product;
|
||||||
|
await GuestGate.RunAsync(() => Cart.Add(product, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RemoveFromCart()
|
private async Task RemoveFromCart()
|
||||||
{
|
{
|
||||||
if (_product is null) return;
|
if (_product is null) return;
|
||||||
_qty--;
|
await GuestGate.RunAsync(async () =>
|
||||||
await Cart.UpdateQuantity(CurrentCartItem.ProductId, _qty);
|
{
|
||||||
|
_qty--;
|
||||||
|
await Cart.UpdateQuantity(CurrentCartItem!.ProductId, _qty);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void IncreaseLocalQty()
|
private void IncreaseLocalQty()
|
||||||
|
|||||||
@@ -118,7 +118,8 @@
|
|||||||
@foreach (var p in _products)
|
@foreach (var p in _products)
|
||||||
{
|
{
|
||||||
<MudItem xs="6" sm="6" md="3"
|
<MudItem xs="6" sm="6" md="3"
|
||||||
onclick="@(() => Navigation.NavigateTo($"{RouteConstants.Store.ProductDetail}{p.Id}"))">
|
onclick="@(() => OpenProduct(p.Id))">
|
||||||
|
<div id="@($"shop-product-{p.Id}")" class="h-100">
|
||||||
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden"
|
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden"
|
||||||
Style="cursor:pointer;">
|
Style="cursor:pointer;">
|
||||||
|
|
||||||
@@ -141,8 +142,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="pa-1 flex-grow-1 d-flex flex-column justify-space-between">
|
<div class="pa-1 flex-grow-1 d-flex flex-column justify-space-between">
|
||||||
<MudText Typo="Typo.subtitle1">@p.Title</MudText>
|
<MudText Typo="Typo.subtitle1">@p.Title</MudText>
|
||||||
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatPrice(p.Price)</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>
|
</div>
|
||||||
</MudCardContent>
|
</MudCardContent>
|
||||||
<MudCardActions Class="mt-auto d-flex justify-space-between pa-2">
|
<MudCardActions Class="mt-auto d-flex justify-space-between pa-2">
|
||||||
@@ -156,6 +165,7 @@
|
|||||||
</MudButton>
|
</MudButton>
|
||||||
</MudCardActions>
|
</MudCardActions>
|
||||||
</MudCard>
|
</MudCard>
|
||||||
|
</div>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
}
|
}
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
|
|||||||
@@ -2,26 +2,22 @@ using System.Linq;
|
|||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.AspNetCore.Components.Routing;
|
using Microsoft.AspNetCore.Components.Routing;
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
using Microsoft.AspNetCore.WebUtilities;
|
using Microsoft.JSInterop;
|
||||||
using FrontOffice.Main.Utilities;
|
using FrontOffice.Main.Utilities;
|
||||||
|
|
||||||
namespace FrontOffice.Main.Pages.Store;
|
namespace FrontOffice.Main.Pages.Store;
|
||||||
|
|
||||||
public enum ProductSortOption
|
|
||||||
{
|
|
||||||
PriceDesc, // گرانترین (پیشفرض)
|
|
||||||
PriceAsc, // ارزانترین
|
|
||||||
Newest, // جدیدترین
|
|
||||||
Title // الفبایی
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class Products : ComponentBase, IDisposable
|
public partial class Products : ComponentBase, IDisposable
|
||||||
{
|
{
|
||||||
[Inject] private ProductService ProductService { get; set; } = default!;
|
[Inject] private ProductService ProductService { get; set; } = default!;
|
||||||
[Inject] private CategoryService CategoryService { get; set; } = default!;
|
[Inject] private CategoryService CategoryService { get; set; } = default!;
|
||||||
[Inject] private CartService Cart { get; set; } = default!;
|
[Inject] private CartService Cart { get; set; } = default!;
|
||||||
[Inject] private VATService VAT { get; set; } = default!;
|
[Inject] private VATService VAT { get; set; } = default!;
|
||||||
|
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
||||||
|
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||||
|
[Inject] private IJSRuntime Js { get; set; } = default!;
|
||||||
|
|
||||||
|
private bool _isAuthenticated;
|
||||||
private string _query = string.Empty;
|
private string _query = string.Empty;
|
||||||
private bool _loading;
|
private bool _loading;
|
||||||
private bool _loadingMore;
|
private bool _loadingMore;
|
||||||
@@ -32,45 +28,108 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
private List<Product> _products = new();
|
private List<Product> _products = new();
|
||||||
private long? _activeCategoryId;
|
private long? _activeCategoryId;
|
||||||
private string? _activeCategoryTitle;
|
private string? _activeCategoryTitle;
|
||||||
|
private ProductSortOption _sortOption = ProductSortOption.PriceDesc;
|
||||||
private ProductSortOption _sortOption = ProductSortOption.PriceDesc; // پیشفرض: گرانترین
|
private bool _ignoreNextLocationChange;
|
||||||
|
private bool _pendingScrollRestore;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
|
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
|
||||||
await Cart.EnsureInitializedAsync();
|
await Cart.EnsureInitializedAsync();
|
||||||
Cart.OnChange += StateHasChanged;
|
Cart.OnChange += StateHasChanged;
|
||||||
Navigation.LocationChanged += HandleLocationChanged;
|
Navigation.LocationChanged += HandleLocationChanged;
|
||||||
await LoadInitial();
|
ApplyStateFromUri();
|
||||||
|
await LoadPages(_currentPage);
|
||||||
|
_pendingScrollRestore = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (firstRender)
|
||||||
{
|
|
||||||
// بارگذاری نرخ VAT
|
|
||||||
await VAT.LoadAsync();
|
await VAT.LoadAsync();
|
||||||
|
|
||||||
|
if (_pendingScrollRestore && !_loading && _products.Count > 0)
|
||||||
|
{
|
||||||
|
_pendingScrollRestore = false;
|
||||||
|
var payload = await ShopListScrollRestore.TakeAsync(Js, ShopListScrollRestore.StoreKey);
|
||||||
|
if (payload is not null)
|
||||||
|
await ShopListScrollRestore.RestoreAsync(Js, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
await base.OnAfterRenderAsync(firstRender);
|
await base.OnAfterRenderAsync(firstRender);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadInitial()
|
private void ApplyStateFromUri()
|
||||||
|
{
|
||||||
|
var state = ShopListQueryState.Parse(Navigation.ToAbsoluteUri(Navigation.Uri));
|
||||||
|
_query = state.Query;
|
||||||
|
_sortOption = ShopListQueryState.ParseSortOption(state.Sort);
|
||||||
|
_activeCategoryId = state.CategoryId;
|
||||||
|
_currentPage = state.Pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ShopListQueryState CaptureState() => new()
|
||||||
|
{
|
||||||
|
Query = _query,
|
||||||
|
Sort = ShopListQueryState.ToSortKey(_sortOption),
|
||||||
|
CategoryId = _activeCategoryId,
|
||||||
|
Pages = Math.Max(1, _currentPage)
|
||||||
|
};
|
||||||
|
|
||||||
|
private void SyncUrl()
|
||||||
|
{
|
||||||
|
var target = CaptureState().ToRelativeUrl(RouteConstants.Store.Products);
|
||||||
|
var currentPathAndQuery = Navigation.ToAbsoluteUri(Navigation.Uri).PathAndQuery;
|
||||||
|
if (string.Equals(currentPathAndQuery, target, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(currentPathAndQuery, target + "/", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_ignoreNextLocationChange = true;
|
||||||
|
Navigation.NavigateTo(target, replace: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadPages(int pagesToLoad)
|
||||||
{
|
{
|
||||||
_loading = true;
|
_loading = true;
|
||||||
_currentPage = 1;
|
|
||||||
_products.Clear();
|
_products.Clear();
|
||||||
UpdateCategoryFilterFromUri();
|
_hasMore = true;
|
||||||
var sortBy = GetSortByValue();
|
pagesToLoad = Math.Max(1, pagesToLoad);
|
||||||
var result = await ProductService.GetProductsPagedAsync(_query, _activeCategoryId, sortBy, _currentPage, PageSize);
|
var sortBy = ShopListQueryState.ToApiSortBy(_sortOption);
|
||||||
_products = result.Products;
|
|
||||||
_hasMore = result.HasNext;
|
for (var page = 1; page <= pagesToLoad; page++)
|
||||||
_totalCount = result.TotalCount;
|
{
|
||||||
|
var result = await ProductService.GetProductsPagedAsync(
|
||||||
|
_query, _activeCategoryId, sortBy, page, PageSize);
|
||||||
|
if (page == 1)
|
||||||
|
{
|
||||||
|
_products = result.Products;
|
||||||
|
_totalCount = result.TotalCount;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_products.AddRange(result.Products);
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentPage = page;
|
||||||
|
_hasMore = result.HasNext;
|
||||||
|
if (!_hasMore)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
_activeCategoryTitle = _activeCategoryId is { } categoryId
|
_activeCategoryTitle = _activeCategoryId is { } categoryId
|
||||||
? (await CategoryService.GetByIdAsync(categoryId))?.Title
|
? (await CategoryService.GetByIdAsync(categoryId))?.Title
|
||||||
: null;
|
: null;
|
||||||
_loading = false;
|
_loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ReloadFromFilters()
|
||||||
|
{
|
||||||
|
_currentPage = 1;
|
||||||
|
await LoadPages(1);
|
||||||
|
SyncUrl();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task LoadMore()
|
private async Task LoadMore()
|
||||||
{
|
{
|
||||||
if (_loadingMore || !_hasMore) return;
|
if (_loadingMore || !_hasMore) return;
|
||||||
@@ -79,40 +138,30 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
|
|
||||||
_currentPage++;
|
_currentPage++;
|
||||||
var sortBy = GetSortByValue();
|
var sortBy = ShopListQueryState.ToApiSortBy(_sortOption);
|
||||||
var result = await ProductService.GetProductsPagedAsync(_query, _activeCategoryId, sortBy, _currentPage, PageSize);
|
var result = await ProductService.GetProductsPagedAsync(
|
||||||
|
_query, _activeCategoryId, sortBy, _currentPage, PageSize);
|
||||||
_products.AddRange(result.Products);
|
_products.AddRange(result.Products);
|
||||||
_hasMore = result.HasNext;
|
_hasMore = result.HasNext;
|
||||||
|
SyncUrl();
|
||||||
|
|
||||||
_loadingMore = false;
|
_loadingMore = false;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetSortByValue()
|
private async Task OnSortChanged() => await ReloadFromFilters();
|
||||||
{
|
|
||||||
return _sortOption switch
|
|
||||||
{
|
|
||||||
ProductSortOption.PriceDesc => "price desc",
|
|
||||||
ProductSortOption.PriceAsc => "price asc",
|
|
||||||
ProductSortOption.Newest => "id desc",
|
|
||||||
ProductSortOption.Title => "title asc",
|
|
||||||
_ => "price desc"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnSortChanged()
|
private async Task OnQueryChanged(KeyboardEventArgs _) => await ReloadFromFilters();
|
||||||
{
|
|
||||||
await LoadInitial();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnQueryChanged(KeyboardEventArgs _)
|
|
||||||
{
|
|
||||||
await LoadInitial();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddToCart(Product p)
|
private async Task AddToCart(Product p)
|
||||||
{
|
{
|
||||||
await Cart.Add(p, 1);
|
await GuestGate.RunAsync(() => Cart.Add(p, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OpenProduct(long productId)
|
||||||
|
{
|
||||||
|
await ShopListScrollRestore.SaveAsync(Js, ShopListScrollRestore.StoreKey, productId);
|
||||||
|
Navigation.NavigateTo($"{RouteConstants.Store.ProductDetail}{productId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private string FormatPrice(long price) => $"{VAT.AddVAT(price):N0} تومان";
|
private string FormatPrice(long price) => $"{VAT.AddVAT(price):N0} تومان";
|
||||||
@@ -125,24 +174,33 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
|
|
||||||
private void HandleLocationChanged(object? sender, LocationChangedEventArgs args)
|
private void HandleLocationChanged(object? sender, LocationChangedEventArgs args)
|
||||||
{
|
{
|
||||||
_ = InvokeAsync(LoadInitial);
|
if (_ignoreNextLocationChange)
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateCategoryFilterFromUri()
|
|
||||||
{
|
|
||||||
var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
|
|
||||||
if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("category", out var values) &&
|
|
||||||
long.TryParse(values.FirstOrDefault(), out var categoryId))
|
|
||||||
{
|
{
|
||||||
_activeCategoryId = categoryId;
|
_ignoreNextLocationChange = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_activeCategoryId = null;
|
var uri = Navigation.ToAbsoluteUri(args.Location);
|
||||||
|
if (!uri.AbsolutePath.Equals(RouteConstants.Store.Products, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var incoming = ShopListQueryState.Parse(uri);
|
||||||
|
if (incoming.Matches(CaptureState()))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_ = InvokeAsync(async () =>
|
||||||
|
{
|
||||||
|
ApplyStateFromUri();
|
||||||
|
await LoadPages(_currentPage);
|
||||||
|
_pendingScrollRestore = true;
|
||||||
|
StateHasChanged();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ClearCategoryFilter()
|
private async Task ClearCategoryFilter()
|
||||||
{
|
{
|
||||||
Navigation.NavigateTo(RouteConstants.Store.Products);
|
_activeCategoryId = null;
|
||||||
|
_activeCategoryTitle = null;
|
||||||
|
await ReloadFromFilters();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
|
@model FrontOffice.Main.Pages.HostModel
|
||||||
@using Microsoft.AspNetCore.Components.Web
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
@namespace FrontOffice.Main.Pages
|
@namespace FrontOffice.Main.Pages
|
||||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
@@ -8,6 +9,26 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>@Model.Seo.Title</title>
|
||||||
|
<meta name="description" content="@Model.Seo.Description" />
|
||||||
|
<link rel="canonical" href="@Model.Seo.CanonicalUrl" />
|
||||||
|
@if (Model.Seo.NoIndex)
|
||||||
|
{
|
||||||
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<meta name="robots" content="index, follow" />
|
||||||
|
}
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:locale" content="fa_IR" />
|
||||||
|
<meta property="og:title" content="@Model.Seo.Title" />
|
||||||
|
<meta property="og:description" content="@Model.Seo.Description" />
|
||||||
|
<meta property="og:url" content="@Model.Seo.CanonicalUrl" />
|
||||||
|
<meta property="og:site_name" content="کارابازار سلامت" />
|
||||||
|
<meta name="twitter:card" content="summary" />
|
||||||
|
<meta name="twitter:title" content="@Model.Seo.Title" />
|
||||||
|
<meta name="twitter:description" content="@Model.Seo.Description" />
|
||||||
<meta name="theme-color" content="#7c4dff" />
|
<meta name="theme-color" content="#7c4dff" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||||
@@ -15,7 +36,7 @@
|
|||||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||||
<component type="typeof(HeadOutlet)" render-mode="Server" />
|
<component type="typeof(HeadOutlet)" render-mode="Server" />
|
||||||
|
|
||||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
|
@* <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" /> *@
|
||||||
<!-- MudBlazor CSS (باید قبل از site.css باشه تا override بشه) -->
|
<!-- MudBlazor CSS (باید قبل از site.css باشه تا override بشه) -->
|
||||||
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" asp-append-version="true" />
|
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" asp-append-version="true" />
|
||||||
<!-- Custom styles (بعد از MudBlazor برای override) -->
|
<!-- Custom styles (بعد از MudBlazor برای override) -->
|
||||||
@@ -23,8 +44,26 @@
|
|||||||
<link href="FrontOffice.Main.styles.css" rel="stylesheet" />
|
<link href="FrontOffice.Main.styles.css" rel="stylesheet" />
|
||||||
<!-- d3-org-chart custom styles -->
|
<!-- d3-org-chart custom styles -->
|
||||||
<link href="css/org-chart.css" rel="stylesheet" asp-append-version="true" />
|
<link href="css/org-chart.css" rel="stylesheet" asp-append-version="true" />
|
||||||
|
<style>
|
||||||
|
.seo-crawlable {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Model.Seo.CrawlableContent))
|
||||||
|
{
|
||||||
|
<div class="seo-crawlable">@Model.Seo.CrawlableContent</div>
|
||||||
|
}
|
||||||
|
|
||||||
<component type="typeof(App)" render-mode="Server" />
|
<component type="typeof(App)" render-mode="Server" />
|
||||||
|
|
||||||
<div id="blazor-error-ui">
|
<div id="blazor-error-ui">
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using FrontOffice.Main.Utilities.Seo;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace FrontOffice.Main.Pages;
|
||||||
|
|
||||||
|
public class HostModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly SeoMetadataProvider _seoMetadataProvider;
|
||||||
|
|
||||||
|
public SeoPageMetadata Seo { get; private set; } = default!;
|
||||||
|
|
||||||
|
public HostModel(SeoMetadataProvider seoMetadataProvider)
|
||||||
|
{
|
||||||
|
_seoMetadataProvider = seoMetadataProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnGetAsync()
|
||||||
|
{
|
||||||
|
Seo = await _seoMetadataProvider.GetForPathAsync(Request.Path);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using FrontOffice.Main.Utilities;
|
using FrontOffice.Main.Utilities;
|
||||||
|
using FrontOffice.Main.Utilities.Seo;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using FrontOffice.Main.Utilities.Pdf;
|
using FrontOffice.Main.Utilities.Pdf;
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ builder.Services.AddServerSideBlazor();
|
|||||||
#region AddCommonServices
|
#region AddCommonServices
|
||||||
|
|
||||||
builder.Services.AddCommonServices();
|
builder.Services.AddCommonServices();
|
||||||
|
builder.Services.Configure<SeoSettings>(builder.Configuration.GetSection(SeoSettings.SectionName));
|
||||||
builder.Services.AddSingleton<MainService>();
|
builder.Services.AddSingleton<MainService>();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -55,6 +57,17 @@ webApp.UseStaticFiles();
|
|||||||
webApp.UseRouting();
|
webApp.UseRouting();
|
||||||
|
|
||||||
webApp.MapBlazorHub();
|
webApp.MapBlazorHub();
|
||||||
|
|
||||||
|
webApp.MapGet("/sitemap.xml", async (SitemapGenerator sitemapGenerator, CancellationToken cancellationToken) =>
|
||||||
|
{
|
||||||
|
var bytes = await sitemapGenerator.GenerateBytesAsync(cancellationToken);
|
||||||
|
return Results.File(bytes, "application/xml; charset=utf-8");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Legacy URLs from old site / common e-commerce paths — redirect to homepage
|
||||||
|
webApp.MapGet("/wishlist", () => Results.Redirect("/", permanent: true));
|
||||||
|
webApp.MapGet("/wishlist/", () => Results.Redirect("/", permanent: true));
|
||||||
|
|
||||||
webApp.MapFallbackToPage("/_Host");
|
webApp.MapFallbackToPage("/_Host");
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
PhoneNumber="@_phoneNumber"
|
PhoneNumber="@_phoneNumber"
|
||||||
ResendRemaining="_resendRemaining"
|
ResendRemaining="_resendRemaining"
|
||||||
OnChangePhone="ChangePhoneAsync"
|
OnChangePhone="ChangePhoneAsync"
|
||||||
OnResendOtp="ResendOtpAsync" />
|
OnResendOtp="ResendOtpAsync"
|
||||||
|
ReferralCode="@_referralCode"
|
||||||
|
ReferralCodeChanged="OnReferralCodeChanged" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -51,7 +53,9 @@ else
|
|||||||
PhoneNumber="@_phoneNumber"
|
PhoneNumber="@_phoneNumber"
|
||||||
ResendRemaining="_resendRemaining"
|
ResendRemaining="_resendRemaining"
|
||||||
OnChangePhone="ChangePhoneAsync"
|
OnChangePhone="ChangePhoneAsync"
|
||||||
OnResendOtp="ResendOtpAsync" />
|
OnResendOtp="ResendOtpAsync"
|
||||||
|
ReferralCode="@_referralCode"
|
||||||
|
ReferralCodeChanged="OnReferralCodeChanged" />
|
||||||
|
|
||||||
<MudStack Class="mt-4" Spacing="2">
|
<MudStack Class="mt-4" Spacing="2">
|
||||||
@if (_currentStep == AuthStep.Phone)
|
@if (_currentStep == AuthStep.Phone)
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ public partial class AuthDialog : IDisposable
|
|||||||
private string? _captchaCode;
|
private string? _captchaCode;
|
||||||
private string? _captchaInput;
|
private string? _captchaInput;
|
||||||
|
|
||||||
|
// Referral code field
|
||||||
|
private string? _referralCode;
|
||||||
|
|
||||||
[Inject] private ILocalStorageService LocalStorage { get; set; } = default!;
|
[Inject] private ILocalStorageService LocalStorage { get; set; } = default!;
|
||||||
[Inject] private UserContract.UserContractClient UserClient { get; set; } = default!;
|
[Inject] private UserContract.UserContractClient UserClient { get; set; } = default!;
|
||||||
|
|
||||||
@@ -66,9 +69,20 @@ public partial class AuthDialog : IDisposable
|
|||||||
{
|
{
|
||||||
_phoneRequest.Mobile = storedPhone;
|
_phoneRequest.Mobile = storedPhone;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_referralCode = await LocalStorage.GetItemAsync<string>("referral:code");
|
||||||
// await LocalStorage.RemoveItemAsync(TokenStorageKey);
|
// await LocalStorage.RemoveItemAsync(TokenStorageKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task OnReferralCodeChanged(string? value)
|
||||||
|
{
|
||||||
|
_referralCode = value;
|
||||||
|
if (!string.IsNullOrWhiteSpace(value))
|
||||||
|
await LocalStorage.SetItemAsync("referral:code", value);
|
||||||
|
else
|
||||||
|
await LocalStorage.RemoveItemAsync("referral:code");
|
||||||
|
}
|
||||||
|
|
||||||
private void GenerateCaptcha()
|
private void GenerateCaptcha()
|
||||||
{
|
{
|
||||||
_captchaCode = Guid.NewGuid().ToString("N")[..6].ToUpperInvariant();
|
_captchaCode = Guid.NewGuid().ToString("N")[..6].ToUpperInvariant();
|
||||||
@@ -182,9 +196,12 @@ public partial class AuthDialog : IDisposable
|
|||||||
_verifyRequest.Mobile = _phoneNumber;
|
_verifyRequest.Mobile = _phoneNumber;
|
||||||
|
|
||||||
var storedReferralCode = await LocalStorage.GetItemAsync<string>("referral:code");
|
var storedReferralCode = await LocalStorage.GetItemAsync<string>("referral:code");
|
||||||
if (!string.IsNullOrWhiteSpace(storedReferralCode))
|
var effectiveReferralCode = !string.IsNullOrWhiteSpace(_referralCode)
|
||||||
|
? _referralCode
|
||||||
|
: storedReferralCode;
|
||||||
|
if (!string.IsNullOrWhiteSpace(effectiveReferralCode))
|
||||||
{
|
{
|
||||||
_verifyRequest.ParentReferralCode = storedReferralCode;
|
_verifyRequest.ParentReferralCode = effectiveReferralCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
var validationResult = true; // _verifyRequestValidator.Validate(_verifyRequest);
|
var validationResult = true; // _verifyRequestValidator.Validate(_verifyRequest);
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
@using FrontOffice.Main.Utilities
|
||||||
|
|
||||||
|
<MudDialog>
|
||||||
|
<TitleContent>
|
||||||
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Color="Color.Warning" />
|
||||||
|
<MudText Typo="Typo.h6">موجودی کیف پول اصلی کافی نیست</MudText>
|
||||||
|
</MudStack>
|
||||||
|
</TitleContent>
|
||||||
|
<DialogContent>
|
||||||
|
<MudStack Spacing="2">
|
||||||
|
@if (AllowChargeCredit)
|
||||||
|
{
|
||||||
|
@if (IsMagicWallet && !MagicCeilingFull)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body2" Class="mud-text-secondary">
|
||||||
|
برای تکمیل این سفارش موجودی شما کافی نیست. کیف پول شما در حالت جادویی است؛ برای دریافت ضریب، از مسیر شارژ جادویی اقدام کنید.
|
||||||
|
</MudText>
|
||||||
|
}
|
||||||
|
else if (IsMagicWallet && MagicCeilingFull)
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Warning" Dense="true" Variant="Variant.Outlined">
|
||||||
|
سقف شارژ جادویی شما در این دور پر است. میتوانید بهصورت استثنایی کیف اصلی را بدون ضریب (۱:۱) شارژ کنید.
|
||||||
|
</MudAlert>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body2" Class="mud-text-secondary">
|
||||||
|
برای تکمیل این سفارش، موجودی اصلی شما کافی نیست. میتوانید کیف پول را شارژ کنید و سپس سفارش را ثبت کنید.
|
||||||
|
</MudText>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Warning" Dense="true" Variant="Variant.Outlined">
|
||||||
|
برای شارژ کیف پول اصلی ابتدا باید پکیج را خریداری کرده و قرارداد باشگاه مشتریان را امضا کنید.
|
||||||
|
</MudAlert>
|
||||||
|
}
|
||||||
|
|
||||||
|
<MudPaper Outlined="true" Class="pa-3 rounded-lg">
|
||||||
|
<MudStack Spacing="1">
|
||||||
|
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||||
|
<MudText Typo="Typo.body2">موجودی فعلی:</MudText>
|
||||||
|
<MudText Typo="Typo.body2"><strong>@FormatPrice(CurrentBalance)</strong></MudText>
|
||||||
|
</MudStack>
|
||||||
|
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||||
|
<MudText Typo="Typo.body2">مبلغ سفارش:</MudText>
|
||||||
|
<MudText Typo="Typo.body2"><strong>@FormatPrice(RequiredAmount)</strong></MudText>
|
||||||
|
</MudStack>
|
||||||
|
<MudDivider Class="my-1" />
|
||||||
|
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||||
|
<MudText Typo="Typo.body2" Color="Color.Error">کمبود:</MudText>
|
||||||
|
<MudText Typo="Typo.body2" Color="Color.Error"><strong>@FormatPrice(ShortfallAmount)</strong></MudText>
|
||||||
|
</MudStack>
|
||||||
|
</MudStack>
|
||||||
|
</MudPaper>
|
||||||
|
</MudStack>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<MudButton OnClick="Cancel">بستن</MudButton>
|
||||||
|
@if (AllowChargeCredit)
|
||||||
|
{
|
||||||
|
<MudButton Variant="Variant.Filled"
|
||||||
|
Color="@(IsMagicWallet && !MagicCeilingFull ? Color.Secondary : Color.Primary)"
|
||||||
|
StartIcon="@(IsMagicWallet && !MagicCeilingFull ? Icons.Material.Filled.AutoAwesome : Icons.Material.Filled.Payment)"
|
||||||
|
OnClick="GoToCharge">
|
||||||
|
@ChargeButtonLabel
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
else if (!HasPurchasedPackage)
|
||||||
|
{
|
||||||
|
<MudButton Variant="Variant.Filled"
|
||||||
|
Color="Color.Primary"
|
||||||
|
StartIcon="@Icons.Material.Filled.CardGiftcard"
|
||||||
|
OnClick="GoToPackages">
|
||||||
|
مشاهده پکیجها
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudButton Variant="Variant.Filled"
|
||||||
|
Color="Color.Primary"
|
||||||
|
StartIcon="@Icons.Material.Filled.Handshake"
|
||||||
|
OnClick="GoToClubMembership">
|
||||||
|
امضای قرارداد باشگاه
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
</DialogActions>
|
||||||
|
</MudDialog>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[CascadingParameter]
|
||||||
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public long CurrentBalance { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public long RequiredAmount { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public long ShortfallAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>اگر false باشد، به صفحه شارژ هدایت نمیشود.</summary>
|
||||||
|
[Parameter]
|
||||||
|
public bool AllowChargeCredit { get; set; } = true;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool HasPurchasedPackage { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool IsMagicWallet { get; set; }
|
||||||
|
|
||||||
|
/// <summary>سقف واریز جادویی این دور پر شده است.</summary>
|
||||||
|
[Parameter]
|
||||||
|
public bool MagicCeilingFull { get; set; }
|
||||||
|
|
||||||
|
private string ChargeButtonLabel =>
|
||||||
|
IsMagicWallet && !MagicCeilingFull
|
||||||
|
? "شارژ کیف جادویی"
|
||||||
|
: IsMagicWallet && MagicCeilingFull
|
||||||
|
? "شارژ عادی بدون ضریب (سقف جادویی پر است)"
|
||||||
|
: "شارژ حساب اصلی";
|
||||||
|
|
||||||
|
private void Cancel() => MudDialog.Cancel();
|
||||||
|
|
||||||
|
private void GoToCharge()
|
||||||
|
{
|
||||||
|
if (IsMagicWallet && !MagicCeilingFull)
|
||||||
|
{
|
||||||
|
MudDialog.Cancel();
|
||||||
|
Navigation.NavigateTo(RouteConstants.Profile.MagicWallet);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MudDialog.Close(DialogResult.Ok(ShortfallAmount));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GoToPackages()
|
||||||
|
{
|
||||||
|
MudDialog.Cancel();
|
||||||
|
Navigation.NavigateTo(RouteConstants.Package.List);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GoToClubMembership()
|
||||||
|
{
|
||||||
|
MudDialog.Cancel();
|
||||||
|
Navigation.NavigateTo(RouteConstants.Club.Membership);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
|
||||||
|
}
|
||||||
@@ -36,6 +36,14 @@
|
|||||||
Color="Color.Primary"
|
Color="Color.Primary"
|
||||||
Class="mb-2" />
|
Class="mb-2" />
|
||||||
|
|
||||||
|
<MudTextField Value="ReferralCode"
|
||||||
|
ValueChanged="@((string? v) => ReferralCodeChanged.InvokeAsync(v))"
|
||||||
|
Label="کد معرف (اختیاری)"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Immediate="true"
|
||||||
|
Placeholder="کد معرف را وارد کنید"
|
||||||
|
Class="mb-3" />
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
|
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
|
||||||
{
|
{
|
||||||
<MudAlert Severity="Severity.Error" Dense="true" Elevation="0"
|
<MudAlert Severity="Severity.Error" Dense="true" Elevation="0"
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ public partial class PhoneVerifyForm
|
|||||||
[Parameter] public string? PhoneNumber { get; set; }
|
[Parameter] public string? PhoneNumber { get; set; }
|
||||||
[Parameter] public int ResendRemaining { get; set; }
|
[Parameter] public int ResendRemaining { get; set; }
|
||||||
|
|
||||||
|
// ── Referral ──
|
||||||
|
[Parameter] public string? ReferralCode { get; set; }
|
||||||
|
[Parameter] public EventCallback<string?> ReferralCodeChanged { get; set; }
|
||||||
|
|
||||||
// ── Verify actions ──
|
// ── Verify actions ──
|
||||||
[Parameter] public EventCallback OnChangePhone { get; set; }
|
[Parameter] public EventCallback OnChangePhone { get; set; }
|
||||||
[Parameter] public EventCallback OnResendOtp { get; set; }
|
[Parameter] public EventCallback OnResendOtp { get; set; }
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ public class AuthService
|
|||||||
var isSignMainContractStr = claims.FirstOrDefault(c => c.key == "IsSignMainContract").value;
|
var isSignMainContractStr = claims.FirstOrDefault(c => c.key == "IsSignMainContract").value;
|
||||||
var hasPurchasedPackageStr = claims.FirstOrDefault(c => c.key == "HasPurchasedPackage").value;
|
var hasPurchasedPackageStr = claims.FirstOrDefault(c => c.key == "HasPurchasedPackage").value;
|
||||||
var isClubMemberActiveStr = claims.FirstOrDefault(c => c.key == "IsClubMemberActive").value;
|
var isClubMemberActiveStr = claims.FirstOrDefault(c => c.key == "IsClubMemberActive").value;
|
||||||
|
var hasAddressStr = claims.FirstOrDefault(c => c.key == "HasAddress").value;
|
||||||
|
|
||||||
_userAuthInfo.UserId = long.TryParse(userIdStr, out var userId) ? userId : 0;
|
_userAuthInfo.UserId = long.TryParse(userIdStr, out var userId) ? userId : 0;
|
||||||
_userAuthInfo.FirstName = firstName ?? string.Empty;
|
_userAuthInfo.FirstName = firstName ?? string.Empty;
|
||||||
@@ -88,6 +89,7 @@ public class AuthService
|
|||||||
_userAuthInfo.IsSignMainContract = bool.TryParse(isSignMainContractStr, out var isSignMainContract) && isSignMainContract;
|
_userAuthInfo.IsSignMainContract = bool.TryParse(isSignMainContractStr, out var isSignMainContract) && isSignMainContract;
|
||||||
_userAuthInfo.HasPurchasedPackage = bool.TryParse(hasPurchasedPackageStr, out var hasPurchased) && hasPurchased;
|
_userAuthInfo.HasPurchasedPackage = bool.TryParse(hasPurchasedPackageStr, out var hasPurchased) && hasPurchased;
|
||||||
_userAuthInfo.IsClubMemberActive = bool.TryParse(isClubMemberActiveStr, out var isClubActive) && isClubActive;
|
_userAuthInfo.IsClubMemberActive = bool.TryParse(isClubMemberActiveStr, out var isClubActive) && isClubActive;
|
||||||
|
_userAuthInfo.HasAddress = bool.TryParse(hasAddressStr, out var hasAddress) && hasAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<(string key, string value)>?> GetTokenClaimsAsync()
|
private async Task<List<(string key, string value)>?> GetTokenClaimsAsync()
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using Grpc.Core;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
|
namespace FrontOffice.Main.Utilities;
|
||||||
|
|
||||||
|
public static class CreditChargeNavigation
|
||||||
|
{
|
||||||
|
public const long MinChargeAmount = 10_000;
|
||||||
|
public const string ReturnUrlStorageKey = "credit_charge_return_url";
|
||||||
|
private const string InsufficientBalanceMarker = "موجودی کیف پول کافی نیست";
|
||||||
|
|
||||||
|
public static long NormalizeChargeAmount(long shortfall) =>
|
||||||
|
Math.Max(shortfall, MinChargeAmount);
|
||||||
|
|
||||||
|
public static string BuildChargeUrl(long amount, string? returnUrl)
|
||||||
|
{
|
||||||
|
var url = $"{RouteConstants.Profile.ChargeCreditWallet}?amount={amount}";
|
||||||
|
if (IsValidReturnUrl(returnUrl))
|
||||||
|
url += $"&returnUrl={Uri.EscapeDataString(returnUrl!)}";
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsValidReturnUrl(string? url) =>
|
||||||
|
!string.IsNullOrWhiteSpace(url)
|
||||||
|
&& url.StartsWith('/')
|
||||||
|
&& !url.StartsWith("//", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
public static bool IsInsufficientWalletBalanceMessage(string? message) =>
|
||||||
|
message?.Contains(InsufficientBalanceMarker, StringComparison.Ordinal) == true;
|
||||||
|
|
||||||
|
public static bool IsInsufficientWalletBalance(Exception ex) =>
|
||||||
|
ex switch
|
||||||
|
{
|
||||||
|
RpcException rpc => IsInsufficientWalletBalanceMessage(rpc.Status.Detail)
|
||||||
|
|| IsInsufficientWalletBalanceMessage(rpc.Message),
|
||||||
|
_ => IsInsufficientWalletBalanceMessage(ex.Message)
|
||||||
|
};
|
||||||
|
|
||||||
|
public static async Task SaveReturnUrlAsync(IJSRuntime js, string returnUrl)
|
||||||
|
{
|
||||||
|
if (!IsValidReturnUrl(returnUrl)) return;
|
||||||
|
await js.InvokeVoidAsync("sessionStorage.setItem", ReturnUrlStorageKey, returnUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<string?> GetReturnUrlAsync(IJSRuntime js) =>
|
||||||
|
await js.InvokeAsync<string?>("sessionStorage.getItem", ReturnUrlStorageKey);
|
||||||
|
|
||||||
|
public static async Task ClearReturnUrlAsync(IJSRuntime js) =>
|
||||||
|
await js.InvokeVoidAsync("sessionStorage.removeItem", ReturnUrlStorageKey);
|
||||||
|
}
|
||||||
@@ -219,6 +219,8 @@ public class DiscountOrderService
|
|||||||
2 => "ارسال شده",
|
2 => "ارسال شده",
|
||||||
3 => "تحویل داده شده",
|
3 => "تحویل داده شده",
|
||||||
4 => "لغو شده",
|
4 => "لغو شده",
|
||||||
|
5 => "آماده تحویل در دفتر",
|
||||||
|
6 => "مرجوع شده",
|
||||||
_ => "نامشخص"
|
_ => "نامشخص"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -228,7 +230,9 @@ public class DiscountOrderService
|
|||||||
1 => Color.Info,
|
1 => Color.Info,
|
||||||
2 => Color.Primary,
|
2 => Color.Primary,
|
||||||
3 => Color.Success,
|
3 => Color.Success,
|
||||||
4 => Color.Error,
|
4 => Color.Dark,
|
||||||
|
5 => Color.Primary,
|
||||||
|
6 => Color.Error,
|
||||||
_ => Color.Default
|
_ => Color.Default
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ public class DiscountProductService
|
|||||||
public async Task<DiscountProductListResult> GetProductsAsync(
|
public async Task<DiscountProductListResult> GetProductsAsync(
|
||||||
int page = 1, int pageSize = 12,
|
int page = 1, int pageSize = 12,
|
||||||
string? search = null, long? categoryId = null,
|
string? search = null, long? categoryId = null,
|
||||||
bool? inStock = null)
|
bool? inStock = null, string? sortBy = null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -97,6 +97,8 @@ public class DiscountProductService
|
|||||||
request.CategoryId = categoryId.Value;
|
request.CategoryId = categoryId.Value;
|
||||||
if (inStock.HasValue)
|
if (inStock.HasValue)
|
||||||
request.InStock = inStock.Value;
|
request.InStock = inStock.Value;
|
||||||
|
if (!string.IsNullOrWhiteSpace(sortBy))
|
||||||
|
request.SortBy = sortBy;
|
||||||
|
|
||||||
var response = await _productClient.GetDiscountProductsAsync(request);
|
var response = await _productClient.GetDiscountProductsAsync(request);
|
||||||
|
|
||||||
@@ -125,6 +127,9 @@ public class DiscountProductService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<DiscountProductListResult> GetTopSellingAsync(int count = 6, bool? inStock = null)
|
||||||
|
=> GetProductsAsync(page: 1, pageSize: count, sortBy: "SaleCount desc", inStock: inStock);
|
||||||
|
|
||||||
public async Task<DiscountProductDetail?> GetByIdAsync(long productId)
|
public async Task<DiscountProductDetail?> GetByIdAsync(long productId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
namespace FrontOffice.Main.Utilities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// هر action ای که نیاز به لاگین دارد را از طریق این سرویس اجرا کنید.
|
||||||
|
/// اگر کاربر لاگین نباشد، مودال ورود نشان داده میشود.
|
||||||
|
/// پس از ورود موفق، action به صورت خودکار اجرا میشود.
|
||||||
|
/// </summary>
|
||||||
|
public class GuestActionGate
|
||||||
|
{
|
||||||
|
private readonly AuthService _authService;
|
||||||
|
private readonly AuthDialogService _authDialogService;
|
||||||
|
|
||||||
|
public GuestActionGate(AuthService authService, AuthDialogService authDialogService)
|
||||||
|
{
|
||||||
|
_authService = authService;
|
||||||
|
_authDialogService = authDialogService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// اگر کاربر لاگین باشد action را اجرا میکند.
|
||||||
|
/// در غیر این صورت مودال لاگین را باز کرده و پس از ورود موفق، action را اجرا میکند.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>true اگر action اجرا شد، false اگر کاربر لاگین نکرد.</returns>
|
||||||
|
public async Task<bool> RunAsync(Func<Task> action)
|
||||||
|
{
|
||||||
|
if (await _authService.IsAuthenticatedAsync())
|
||||||
|
{
|
||||||
|
await action();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _authDialogService.ShowAuthDialogAsync();
|
||||||
|
|
||||||
|
if (await _authService.IsAuthenticatedAsync())
|
||||||
|
{
|
||||||
|
await action();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,11 @@ public class NetworkNodeDto
|
|||||||
/// کد معرف کاربر
|
/// کد معرف کاربر
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ReferralCode { get; set; }
|
public string? ReferralCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نام پکیج موثر (مثلاً پکیج طلایی / نقرهای)
|
||||||
|
/// </summary>
|
||||||
|
public string? PackageName { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -43,6 +48,11 @@ public class FlatNetworkNodeDto
|
|||||||
/// کد معرف کاربر - فقط برای کاربران فعال در باشگاه نمایش داده شود
|
/// کد معرف کاربر - فقط برای کاربران فعال در باشگاه نمایش داده شود
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ReferralCode { get; set; }
|
public string? ReferralCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نام پکیج موثر (مثلاً پکیج طلایی / نقرهای)
|
||||||
|
/// </summary>
|
||||||
|
public string? PackageName { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -81,7 +91,8 @@ public class NetworkTreeDto
|
|||||||
IsClubActive = node.IsClubActive,
|
IsClubActive = node.IsClubActive,
|
||||||
ActivationWeekNumber = node.ActivationWeekNumber,
|
ActivationWeekNumber = node.ActivationWeekNumber,
|
||||||
JoinedAt = node.JoinedAt,
|
JoinedAt = node.JoinedAt,
|
||||||
ReferralCode = node.ReferralCode
|
ReferralCode = node.ReferralCode,
|
||||||
|
PackageName = node.PackageName
|
||||||
};
|
};
|
||||||
|
|
||||||
result.Add(flatNode);
|
result.Add(flatNode);
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ public class NetworkMembershipService
|
|||||||
IsActive = node.IsActive,
|
IsActive = node.IsActive,
|
||||||
IsClubActive = node.IsClubActive,
|
IsClubActive = node.IsClubActive,
|
||||||
ReferralCode = node.ReferralCode,
|
ReferralCode = node.ReferralCode,
|
||||||
|
PackageName = string.IsNullOrWhiteSpace(node.PackageName) ? null : node.PackageName,
|
||||||
ActivationWeekNumber = node.ActivationWeekDefinitionId?.ToString(),
|
ActivationWeekNumber = node.ActivationWeekDefinitionId?.ToString(),
|
||||||
JoinedAt = node.JoinedAt?.ToDateTime(),
|
JoinedAt = node.JoinedAt?.ToDateTime(),
|
||||||
LeftChild = MapNodeFromProto(node.LeftChild),
|
LeftChild = MapNodeFromProto(node.LeftChild),
|
||||||
|
|||||||
@@ -71,9 +71,12 @@ public class ProductService
|
|||||||
return result.Products;
|
return result.Products;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<ProductListResult> GetTopSellingAsync(int count = 6, bool? inStock = null)
|
||||||
|
=> GetProductsPagedAsync(sortBy: "SaleCount desc", page: 1, pageSize: count, inStock: inStock);
|
||||||
|
|
||||||
public async Task<ProductListResult> GetProductsPagedAsync(
|
public async Task<ProductListResult> GetProductsPagedAsync(
|
||||||
string? query = null, long? categoryId = null, string? sortBy = null,
|
string? query = null, long? categoryId = null, string? sortBy = null,
|
||||||
int page = 1, int pageSize = 12)
|
int page = 1, int pageSize = 12, bool? inStock = null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -84,20 +87,25 @@ public class ProductService
|
|||||||
PageNumber = page,
|
PageNumber = page,
|
||||||
PageSize = pageSize
|
PageSize = pageSize
|
||||||
},
|
},
|
||||||
Filter = new GetAllProductsByFilterFilter
|
Filter = new GetAllProductsByFilterFilter()
|
||||||
{
|
|
||||||
Title = query ?? string.Empty,
|
|
||||||
Description = query ?? string.Empty,
|
|
||||||
ShortInfomation = query ?? string.Empty,
|
|
||||||
FullInformation = query ?? string.Empty
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// فقط Title — CMS فیلترها را AND میزند؛ ارسال query در همه فیلدها جستجو را میشکند
|
||||||
|
if (!string.IsNullOrWhiteSpace(query))
|
||||||
|
{
|
||||||
|
request.Filter.Title = query.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
if (categoryId is { } value)
|
if (categoryId is { } value)
|
||||||
{
|
{
|
||||||
request.Filter.CategoryId = value;
|
request.Filter.CategoryId = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (inStock.HasValue)
|
||||||
|
{
|
||||||
|
request.Filter.InStock = inStock.Value;
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(sortBy))
|
if (!string.IsNullOrEmpty(sortBy))
|
||||||
{
|
{
|
||||||
request.SortBy = sortBy;
|
request.SortBy = sortBy;
|
||||||
@@ -129,30 +137,66 @@ public class ProductService
|
|||||||
|
|
||||||
public async Task<Product?> GetByIdAsync(long id)
|
public async Task<Product?> GetByIdAsync(long id)
|
||||||
{
|
{
|
||||||
if (TryGetCachedProduct(id, out var cached) && HasDetailedData(cached))
|
if (id <= 0)
|
||||||
{
|
return null;
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
TryGetCachedProduct(id, out var cached);
|
||||||
|
|
||||||
|
if (cached is not null && HasDetailedData(cached))
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
// جزئیات کامل (گالری + دستهبندی)
|
||||||
|
var detailed = await TryFetchDetailAsync(id);
|
||||||
|
if (detailed is not null)
|
||||||
|
return detailed;
|
||||||
|
|
||||||
|
// fallback: همان API لیست محصولات — ناموجودها را هم برمیگرداند
|
||||||
|
var fromFilter = await TryFetchByFilterIdAsync(id);
|
||||||
|
if (fromFilter is not null)
|
||||||
|
return fromFilter;
|
||||||
|
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Product?> TryFetchDetailAsync(long id)
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var resp = await _client.GetProductsAsync(new GetProductsRequest { Id = id });
|
var resp = await _client.GetProductsAsync(new GetProductsRequest { Id = id });
|
||||||
if (resp == null)
|
if (resp is null || resp.Id <= 0)
|
||||||
{
|
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
return MapAndCache(resp);
|
return MapAndCache(resp);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
if (cached is not null)
|
return null;
|
||||||
{
|
}
|
||||||
return cached;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
TryGetCachedProduct(id, out var result);
|
private async Task<Product?> TryFetchByFilterIdAsync(long id)
|
||||||
return result;
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var resp = await _client.GetAllProductsByFilterAsync(new GetAllProductsByFilterRequest
|
||||||
|
{
|
||||||
|
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
|
||||||
|
{
|
||||||
|
PageNumber = 1,
|
||||||
|
PageSize = 1
|
||||||
|
},
|
||||||
|
Filter = new GetAllProductsByFilterFilter { Id = id }
|
||||||
|
});
|
||||||
|
|
||||||
|
var model = resp.Models.FirstOrDefault(m => m.Id == id);
|
||||||
|
if (model is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return MapAndCache(resp.Models).FirstOrDefault(p => p.Id == id);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public static class RouteConstants
|
|||||||
public const string Wallet = "/profile/wallet";
|
public const string Wallet = "/profile/wallet";
|
||||||
public const string MagicWallet = "/profile/magic-wallet";
|
public const string MagicWallet = "/profile/magic-wallet";
|
||||||
public const string ChargeDiscountWallet = "/profile/charge-discount-wallet";
|
public const string ChargeDiscountWallet = "/profile/charge-discount-wallet";
|
||||||
|
public const string ChargeCreditWallet = "/profile/charge-credit-wallet";
|
||||||
public const string WithdrawalRequests = "/profile/withdrawal-requests";
|
public const string WithdrawalRequests = "/profile/withdrawal-requests";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace FrontOffice.Main.Utilities.Seo;
|
||||||
|
|
||||||
|
public class SeoMetadataProvider
|
||||||
|
{
|
||||||
|
private readonly BlogPostService _blogPostService;
|
||||||
|
private readonly SeoSettings _settings;
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, (string Title, string Description)> StaticPages = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["/"] = (
|
||||||
|
"کارابازار سلامت | باشگاه مشتریان KBS",
|
||||||
|
"باشگاه مشتریان کارابازار سلامت — رشد تیم، فروش واقعی و پاداش شفاف. فروشگاه محصولات سلامت، کیف پول و ثبتنام رایگان."),
|
||||||
|
[RouteConstants.About.Index] = (
|
||||||
|
"درباره ما | کارابازار سلامت",
|
||||||
|
"آشنایی با کارابازار سلامت، باشگاه مشتریان KBS و خدمات فروشگاه محصولات سلامت."),
|
||||||
|
[RouteConstants.FAQ.Index] = (
|
||||||
|
"سوالات متداول | کارابازار سلامت",
|
||||||
|
"پاسخ سوالات متداول درباره باشگاه مشتریان، کیف پول، پاداشها و خرید از کارابازار سلامت."),
|
||||||
|
[RouteConstants.Contact.Index] = (
|
||||||
|
"ارتباط با ما | کارابازار سلامت",
|
||||||
|
"تماس با تیم پشتیبانی کارابازار سلامت — سوالات، پیشنهادات و پشتیبانی کاربران."),
|
||||||
|
[RouteConstants.Licenses.Index] = (
|
||||||
|
"مجوزها و گواهینامهها | کارابازار سلامت",
|
||||||
|
"مجوزها، گواهینامهها و مدارک رسمی کارابازار سلامت."),
|
||||||
|
[RouteConstants.Blog.Index] = (
|
||||||
|
"بلاگ | کارابازار سلامت",
|
||||||
|
"آخرین مقالات و اخبار باشگاه مشتریان کارابازار سلامت."),
|
||||||
|
[RouteConstants.Store.Products] = (
|
||||||
|
"محصولات | کارابازار سلامت",
|
||||||
|
"فروشگاه محصولات سلامت کارابازار — خرید آنلاین با پاداش باشگاه مشتریان."),
|
||||||
|
[RouteConstants.Store.Categories] = (
|
||||||
|
"دستهبندیها | کارابازار سلامت",
|
||||||
|
"دستهبندی محصولات فروشگاه کارابازار سلامت."),
|
||||||
|
[RouteConstants.Package.List] = (
|
||||||
|
"پکیجها | کارابازار سلامت",
|
||||||
|
"پکیجهای عضویت باشگاه مشتریان کارابازار سلامت."),
|
||||||
|
[RouteConstants.Club.Membership] = (
|
||||||
|
"عضویت باشگاه مشتریان | کارابازار سلامت",
|
||||||
|
"عضویت در باشگاه مشتریان KBS و بهرهمندی از مزایا و پاداشها."),
|
||||||
|
[RouteConstants.Club.Features] = (
|
||||||
|
"ویژگیهای باشگاه مشتریان | کارابازار سلامت",
|
||||||
|
"امکانات و ویژگیهای باشگاه مشتریان کارابازار سلامت."),
|
||||||
|
[RouteConstants.DiscountStore.Products] = (
|
||||||
|
"فروشگاه اعتباری | کارابازار سلامت",
|
||||||
|
"خرید از فروشگاه اعتباری کارابازار سلامت با کیف پول تخفیفی."),
|
||||||
|
[RouteConstants.Registration.Wizard] = (
|
||||||
|
"ثبتنام | کارابازار سلامت",
|
||||||
|
"ثبتنام سریع در باشگاه مشتریان کارابازار سلامت."),
|
||||||
|
[RouteConstants.Gateway.StoreChooser] = (
|
||||||
|
"فروشگاهها | کارابازار سلامت",
|
||||||
|
"انتخاب فروشگاه اصلی یا اعتباری در کارابازار سلامت."),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly HashSet<string> NoIndexPaths = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"/wishlist",
|
||||||
|
"/wp-admin",
|
||||||
|
"/wp-login.php",
|
||||||
|
"/feed",
|
||||||
|
"/xmlrpc.php",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly HashSet<string> NoIndexPrefixes = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
RouteConstants.Profile.Index,
|
||||||
|
"/profile/",
|
||||||
|
RouteConstants.Checkout.Index,
|
||||||
|
RouteConstants.Store.Cart,
|
||||||
|
RouteConstants.Store.CheckoutSummary,
|
||||||
|
RouteConstants.Store.Orders,
|
||||||
|
"/order/",
|
||||||
|
RouteConstants.DiscountStore.Cart,
|
||||||
|
RouteConstants.DiscountStore.Checkout,
|
||||||
|
RouteConstants.DiscountStore.Orders,
|
||||||
|
"/discount-store/order/",
|
||||||
|
RouteConstants.Gateway.OrdersChooser,
|
||||||
|
RouteConstants.Gateway.CartChooser,
|
||||||
|
RouteConstants.Commission.Dashboard,
|
||||||
|
"/commission/",
|
||||||
|
RouteConstants.Network.Statistics,
|
||||||
|
"/network/",
|
||||||
|
RouteConstants.Package.MyPackages,
|
||||||
|
"/wp-content/",
|
||||||
|
"/wp-includes/",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Regex ProductDetailPattern = new(@"^/product/\d+$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
private static readonly Regex DiscountProductPattern = new(@"^/discount-store/product/\d+$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
private static readonly Regex PackageDetailPattern = new(@"^/package/\d+$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
private static readonly Regex CheckoutPackagePattern = new(@"^/checkout/\d+$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
|
||||||
|
public SeoMetadataProvider(BlogPostService blogPostService, IOptions<SeoSettings> settings)
|
||||||
|
{
|
||||||
|
_blogPostService = blogPostService;
|
||||||
|
_settings = settings.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SeoPageMetadata> GetForPathAsync(string path, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var normalizedPath = NormalizePath(path);
|
||||||
|
var baseUrl = _settings.SiteUrl.TrimEnd('/');
|
||||||
|
|
||||||
|
if (IsNoIndexPath(normalizedPath))
|
||||||
|
{
|
||||||
|
return new SeoPageMetadata
|
||||||
|
{
|
||||||
|
Title = _settings.SiteName,
|
||||||
|
Description = _settings.DefaultDescription,
|
||||||
|
CanonicalUrl = $"{baseUrl}{normalizedPath}",
|
||||||
|
NoIndex = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedPath.StartsWith(RouteConstants.Blog.Post, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& normalizedPath.Length > RouteConstants.Blog.Post.Length)
|
||||||
|
{
|
||||||
|
var slug = normalizedPath[RouteConstants.Blog.Post.Length..].Trim('/');
|
||||||
|
if (!string.IsNullOrWhiteSpace(slug))
|
||||||
|
{
|
||||||
|
var post = await _blogPostService.GetBySlugAsync(slug);
|
||||||
|
if (post != null)
|
||||||
|
{
|
||||||
|
var description = !string.IsNullOrWhiteSpace(post.Summary)
|
||||||
|
? post.Summary
|
||||||
|
: $"مقاله {post.Title} — بلاگ کارابازار سلامت";
|
||||||
|
|
||||||
|
return new SeoPageMetadata
|
||||||
|
{
|
||||||
|
Title = $"{post.Title} | بلاگ کارابازار سلامت",
|
||||||
|
Description = description,
|
||||||
|
CanonicalUrl = $"{baseUrl}{RouteConstants.Blog.Post}{post.Slug}",
|
||||||
|
CrawlableContent = $"{post.Title}. {description}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StaticPages.TryGetValue(normalizedPath, out var page))
|
||||||
|
{
|
||||||
|
return new SeoPageMetadata
|
||||||
|
{
|
||||||
|
Title = page.Title,
|
||||||
|
Description = page.Description,
|
||||||
|
CanonicalUrl = $"{baseUrl}{normalizedPath}",
|
||||||
|
CrawlableContent = normalizedPath == "/" ? GetHomeCrawlableContent() : null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsIndexableDynamicRoute(normalizedPath))
|
||||||
|
{
|
||||||
|
return new SeoPageMetadata
|
||||||
|
{
|
||||||
|
Title = _settings.SiteName,
|
||||||
|
Description = _settings.DefaultDescription,
|
||||||
|
CanonicalUrl = $"{baseUrl}{normalizedPath}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SeoPageMetadata
|
||||||
|
{
|
||||||
|
Title = _settings.SiteName,
|
||||||
|
Description = _settings.DefaultDescription,
|
||||||
|
CanonicalUrl = $"{baseUrl}{normalizedPath}",
|
||||||
|
NoIndex = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsIndexableDynamicRoute(string path) =>
|
||||||
|
ProductDetailPattern.IsMatch(path)
|
||||||
|
|| DiscountProductPattern.IsMatch(path)
|
||||||
|
|| PackageDetailPattern.IsMatch(path)
|
||||||
|
|| CheckoutPackagePattern.IsMatch(path);
|
||||||
|
|
||||||
|
private static string NormalizePath(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
return "/";
|
||||||
|
|
||||||
|
var normalized = path.Split('?', '#')[0].Trim();
|
||||||
|
if (!normalized.StartsWith('/'))
|
||||||
|
normalized = "/" + normalized;
|
||||||
|
|
||||||
|
if (normalized.Length > 1 && normalized.EndsWith('/'))
|
||||||
|
normalized = normalized.TrimEnd('/');
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsNoIndexPath(string path)
|
||||||
|
{
|
||||||
|
if (NoIndexPaths.Contains(path))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
foreach (var prefix in NoIndexPrefixes)
|
||||||
|
{
|
||||||
|
if (path.Equals(prefix, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetHomeCrawlableContent() =>
|
||||||
|
"کارابازار سلامت (KBS) — باشگاه مشتریان و فروشگاه محصولات سلامت. " +
|
||||||
|
"با عضویت در باشگاه مشتریان KBS از پاداش شفاف، کیف پول جادویی، فروشگاه اعتباری و شبکه فروش بهرهمند شوید. " +
|
||||||
|
"ثبتنام رایگان در kbs2.ir.";
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace FrontOffice.Main.Utilities.Seo;
|
||||||
|
|
||||||
|
public sealed class SeoPageMetadata
|
||||||
|
{
|
||||||
|
public required string Title { get; init; }
|
||||||
|
public required string Description { get; init; }
|
||||||
|
public required string CanonicalUrl { get; init; }
|
||||||
|
public string? CrawlableContent { get; init; }
|
||||||
|
public bool NoIndex { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace FrontOffice.Main.Utilities.Seo;
|
||||||
|
|
||||||
|
public class SeoSettings
|
||||||
|
{
|
||||||
|
public const string SectionName = "Seo";
|
||||||
|
|
||||||
|
public string SiteUrl { get; set; } = "https://kbs2.ir";
|
||||||
|
public string SiteName { get; set; } = "کارابازار سلامت";
|
||||||
|
public string DefaultDescription { get; set; } =
|
||||||
|
"باشگاه مشتریان کارابازار سلامت (KBS) — فروشگاه محصولات سلامت، پاداش شفاف، کیف پول و شبکه فروش.";
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Xml;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace FrontOffice.Main.Utilities.Seo;
|
||||||
|
|
||||||
|
public class SitemapGenerator
|
||||||
|
{
|
||||||
|
private readonly BlogPostService _blogPostService;
|
||||||
|
private readonly SeoSettings _settings;
|
||||||
|
|
||||||
|
private static readonly string[] StaticPaths =
|
||||||
|
[
|
||||||
|
RouteConstants.Main.MainPage,
|
||||||
|
RouteConstants.About.Index,
|
||||||
|
RouteConstants.FAQ.Index,
|
||||||
|
RouteConstants.Contact.Index,
|
||||||
|
RouteConstants.Licenses.Index,
|
||||||
|
RouteConstants.Blog.Index,
|
||||||
|
RouteConstants.Store.Products,
|
||||||
|
RouteConstants.Package.List,
|
||||||
|
RouteConstants.Club.Membership,
|
||||||
|
RouteConstants.Club.Features,
|
||||||
|
RouteConstants.DiscountStore.Products,
|
||||||
|
RouteConstants.Registration.Wizard,
|
||||||
|
];
|
||||||
|
|
||||||
|
public SitemapGenerator(BlogPostService blogPostService, IOptions<SeoSettings> settings)
|
||||||
|
{
|
||||||
|
_blogPostService = blogPostService;
|
||||||
|
_settings = settings.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GenerateAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var baseUrl = _settings.SiteUrl.TrimEnd('/');
|
||||||
|
var ns = XNamespace.Get("http://www.sitemaps.org/schemas/sitemap/0.9");
|
||||||
|
var urls = new List<XElement>();
|
||||||
|
|
||||||
|
foreach (var path in StaticPaths)
|
||||||
|
{
|
||||||
|
urls.Add(CreateUrlElement(ns, $"{baseUrl}{path}", priority: path == "/" ? "1.0" : "0.8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var page = 1;
|
||||||
|
const int pageSize = 100;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var result = await _blogPostService.GetPublishedPostsAsync(page: page, pageSize: pageSize);
|
||||||
|
if (result.Posts.Count == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
foreach (var post in result.Posts)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(post.Slug))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var lastMod = post.PublishedAt?.ToString("yyyy-MM-dd") ?? DateTime.UtcNow.ToString("yyyy-MM-dd");
|
||||||
|
urls.Add(CreateUrlElement(
|
||||||
|
ns,
|
||||||
|
$"{baseUrl}{RouteConstants.Blog.Post}{post.Slug}",
|
||||||
|
lastMod: lastMod,
|
||||||
|
priority: "0.6"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page >= result.TotalPages)
|
||||||
|
break;
|
||||||
|
|
||||||
|
page++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var document = new XDocument(
|
||||||
|
new XDeclaration("1.0", "utf-8", null),
|
||||||
|
new XElement(ns + "urlset", urls));
|
||||||
|
|
||||||
|
var settings = new XmlWriterSettings
|
||||||
|
{
|
||||||
|
Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false),
|
||||||
|
Indent = true,
|
||||||
|
OmitXmlDeclaration = false
|
||||||
|
};
|
||||||
|
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
using (var writer = XmlWriter.Create(stream, settings))
|
||||||
|
{
|
||||||
|
document.Save(writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetString(stream.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<byte[]> GenerateBytesAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var xml = await GenerateAsync(cancellationToken);
|
||||||
|
return Encoding.UTF8.GetBytes(xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static XElement CreateUrlElement(
|
||||||
|
XNamespace ns,
|
||||||
|
string loc,
|
||||||
|
string? lastMod = null,
|
||||||
|
string changefreq = "weekly",
|
||||||
|
string priority = "0.5")
|
||||||
|
{
|
||||||
|
var element = new XElement(ns + "url",
|
||||||
|
new XElement(ns + "loc", loc),
|
||||||
|
new XElement(ns + "changefreq", changefreq),
|
||||||
|
new XElement(ns + "priority", priority));
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(lastMod))
|
||||||
|
element.Add(new XElement(ns + "lastmod", lastMod));
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
namespace FrontOffice.Main.Utilities;
|
||||||
|
|
||||||
|
public enum ProductSortOption
|
||||||
|
{
|
||||||
|
PriceDesc,
|
||||||
|
PriceAsc,
|
||||||
|
Newest,
|
||||||
|
Title
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// وضعیت لیست فروشگاه در query string — فقط برای /products و /discount-store.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ShopListQueryState
|
||||||
|
{
|
||||||
|
public string Query { get; init; } = string.Empty;
|
||||||
|
public string? Sort { get; init; }
|
||||||
|
public long? CategoryId { get; init; }
|
||||||
|
public int Pages { get; init; } = 1;
|
||||||
|
|
||||||
|
public static ShopListQueryState Parse(Uri uri)
|
||||||
|
{
|
||||||
|
var q = QueryHelpers.ParseQuery(uri.Query);
|
||||||
|
return new ShopListQueryState
|
||||||
|
{
|
||||||
|
Query = GetString(q, "q") ?? string.Empty,
|
||||||
|
Sort = GetString(q, "sort"),
|
||||||
|
CategoryId = GetLong(q, "category"),
|
||||||
|
Pages = Math.Max(1, GetInt(q, "pages") ?? 1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToRelativeUrl(string path)
|
||||||
|
{
|
||||||
|
var dict = new Dictionary<string, string?>();
|
||||||
|
if (!string.IsNullOrWhiteSpace(Query))
|
||||||
|
dict["q"] = Query.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(Sort) && !string.Equals(Sort, "price-desc", StringComparison.OrdinalIgnoreCase))
|
||||||
|
dict["sort"] = Sort;
|
||||||
|
if (CategoryId is > 0)
|
||||||
|
dict["category"] = CategoryId.Value.ToString();
|
||||||
|
if (Pages > 1)
|
||||||
|
dict["pages"] = Pages.ToString();
|
||||||
|
|
||||||
|
return dict.Count == 0
|
||||||
|
? path
|
||||||
|
: QueryHelpers.AddQueryString(path, dict!);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Matches(ShopListQueryState other) =>
|
||||||
|
string.Equals(Query?.Trim() ?? "", other.Query?.Trim() ?? "", StringComparison.Ordinal)
|
||||||
|
&& string.Equals(NormalizeSort(Sort), NormalizeSort(other.Sort), StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& CategoryId == other.CategoryId
|
||||||
|
&& Pages == other.Pages;
|
||||||
|
|
||||||
|
public static string NormalizeSort(string? sort) =>
|
||||||
|
string.IsNullOrWhiteSpace(sort) ? "price-desc" : sort.Trim().ToLowerInvariant();
|
||||||
|
|
||||||
|
public static string ToSortKey(ProductSortOption option) => option switch
|
||||||
|
{
|
||||||
|
ProductSortOption.PriceAsc => "price-asc",
|
||||||
|
ProductSortOption.Newest => "newest",
|
||||||
|
ProductSortOption.Title => "title",
|
||||||
|
_ => "price-desc"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static ProductSortOption ParseSortOption(string? sort) => NormalizeSort(sort) switch
|
||||||
|
{
|
||||||
|
"price-asc" => ProductSortOption.PriceAsc,
|
||||||
|
"newest" => ProductSortOption.Newest,
|
||||||
|
"title" => ProductSortOption.Title,
|
||||||
|
_ => ProductSortOption.PriceDesc
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string ToApiSortBy(ProductSortOption option) => option switch
|
||||||
|
{
|
||||||
|
ProductSortOption.PriceAsc => "price asc",
|
||||||
|
ProductSortOption.Newest => "id desc",
|
||||||
|
ProductSortOption.Title => "title asc",
|
||||||
|
_ => "price desc"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string? GetString(Dictionary<string, StringValues> q, string key) =>
|
||||||
|
q.TryGetValue(key, out var v) ? v.FirstOrDefault() : null;
|
||||||
|
|
||||||
|
private static long? GetLong(Dictionary<string, StringValues> q, string key) =>
|
||||||
|
long.TryParse(GetString(q, key), out var n) ? n : null;
|
||||||
|
|
||||||
|
private static int? GetInt(Dictionary<string, StringValues> q, string key) =>
|
||||||
|
int.TryParse(GetString(q, key), out var n) ? n : null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
|
namespace FrontOffice.Main.Utilities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// اسکرول/فوکوس محصول پس از بازگشت از جزئیات — کلیدهای محدود به لیست فروشگاه (نه لندینگ).
|
||||||
|
/// </summary>
|
||||||
|
public static class ShopListScrollRestore
|
||||||
|
{
|
||||||
|
public const string StoreKey = "fo:store:scroll";
|
||||||
|
public const string DiscountKey = "fo:discount:scroll";
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
};
|
||||||
|
|
||||||
|
public static async Task SaveAsync(IJSRuntime js, string storageKey, long productId)
|
||||||
|
{
|
||||||
|
double scrollY = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
scrollY = await js.InvokeAsync<double>("eval", "window.scrollY || window.pageYOffset || 0");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = JsonSerializer.Serialize(new ScrollPayload(productId, scrollY), JsonOptions);
|
||||||
|
await js.InvokeVoidAsync("sessionStorage.setItem", storageKey, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<ScrollPayload?> TakeAsync(IJSRuntime js, string storageKey)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var raw = await js.InvokeAsync<string?>("sessionStorage.getItem", storageKey);
|
||||||
|
await js.InvokeVoidAsync("sessionStorage.removeItem", storageKey);
|
||||||
|
if (string.IsNullOrWhiteSpace(raw))
|
||||||
|
return null;
|
||||||
|
return JsonSerializer.Deserialize<ScrollPayload>(raw, JsonOptions);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task RestoreAsync(IJSRuntime js, ScrollPayload payload)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await js.InvokeVoidAsync("eval",
|
||||||
|
$@"(function(){{
|
||||||
|
var el = document.getElementById('shop-product-{payload.ProductId}');
|
||||||
|
if (el) {{ el.scrollIntoView({{ block: 'center', behavior: 'instant' }}); return; }}
|
||||||
|
window.scrollTo(0, {payload.ScrollY.ToString(System.Globalization.CultureInfo.InvariantCulture)});
|
||||||
|
}})()");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record ScrollPayload(long ProductId, double ScrollY);
|
||||||
|
}
|
||||||
@@ -69,7 +69,7 @@ public class TokenNotificationService : IAsyncDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var gwUrl = _configuration["GwUrl"]?.TrimEnd('/') ?? "https://localhost:5002";
|
var gwUrl = (_configuration["GW_URL"] ?? _configuration["GwUrl"])?.TrimEnd('/') ?? "https://localhost:5002";
|
||||||
var hubPath = _configuration["SignalR:HubPath"] ?? "/hubs/token-relay";
|
var hubPath = _configuration["SignalR:HubPath"] ?? "/hubs/token-relay";
|
||||||
var hubUrl = $"{gwUrl}{hubPath}";
|
var hubUrl = $"{gwUrl}{hubPath}";
|
||||||
|
|
||||||
|
|||||||
@@ -11,4 +11,5 @@ public class UserAuthInfo
|
|||||||
public bool IsSignMainContract { get; set; }
|
public bool IsSignMainContract { get; set; }
|
||||||
public bool HasPurchasedPackage { get; set; }
|
public bool HasPurchasedPackage { get; set; }
|
||||||
public bool IsClubMemberActive { get; set; }
|
public bool IsClubMemberActive { get; set; }
|
||||||
|
public bool HasAddress { get; set; }
|
||||||
}
|
}
|
||||||
@@ -123,7 +123,7 @@ public class WalletService
|
|||||||
parts.Add(model.ChangeValue > 0 ? "شارژ اصلی" : "برداشت اصلی");
|
parts.Add(model.ChangeValue > 0 ? "شارژ اصلی" : "برداشت اصلی");
|
||||||
|
|
||||||
if (model.ChangeNerworkValue != 0)
|
if (model.ChangeNerworkValue != 0)
|
||||||
parts.Add(model.ChangeNerworkValue > 0 ? "دریافت پاداش تیمی" : "برداشت پاداش تیمی");
|
parts.Add(model.ChangeNerworkValue > 0 ? "دریافت پاداش های دریافتی" : "برداشت پاداش های دریافتی");
|
||||||
|
|
||||||
if (model.ChangeDiscountValue != 0)
|
if (model.ChangeDiscountValue != 0)
|
||||||
parts.Add(model.ChangeDiscountValue > 0 ? "شارژ اعتباری" : "خرید اعتباری");
|
parts.Add(model.ChangeDiscountValue > 0 ? "شارژ اعتباری" : "خرید اعتباری");
|
||||||
@@ -266,6 +266,26 @@ public class WalletService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<(bool Success, string? GatewayUrl, string? Error)> InitiateCreditChargeAsync(long amount)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await _client.InitiateCreditChargeAsync(new InitiateCreditChargeRequest
|
||||||
|
{
|
||||||
|
Amount = amount
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.IsSuccess)
|
||||||
|
return (true, response.GatewayUrl, null);
|
||||||
|
|
||||||
|
return (false, null, response.ErrorMessage);
|
||||||
|
}
|
||||||
|
catch (Grpc.Core.RpcException ex)
|
||||||
|
{
|
||||||
|
return (false, null, ex.Status.Detail ?? "خطا در ارتباط با سرور");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============= Wallet Verify Methods =============
|
// ============= Wallet Verify Methods =============
|
||||||
|
|
||||||
public async Task<(bool Success, string Message)> VerifyMagicChargeAsync(string authority, string status)
|
public async Task<(bool Success, string Message)> VerifyMagicChargeAsync(string authority, string status)
|
||||||
@@ -304,6 +324,24 @@ public class WalletService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<(bool Success, string Message)> VerifyCreditChargeAsync(string authority, string status)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await _client.VerifyCreditChargeAsync(new VerifyWalletChargeRequest
|
||||||
|
{
|
||||||
|
Authority = authority,
|
||||||
|
Status = status
|
||||||
|
});
|
||||||
|
|
||||||
|
return (response.Success, response.Message);
|
||||||
|
}
|
||||||
|
catch (Grpc.Core.RpcException ex)
|
||||||
|
{
|
||||||
|
return (false, ex.Status.Detail ?? "خطا در بررسی وضعیت پرداخت");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public int GetWalletMode()
|
public int GetWalletMode()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -14,5 +14,10 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Seo": {
|
||||||
|
"SiteUrl": "https://kbs2.ir",
|
||||||
|
"SiteName": "کارابازار سلامت",
|
||||||
|
"DefaultDescription": "باشگاه مشتریان کارابازار سلامت (KBS) — فروشگاه محصولات سلامت، پاداش شفاف، کیف پول و شبکه فروش."
|
||||||
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
{
|
{
|
||||||
|
"Seo": {
|
||||||
|
"SiteUrl": "https://kbs1.ir",
|
||||||
|
"SiteName": "کارابازار سلامت",
|
||||||
|
"DefaultDescription": "باشگاه مشتریان کارابازار سلامت (KBS) — فروشگاه محصولات سلامت، پاداش شفاف، کیف پول و شبکه فروش."
|
||||||
|
},
|
||||||
"GwUrl": "https://cms.se.kbs1.ir",
|
"GwUrl": "https://cms.se.kbs1.ir",
|
||||||
"DownloadUrl": "",
|
"DownloadUrl": "",
|
||||||
"EncryptionSettings": {
|
"EncryptionSettings": {
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
{
|
{
|
||||||
"GwUrl": "https://localhost:32846",
|
"Seo": {
|
||||||
|
"SiteUrl": "http://localhost:5268",
|
||||||
|
"SiteName": "کارابازار سلامت",
|
||||||
|
"DefaultDescription": "باشگاه مشتریان کارابازار سلامت (KBS) — فروشگاه محصولات سلامت، پاداش شفاف، کیف پول و شبکه فروش."
|
||||||
|
},
|
||||||
|
"GwUrl": "https://cms.se.kbs1.ir",
|
||||||
"DownloadUrl": "",
|
"DownloadUrl": "",
|
||||||
"EncryptionSettings": {
|
"EncryptionSettings": {
|
||||||
"Key": "kmcQ3XTmH4mrdh8VHziuscyf8LLYjG//Kyni81nH/0E=",
|
"Key": "kmcQ3XTmH4mrdh8VHziuscyf8LLYjG//Kyni81nH/0E=",
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIC9jCCAd6gAwIBAgIQAwobM2pDCheGeQdvVZzMPTANBgkqhkiG9w0BAQsFADAV
|
||||||
|
MRMwEQYDVQQDEwpzdGFnaW5nLWNhMB4XDTI2MDQyODE5MjU0NVoXDTM2MDQyNTE5
|
||||||
|
MjU0NVowFTETMBEGA1UEAxMKc3RhZ2luZy1jYTCCASIwDQYJKoZIhvcNAQEBBQAD
|
||||||
|
ggEPADCCAQoCggEBALOOLPxgHegrkI9YXm/0wHKchE5ukb8omv2oDYPp/CjQn8yJ
|
||||||
|
KBpn+8tev8wT1SECACNuA3XhxtjV9dryV5U6lxkmQv2YlLrJOFa3ljcODQpAkKXl
|
||||||
|
Q+PMWic2VyO9/UkW1a4HcPQfGhpgN710evOfBFB4Ora4CUVsADKPLbjJfX8jpqkU
|
||||||
|
LAZiQCrA1kJ087fiKheuXEAWPgcwEE5q0BCs876zIHyST6FaLVNabM5/m0sr7Bky
|
||||||
|
1GQCJlgOLtUeDkaXAv/WJqq/LetcjNy2dWS26PJc7C2byShTfaaWDM8Ki5FG/uMQ
|
||||||
|
/6L54Lx5QKGbl8MzIKmEscXe4GYnmE8MyHQX0aECAwEAAaNCMEAwDgYDVR0PAQH/
|
||||||
|
BAQDAgKkMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFELZSJbeWDRQgj6cTQWY
|
||||||
|
K2sT9ZsLMA0GCSqGSIb3DQEBCwUAA4IBAQCT2eGmHcd1t4K80NobrUzm1d3SB4EW
|
||||||
|
XOLCR+TTYFIYsim0eltwzrOuch+MBTf4+v+4zXWE4e7d1B1lzJIqFIXcgLpDu0i2
|
||||||
|
QeZMrPxY1rkUhqGPwwsoOWBIK/cGot2yg6SZEwj64f7lKUbUB8aWY+xSzct2mwlG
|
||||||
|
i0j6wt0/eRh6hKGdvEiYORffdubC94utkNzr0kFszY4yb3vG0zCVWYiGzkxe0IiF
|
||||||
|
Gc8zN8ZHTY9tk+G5wQuj2WAo7M3DXgFWBbhNgharKulEXTxLXhQksw/nXITa2IwM
|
||||||
|
qt4MWHdGn7/iIyUp/774ySkFk2n6cHi7gQAvolT6OZNCuAOYm8+No7P6
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@@ -124,6 +124,23 @@
|
|||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.org-node-card .package-name-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 14px;
|
||||||
|
padding: 0 6px;
|
||||||
|
margin-top: 2px;
|
||||||
|
border-radius: 7px;
|
||||||
|
font-size: 9px;
|
||||||
|
color: #546e7a;
|
||||||
|
background: #eceff1;
|
||||||
|
border: 1px solid #b0bec5;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Referral Code - only for club active members */
|
/* Referral Code - only for club active members */
|
||||||
.node-referral {
|
.node-referral {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -39,7 +39,31 @@
|
|||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(../fonts/Vazir-Bold.ttf) format('truetype');
|
src: url(../fonts/Vazir-Bold.ttf) format('truetype');
|
||||||
}
|
}
|
||||||
|
/* Roboto - Latin Extended */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300 700;
|
||||||
|
font-stretch: 100%;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('../fonts/roboto/roboto-latin-ext.woff2') format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
|
||||||
|
U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
|
||||||
|
U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Roboto - Latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300 700;
|
||||||
|
font-stretch: 100%;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('../fonts/roboto/roboto-latin.woff2') format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
|
||||||
|
U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
:root {
|
:root {
|
||||||
--app-font-family: 'Vazir', Tahoma, 'Segoe UI', Arial, sans-serif;
|
--app-font-family: 'Vazir', Tahoma, 'Segoe UI', Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -38,7 +38,7 @@ window.OrgChart = {
|
|||||||
.container('#' + containerId)
|
.container('#' + containerId)
|
||||||
.data(data)
|
.data(data)
|
||||||
.nodeWidth((d) => 140)
|
.nodeWidth((d) => 140)
|
||||||
.nodeHeight((d) => 70)
|
.nodeHeight((d) => 85)
|
||||||
.childrenMargin((d) => 50)
|
.childrenMargin((d) => 50)
|
||||||
.compactMarginBetween((d) => 15)
|
.compactMarginBetween((d) => 15)
|
||||||
.compactMarginPair((d) => 15)
|
.compactMarginPair((d) => 15)
|
||||||
@@ -67,6 +67,11 @@ window.OrgChart = {
|
|||||||
// Avatar - first letter of name
|
// Avatar - first letter of name
|
||||||
const firstChar = data.fullName ? data.fullName.charAt(0) : '?';
|
const firstChar = data.fullName ? data.fullName.charAt(0) : '?';
|
||||||
|
|
||||||
|
const packageName = data.packageName || data.PackageName || '';
|
||||||
|
const packageBadge = packageName
|
||||||
|
? `<div class="package-name-badge">${packageName}</div>`
|
||||||
|
: '';
|
||||||
|
|
||||||
// نمایش کد معرف فقط برای کاربران فعال در باشگاه
|
// نمایش کد معرف فقط برای کاربران فعال در باشگاه
|
||||||
const referralCodeHtml = data.isClubActive && data.referralCode
|
const referralCodeHtml = data.isClubActive && data.referralCode
|
||||||
? `<div class="node-referral" onclick="event.stopPropagation(); OrgChart.copyReferralCode('${data.referralCode}');" title="کپی کد معرف">
|
? `<div class="node-referral" onclick="event.stopPropagation(); OrgChart.copyReferralCode('${data.referralCode}');" title="کپی کد معرف">
|
||||||
@@ -82,6 +87,7 @@ window.OrgChart = {
|
|||||||
<div class="node-info">
|
<div class="node-info">
|
||||||
<div class="node-name-sm">${data.fullName || 'بدون نام'}</div>
|
<div class="node-name-sm">${data.fullName || 'بدون نام'}</div>
|
||||||
<div class="node-level-sm">L${data.level || 0}${!isRoot ? ' • ' + (data.position === 'Left' ? 'چپ' : 'راست') : ''}</div>
|
<div class="node-level-sm">L${data.level || 0}${!isRoot ? ' • ' + (data.position === 'Left' ? 'چپ' : 'راست') : ''}</div>
|
||||||
|
${packageBadge}
|
||||||
${referralCodeHtml}
|
${referralCodeHtml}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Disallow: /profile/
|
||||||
|
Disallow: /checkout
|
||||||
|
Disallow: /checkout-summary
|
||||||
|
Disallow: /cart
|
||||||
|
Disallow: /orders
|
||||||
|
Disallow: /order/
|
||||||
|
Disallow: /discount-store/cart
|
||||||
|
Disallow: /discount-store/checkout
|
||||||
|
Disallow: /discount-store/orders
|
||||||
|
Disallow: /discount-store/order/
|
||||||
|
Disallow: /my-orders
|
||||||
|
Disallow: /my-cart
|
||||||
|
Disallow: /wishlist
|
||||||
|
Disallow: /commission/
|
||||||
|
Disallow: /network/
|
||||||
|
Disallow: /my-packages
|
||||||
|
|
||||||
|
Sitemap: https://kbs2.ir/sitemap.xml
|
||||||
Reference in New Issue
Block a user