Compare commits

..

34 Commits

Author SHA1 Message Date
masoodafar-web db35814ce3 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2026-07-03 05:43:14 +03:30
masoodafar-web 7a5a2110c6 feat: add SEO configuration for production environment
Build and Deploy to Production / build-and-deploy (push) Failing after 19m58s
- Introduced SEO settings in appsettings.Production.json, including SiteUrl, SiteName, and DefaultDescription.
- These additions enhance the application's search engine optimization capabilities, improving visibility and user engagement.
2026-07-01 21:48:49 +03:30
masoodafar-web 8dbc64cd86 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2026-07-01 21:45:43 +03:30
masoodafar-web deadd197e6 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2026-07-01 21:31:49 +03:30
masoodafar-web 671c8407e5 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2026-06-30 02:50:03 +03:30
masoodafar-web 1956dcc5f4 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2026-06-30 02:28:13 +03:30
masoodafar-web d59d829d5f Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Failing after 17m21s
2026-06-26 19:30:51 +03:30
masoodafar-web 3c216feb32 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Failing after 15m3s
2026-06-19 02:38:15 +03:30
masoodafar-web 032b8eb448 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 8m14s
2026-06-08 01:55:28 +03:30
masoodafar-web 088ea8dc7f Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 6m36s
2026-05-29 19:59:19 +03:30
masoodafar-web cd92a2f7b0 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 4m24s
2026-05-14 20:12:20 +03:30
masoodafar-web 0c9eb5f9ef Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 4m25s
2026-05-14 02:07:56 +03:30
masoodafar-web 94fbf49fb9 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 4m51s
2026-05-14 01:56:18 +03:30
masoodafar-web 4330a5eaa5 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 4m16s
2026-05-14 01:12:35 +03:30
masoodafar-web 1e5e6bcf3d Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 5m50s
2026-05-04 00:04:13 +03:30
masoodafar-web d751c06a67 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 4m5s
2026-05-01 00:16:24 +03:30
masoodafar-web 1c5aa0ce15 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 14m28s
- Proto version: 0.0.192
- Remove local nupkg source from NuGet.config (breaks Docker builds)
2026-03-15 03:26:17 +03:30
masoodafar-web caaf28b08e fix: update CMS protobuf package version to 0.0.191 and comment out local package reference
Build and Deploy to Production / build-and-deploy (push) Successful in 6m22s
2026-02-28 05:07:54 +03:30
masoodafar-web 1914f703db Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Failing after 2m40s
2026-02-28 04:47:34 +03:30
masoodafar-web a606cd93cd fix: rename wallet labels — اصلی، اعتباری، پاداش تیمی
Build and Deploy to Production / build-and-deploy (push) Successful in 6m19s
- عادی → اصلی (کیف پول اصلی)
- شبکه → پاداش تیمی
- پورسانت → پاداش تیمی
- موجودی کیف پول → موجودی کیف پول اصلی

Files: Wallet.razor, WalletService.cs, MagicWallet.razor,
       Profile/Index.razor, PaymentCallback.razor
2026-02-23 23:26:52 +03:30
masoodafar-web 8b78235ef5 chore: prevent appsettings.Production.json from merging — keep ours strategy
Build and Deploy to Production / build-and-deploy (push) Successful in 6m27s
2026-02-23 00:03:00 +03:30
masoodafar-web f02d082d7b fix: correct GwUrl in Production config to cms.kbs2.ir
Build and Deploy to Production / build-and-deploy (push) Successful in 6m23s
2026-02-22 22:29:17 +03:30
masoodafar-web a11a3765b8 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2026-02-22 22:22:43 +03:30
admin 3cf5ad429d Update src/FrontOffice.Main/Shared/Footer.razor
Build and Deploy to Production / build-and-deploy (push) Successful in 6m39s
2026-02-22 10:25:35 +00:00
masoodafar-web 710ddbe261 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 6m2s
2026-02-21 20:38:23 +03:30
masoodafar-web e2ca65ed98 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 6m42s
2026-02-18 01:19:34 +03:30
masoodafar-web ae9b448a64 merge: kub-stage → production (1:1 square images + site pages + landing animation fix)
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2026-02-18 01:01:44 +03:30
Masoud 1ea928118d fix: update references from تخفیفی to اعتباری across multiple files
Build and Deploy to Production / build-and-deploy (push) Successful in 5m46s
2026-02-17 15:26:09 +03:30
Masoud 8f2d739d1f fix: update references from تخفیفی to اعتباری across multiple files
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2026-02-17 15:24:13 +03:30
masoodafar-web 8e89d5e064 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 5m43s
2026-02-17 01:59:40 +03:30
masoodafar-web 8bd961ade1 Fix production workflow: use SERVER_PASSWORD, add docker login, use set image for deploy
Build and Deploy to Production / build-and-deploy (push) Successful in 5m39s
2026-02-17 01:24:06 +03:30
masoodafar-web 750d4c6b97 Remove staging workflow files from production branch to prevent cross-deployment 2026-02-17 01:15:59 +03:30
masoodafar-web 276a4c20f2 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 1m12s
Features merged:
- Discount store UI improvements (100% auto-discount, VAT breakdown)
- PaymentStatus display on orders (Pending/Success/Failed/Refunded)
- Failed payment UX with re-order link
- Product discount badges show actual percent
- Various UI and bug fixes

Production workflow preserved.
2026-02-17 00:55:52 +03:30
masoodafar-web 98a68074bc chore: Update production workflow to use Nexus registry (no proxy needed)
Build and Deploy to Production / build-and-deploy (push) Failing after 0s
- Changed Dockerfile base images to use 194.5.195.53:32082 (Nexus)
- Updated workflow to use docker-sshpass image from Nexus
- Removed HTTP_PROXY/HTTPS_PROXY dependencies
- Changed deploy method to SSH instead of kubeconfig
- Registry: 194.5.195.53:30080 (Gitea internal)
- K8S Server: 45.149.79.127 (Production)
2026-01-29 22:04:31 +03:30
73 changed files with 575 additions and 2111 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
# Keep production config from current branch during merges — never ov erwrite
# Keep production config from current branch during merges — never overwrite
src/FrontOffice.Main/appsettings.Production.json merge=ours
-46
View File
@@ -1,46 +0,0 @@
name: Push nuget and docker image Actions Workflow
on:
push:
branches:
- stage-new
jobs:
Deploy:
runs-on: windows
steps:
- name: Checkout
uses: https://git.afrino.co/actions/checkout@v3
- name: Setup dotnet
uses: https://git.afrino.co/actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
- name: Remove Package Source
run: dotnet nuget remove source FourSat
continue-on-error: true
- name: Add Package Source
run: dotnet nuget add source --name FourSat --username systemuser --password sZSA7PTiv3pUSQZ https://git.afrino.co/api/packages/FourSat/nuget/index.json --store-password-in-clear-text
- name: Install dependencies
run: dotnet restore ".\src\FrontOffice.Main\FrontOffice.Main.csproj"
- name: Build
run: dotnet build ".\src\FrontOffice.Main\FrontOffice.Main.csproj" --configuration Release --no-restore
- name: Test
run: dotnet test ".\src\FrontOffice.Main\FrontOffice.Main.csproj" --no-restore --verbosity normal
- name: Recycle Apppool
run: |
& "C:\Windows\System32\inetsrv\appcmd.exe" recycle apppool /apppool.name:kbs1.ir
shell: powershell
- name: Stop Website
run: |
& "C:\Windows\System32\inetsrv\appcmd.exe" stop site /site.name:kbs1.ir
shell: powershell
- name: Publish
run: dotnet publish ".\src\FrontOffice.Main\FrontOffice.Main.csproj" -c Release -o publish
- name: Copy Publish To IIS Directory
run: Get-ChildItem -Path "publish\*" | Copy-Item -Destination "E:\kbs1.ir\kbs1.ir\" -Recurse -Force
- name: Start Website
run: |
& "C:\Windows\System32\inetsrv\appcmd.exe" start site /site.name:kbs1.ir
shell: powershell
+9 -13
View File
@@ -6,15 +6,15 @@ on:
- kub-stage
env:
REGISTRY: 185.252.31.44:30080
REGISTRY: 194.5.195.53:30080
IMAGE_NAME: admin/frontoffice
K8S_SERVER: 185.252.31.44
K8S_SERVER: 194.5.195.53
jobs:
build-and-deploy:
runs-on: ubuntu-latest
container:
image: 185.252.31.44:32082/docker-sshpass:latest
image: 194.5.195.53:32082/docker-sshpass:latest
options: --privileged
steps:
- name: Start Docker daemon
@@ -22,7 +22,7 @@ jobs:
mkdir -p /etc/docker
cat > /etc/docker/daemon.json << 'DAEMON'
{
"insecure-registries": ["185.252.31.44:30080", "185.252.31.44:32500", "185.252.31.44:32082"]
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
}
DAEMON
echo "🚀 Starting Docker daemon..."
@@ -52,7 +52,7 @@ jobs:
- name: Login to Docker registries
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 185.252.31.44:32082 -u admin --password-stdin
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
@@ -73,14 +73,10 @@ jobs:
# Apply manifests and restart
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
kubectl apply -n kbs-stage -f /tmp/frontoffice-deployment.yaml &&
# Drop cached :latest so IfNotPresent/Always both pick the new digest
# (crictl is broken on this host — use containerd ctr)
ctr -n k8s.io images ls -q | grep -E 'frontoffice(:latest|@)' | while read -r ref; do
ctr -n k8s.io images rm \"\$ref\" 2>/dev/null || true
done
kubectl -n kbs-stage rollout restart deployment/frontoffice &&
kubectl -n kbs-stage rollout status deployment/frontoffice --timeout=180s &&
kubectl apply -f /tmp/frontoffice-deployment.yaml &&
crictl rmi ${REGISTRY}/${IMAGE_NAME}:latest 2>/dev/null || true &&
kubectl rollout restart deployment/frontoffice &&
kubectl rollout status deployment/frontoffice --timeout=180s &&
rm -f /tmp/frontoffice-deployment.yaml
"
echo "✅ Deployed!"
+8 -7
View File
@@ -6,7 +6,7 @@ on:
- production
env:
REGISTRY: 185.252.31.44:30080
REGISTRY: 194.5.195.53:30080
IMAGE_NAME: admin/frontoffice
K8S_SERVER: 45.149.79.127
@@ -14,7 +14,7 @@ jobs:
build-and-deploy:
runs-on: ubuntu-latest
container:
image: 185.252.31.44:32082/docker-sshpass:latest
image: 194.5.195.53:32082/docker-sshpass:latest
options: --privileged
steps:
- name: Start Docker daemon
@@ -22,7 +22,7 @@ jobs:
mkdir -p /etc/docker
cat > /etc/docker/daemon.json << 'DAEMON'
{
"insecure-registries": ["185.252.31.44:30080", "185.252.31.44:32500", "185.252.31.44:32082"]
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
}
DAEMON
echo "🚀 Starting Docker daemon..."
@@ -40,7 +40,7 @@ jobs:
done
if ! docker info >/dev/null 2>&1; then
echo "❌ Docker daemon failed to start after 3 minutes"
echo "❌ Docker daemon failed to start"
exit 1
fi
@@ -50,15 +50,16 @@ jobs:
- name: Login to Docker registries
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 185.252.31.44:32082 -u admin --password-stdin
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 194.5.195.53:32082 -u admin --password-stdin
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
- name: Build Docker Image
run: |
cd src
DOCKER_BUILDKIT=0 docker build --network host -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
DOCKER_BUILDKIT=0 docker build --network host -f FrontOffice.Main/Dockerfile \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod \
-f FrontOffice.Main/Dockerfile .
.
- name: Push to Registry
run: |
+5 -5
View File
@@ -1,10 +1,10 @@
# ---> VisualStudio
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
@@ -14,7 +14,7 @@
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
# Mono auto generated files
mono_crash.*
# Build results
-1
View File
@@ -1,4 +1,3 @@
# Docs moved to totalDoc
See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation.
+1
View File
@@ -101,6 +101,7 @@
| 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` | بررسی موجودی برای پرداخت سفارش |
+1 -1
View File
@@ -20,7 +20,7 @@ spec:
spec:
containers:
- name: frontoffice
image: 185.252.31.44:30080/admin/frontoffice:prod
image: 194.5.195.53:30080/admin/frontoffice:prod
imagePullPolicy: Always
ports:
- containerPort: 80
+3 -32
View File
@@ -4,7 +4,7 @@ apiVersion: apps/v1
kind: Deployment
metadata:
name: frontoffice
namespace: kbs-stage
namespace: default
labels:
app: frontoffice
environment: staging
@@ -20,8 +20,7 @@ spec:
spec:
containers:
- name: frontoffice
image: 185.252.31.44:30080/admin/frontoffice:latest
# Staging uses :latest — Always avoids stale node cache after registry retag
image: 194.5.195.53:30080/admin/frontoffice:latest
imagePullPolicy: Always
ports:
- containerPort: 80
@@ -44,7 +43,7 @@ apiVersion: v1
kind: Service
metadata:
name: frontoffice-svc
namespace: kbs-stage
namespace: default
labels:
app: frontoffice
spec:
@@ -55,31 +54,3 @@ spec:
targetPort: 80
name: http
type: ClusterIP
---
# FrontOffice Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: frontoffice-ingress
namespace: kbs-stage
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- frontoffice.se.kbs1.ir
secretName: frontoffice-tls
rules:
- host: frontoffice.se.kbs1.ir
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontoffice-svc
port:
number: 80
+2 -2
View File
@@ -1,4 +1,4 @@
FROM 185.252.31.44:32082/dotnet/sdk:9.0 AS build
FROM 194.5.195.53:32082/dotnet/sdk:9.0 AS build
WORKDIR /src
# Copy NuGet config and project file
@@ -16,7 +16,7 @@ WORKDIR "/src/FrontOffice.Main"
RUN dotnet publish "FrontOffice.Main.csproj" -c Release -o /app/publish
# Runtime stage - aspnet for Blazor Server
FROM 185.252.31.44:32082/dotnet/aspnet:9.0 AS runtime
FROM 194.5.195.53:32082/dotnet/aspnet:9.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
+4 -15
View File
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
@@ -10,7 +10,9 @@
<ItemGroup>
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
<!-- Replace all FrontOffice.BFF protobuf packages with CMS protobuf -->
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.204" />
<!-- <ProjectReference Include="../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj" />-->
<PackageReference Include="MudBlazor" Version="8.14.0" />
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="Mapster" Version="7.4.0" />
@@ -22,19 +24,6 @@
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.0" />
</ItemGroup>
<!-- ============================================================ -->
<!-- Proto References: Direct reference to CMS Protobuf project -->
<!-- This enables direct communication with CMS (no BFF needed) -->
<!-- ============================================================ -->
<!-- CMS Protobuf: NuGet in CI/Docker; ProjectReference for local monorepo dev -->
<ItemGroup Condition="Exists('../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj')">
<ProjectReference Include="../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj" />
</ItemGroup>
<ItemGroup Condition="!Exists('../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj')">
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.213" />
</ItemGroup>
<!-- Local Proto Project Reference for development -->
<!-- <ItemGroup>-->
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.Package.Protobuf\FrontOffice.BFF.Package.Protobuf.csproj" />-->
+1 -1
View File
@@ -3,7 +3,7 @@
<packageSources>
<clear />
<!-- Nexus (hosts FourSat packages + proxies nuget.org) -->
<add key="Nexus" value="http://185.252.31.44:32081/repository/nuget-all/index.json" allowInsecureConnections="true" />
<add key="Nexus" value="http://194.5.195.53:32081/repository/nuget-all/index.json" allowInsecureConnections="true" />
</packageSources>
<packageSourceCredentials>
<Nexus>
+76 -13
View File
@@ -71,6 +71,80 @@
</MudStack>
}
</MudPaper>
<!-- Address Selection -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h5" Class="mb-4">انتخاب آدرس</MudText>
@if (_isLoadingAddresses)
{
<MudStack AlignItems="AlignItems.Center" Class="py-4">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Medium" />
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-2">بارگذاری آدرس‌ها...</MudText>
</MudStack>
}
else if (_addresses.Any())
{
<MudStack Spacing="2">
@foreach (var address in _addresses)
{
<MudPaper Outlined="@(_selectedAddress?.Id != address.Id)"
Elevation="@(_selectedAddress?.Id == address.Id ? 4 : 0)"
Class="pa-3 rounded-xl cursor-pointer"
Style="@(_selectedAddress?.Id == address.Id ? "border: 2px solid var(--mud-palette-primary);" : "")"
@onclick="() => SetAddressAsDefault(address.Id)">
<MudStack Spacing="1">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudCheckBox @bind-Value="address.IsDefault"
Disabled="true"
Color="Color.Primary" />
<MudText Typo="Typo.subtitle2">@(address.Title)</MudText>
@if (address.IsDefault)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Filled" Size="Size.Small">پیش‌فرض</MudChip>
}
@if (!address.IsDefault)
{
@if (_isSettingDefaultAddress && _settingDefaultAddressId == address.Id)
{
<MudProgressCircular Size="Size.Small" Color="Color.Primary" Indeterminate="true" />
}
else
{
<MudButton Variant="Variant.Text"
Color="Color.Primary"
Size="Size.Small"
OnClick="() => SetAddressAsDefault(address.Id)">
تنظیم به عنوان پیش‌فرض
</MudButton>
}
}
</MudStack>
<MudStack Row="true">
<MudText Typo="Typo.body2" Class="mud-text-secondary">@(address.Address)</MudText>
<MudSpacer />
<MudText Typo="Typo.caption" Class="mud-text-secondary">کد پستی: @(address.PostalCode)</MudText>
</MudStack>
</MudStack>
</MudPaper>
}
</MudStack>
}
else
{
<MudStack AlignItems="AlignItems.Center" Class="py-4">
<MudIcon Icon="@Icons.Material.Filled.LocationOff" Size="Size.Large" Color="Color.Default" />
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-2">آدرسی ثبت نشده است.</MudText>
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="() => Navigation.NavigateTo(RouteConstants.Profile.Index)"
Class="mt-2">
افزودن آدرس
</MudButton>
</MudStack>
}
</MudPaper>
</MudStack>
</MudItem>
@@ -140,18 +214,7 @@
OnClick="ProcessPayment"
Disabled="@(!CanProceedToPayment || _isProcessingPayment || !_selectedPackage.SupportsDirectPurchase)"
Class="mt-2">
@(_isProcessingPayment ? "در حال پردازش..." : "پرداخت آنلاین (زرین‌پال)")
</MudButton>
<MudButton Variant="Variant.Filled"
Color="Color.Info"
Size="Size.Large"
FullWidth="true"
StartIcon="@Icons.Material.Filled.AccountBalance"
OnClick="ProcessTwentyCardPayment"
Disabled="@(!CanProceedToPayment || _isProcessingPayment || !_selectedPackage.SupportsDirectPurchase)"
Class="mt-2">
پرداخت با ۲۰کارت
@(_isProcessingPayment ? "در حال پردازش..." : "پرداخت آنلاین")
</MudButton>
@if (_selectedPackage.SupportsDayaPurchase)
@@ -178,7 +241,7 @@
@if (!CanProceedToPayment)
{
<MudText Typo="Typo.caption" Color="Color.Error" Align="Align.Center">
لطفاً پکیج را انتخاب کنید.
لطفاً پکیج و آدرس را انتخاب کنید.
</MudText>
}
</MudStack>
+74 -40
View File
@@ -1,4 +1,6 @@
using CMSMicroservice.Protobuf.Protos.Package;
using CMSMicroservice.Protobuf.Protos.UserAddress;
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
@@ -11,10 +13,14 @@ public partial class Checkout
{
[Inject] private PackageService PackageService { get; set; } = default!;
[Inject] private PackageContract.PackageContractClient PackageClient { get; set; } = default!;
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
[Parameter] public long? PackageId { get; set; }
private Pack? _selectedPackage;
private List<CustomerAddressModel> _addresses = new();
private CustomerAddressModel? _selectedAddress;
private bool _isLoadingAddresses;
private bool _isProcessingPayment;
// Discount code
@@ -25,11 +31,16 @@ public partial class Checkout
private long _discountAmount;
private long _finalPrice;
private bool CanProceedToPayment => _selectedPackage != null;
// Address management
private bool _isSettingDefaultAddress;
private long? _settingDefaultAddressId;
private bool CanProceedToPayment => _selectedPackage != null && _selectedAddress != null;
protected override async Task OnInitializedAsync()
{
await LoadPackageDetails();
await LoadAddresses();
}
private async Task LoadPackageDetails()
@@ -61,6 +72,60 @@ public partial class Checkout
}
}
private async Task LoadAddresses()
{
_isLoadingAddresses = true;
try
{
var response = await UserAddressContract.GetCustomerAddressesAsync(request: new());
if (response?.Models?.Any() == true)
{
_addresses = response.Models.ToList();
// Select default address if available
_selectedAddress = _addresses.FirstOrDefault(a => a.IsDefault) ?? _addresses.First();
}
else
{
_addresses = new List<CustomerAddressModel>();
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری آدرس‌ها: {ex.Message}", Severity.Error);
_addresses = new List<CustomerAddressModel>();
}
finally
{
_isLoadingAddresses = false;
await InvokeAsync(StateHasChanged);
}
}
private async Task SetAddressAsDefault(long addressId)
{
if (_isSettingDefaultAddress) return;
_isSettingDefaultAddress = true;
_settingDefaultAddressId = addressId;
try
{
await UserAddressContract.SetAddressAsDefaultAsync(new() { Id = addressId });
await LoadAddresses(); // Reload addresses to reflect the change
Snackbar.Add("آدرس پیش‌فرض با موفقیت تغییر یافت.", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در تغییر آدرس پیش‌فرض: {ex.Message}", Severity.Error);
}
finally
{
_isSettingDefaultAddress = false;
_settingDefaultAddressId = null;
await InvokeAsync(StateHasChanged);
}
}
private async Task ApplyDiscountCode()
{
if (string.IsNullOrWhiteSpace(_discountCode))
@@ -92,7 +157,7 @@ public partial class Checkout
_finalPrice = _selectedPackage!.Price;
}
}
catch (Exception)
catch (Exception ex)
{
_discountMessage = "خطا در اعمال کد تخفیف.";
_discountApplied = false;
@@ -107,39 +172,12 @@ public partial class Checkout
}
private async Task ProcessPayment()
=> await StartGatewayPayment(gatewayProvider: null);
private async Task ProcessTwentyCardPayment()
{
if (!CanProceedToPayment || _selectedPackage == null)
{
Snackbar.Add("لطفاً پکیج را انتخاب کنید.", Severity.Warning);
return;
}
var options = new DialogOptions
{
MaxWidth = MaxWidth.Small,
FullWidth = true,
CloseOnEscapeKey = true,
BackdropClick = true
};
var dialog = await DialogService.ShowAsync<Shared.TwentyCardCreditDialog>(
string.Empty, options);
var result = await dialog.Result;
if (result.Canceled) return;
await StartGatewayPayment(gatewayProvider: "20card");
}
private async Task StartGatewayPayment(string? gatewayProvider)
{
if (_isProcessingPayment) return;
if (!CanProceedToPayment || _selectedPackage == null)
if (!CanProceedToPayment || _selectedPackage == null || _selectedAddress == null)
{
Snackbar.Add("لطفاً پکیج را انتخاب کنید.", Severity.Warning);
Snackbar.Add("لطفاً پکیج و آدرس را انتخاب کنید.", Severity.Warning);
return;
}
@@ -147,15 +185,11 @@ public partial class Checkout
try
{
var request = new CustomerPurchasePackageRequest
var response = await PackageClient.CustomerPurchasePackageAsync(new CustomerPurchasePackageRequest
{
PackageId = _selectedPackage.Id,
PurchaseMethod = PurchaseMethodEnum.PurchaseMethodGateway
};
if (!string.IsNullOrWhiteSpace(gatewayProvider))
request.GatewayProvider = gatewayProvider;
var response = await PackageClient.CustomerPurchasePackageAsync(request);
});
if (!response.Success)
{
@@ -185,9 +219,9 @@ public partial class Checkout
private async Task DayaLoanPayment()
{
if (_selectedPackage == null)
if (_selectedAddress == null)
{
Snackbar.Add("لطفاً پکیج را انتخاب کنید.", Severity.Warning);
Snackbar.Add("لطفاً آدرس را انتخاب کنید.", Severity.Warning);
return;
}
@@ -196,4 +230,4 @@ public partial class Checkout
}
private record Pack(long Id, string Title, string Body, string Image, long Price, bool SupportsDirectPurchase = true, bool SupportsDayaPurchase = false);
}
}
@@ -22,11 +22,9 @@
else if (_addresses.Count == 0)
{
<MudAlert Severity="Severity.Warning">
هیچ آدرسی ثبت نشده است. می‌توانید همین‌جا آدرس جدید اضافه کنید.
هیچ آدرسی ثبت نشده است. لطفاً از بخش پروفایل آدرس خود را اضافه کنید.
</MudAlert>
<MudButton Class="mt-2" Variant="Variant.Outlined" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="OpenAddAddressDialog">افزودن آدرس</MudButton>
<MudButton Class="mt-2" Variant="Variant.Outlined" Href="/profile/addresses">افزودن آدرس</MudButton>
}
else
{
@@ -43,25 +41,13 @@
<MudText Typo="Typo.subtitle2">@address.Title</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@address.Address</MudText>
</MudStack>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
@if (address.IsDefault)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Outlined" Size="Size.Small">پیش‌فرض</MudChip>
}
<span @onclick:stopPropagation="true">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Size="Size.Small"
Color="Color.Primary"
aria-label="ویرایش آدرس"
OnClick="@(() => OpenEditAddressDialog(address))" />
</span>
</MudStack>
@if (address.IsDefault)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Outlined">پیش‌فرض</MudChip>
}
</MudStack>
</MudPaper>
}
<MudButton Class="mt-2" Variant="Variant.Text" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="OpenAddAddressDialog">افزودن آدرس جدید</MudButton>
</MudStack>
}
</MudPaper>
@@ -1,6 +1,5 @@
using Microsoft.AspNetCore.Components;
using CMSMicroservice.Protobuf.Protos.UserAddress;
using FrontOffice.Main.Pages.Profile.Components;
using FrontOffice.Main.Utilities;
using MudBlazor;
@@ -41,11 +40,7 @@ public partial class Checkout
}
await VAT.LoadAsync();
await DiscountCart.EnsureInitializedAsync();
var userInfo = await AuthService.GetUserAuthInfo();
if (userInfo.HasAddress)
await LoadAddresses();
else
_addresses = new();
await LoadAddresses();
}
private async Task LoadAddresses()
@@ -62,14 +57,12 @@ public partial class Checkout
else
{
_addresses = new();
_selectedAddress = null;
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری آدرس‌ها: {ex.Message}", Severity.Error);
_addresses = new();
_selectedAddress = null;
}
finally
{
@@ -78,30 +71,6 @@ public partial class Checkout
}
}
private async Task OpenAddAddressDialog()
{
var dialog = await DialogService.ShowAsync<AddAddressDialog>("افزودن آدرس جدید");
var result = await dialog.Result;
if (result is { Canceled: false })
{
await AuthService.RefreshTokenAsync();
await LoadAddresses();
}
}
private async Task OpenEditAddressDialog(CustomerAddressModel address)
{
var dialog = await DialogService.ShowAsync<EditAddressDialog>("ویرایش آدرس", new DialogParameters<EditAddressDialog>
{
{ x => x.Model, address }
});
var result = await dialog.Result;
if (result is { Canceled: false })
{
await LoadAddresses();
}
}
private async Task PlaceOrder()
{
if (!CanPlaceOrder || _selectedAddress is null)
@@ -76,7 +76,6 @@
{
<MudItem xs="6" sm="6" md="3"
onclick="@(() => NavigateToProduct(p.Id))">
<div id="@($"shop-product-{p.Id}")" class="h-100">
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden"
Style="cursor:pointer;">
<MudCardContent Class="d-flex flex-column pa-1 h-100">
@@ -128,7 +127,6 @@
</MudButton>
</MudCardActions>
</MudCard>
</div>
</MudItem>
}
</MudGrid>
@@ -1,7 +1,5 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.DiscountStore;
@@ -12,7 +10,6 @@ public partial class Products : ComponentBase, IDisposable
[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;
@@ -23,107 +20,42 @@ public partial class Products : ComponentBase, IDisposable
private bool _hasMore = true;
private int _totalCount;
private const int PageSize = 12;
private const string DefaultSortBy = "price desc";
private List<DiscountProductCard> _products = new();
private List<DiscountCategoryNode> _categories = new();
private bool _ignoreNextLocationChange;
private bool _pendingScrollRestore;
protected override async Task OnInitializedAsync()
{
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
_loading = true;
await DiscountCart.EnsureInitializedAsync();
DiscountCart.OnChange += StateHasChanged;
Navigation.LocationChanged += HandleLocationChanged;
_categories = await ProductService.GetCategoriesAsync();
ApplyStateFromUri();
_loading = true;
await LoadPages(_currentPage);
var categoriesTask = ProductService.GetCategoriesAsync();
var productsTask = ProductService.GetProductsAsync(page: 1, pageSize: PageSize);
await Task.WhenAll(categoriesTask, productsTask);
_categories = categoriesTask.Result;
var result = productsTask.Result;
_products = result.Products;
_totalCount = result.TotalCount;
_hasMore = result.CurrentPage < result.TotalPages;
_loading = false;
_pendingScrollRestore = true;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (_pendingScrollRestore && !_loading && _products.Count > 0)
{
_pendingScrollRestore = false;
var payload = await ShopListScrollRestore.TakeAsync(Js, ShopListScrollRestore.DiscountKey);
if (payload is not null)
await ShopListScrollRestore.RestoreAsync(Js, payload);
}
await base.OnAfterRenderAsync(firstRender);
}
private void ApplyStateFromUri()
{
var state = ShopListQueryState.Parse(Navigation.ToAbsoluteUri(Navigation.Uri));
_search = state.Query;
_selectedCategoryId = state.CategoryId;
_currentPage = state.Pages;
}
private ShopListQueryState CaptureState() => new()
{
Query = _search,
CategoryId = _selectedCategoryId,
Pages = Math.Max(1, _currentPage)
};
private void SyncUrl()
{
var target = CaptureState().ToRelativeUrl(RouteConstants.DiscountStore.Products);
var currentPathAndQuery = Navigation.ToAbsoluteUri(Navigation.Uri).PathAndQuery;
if (string.Equals(currentPathAndQuery, target, StringComparison.OrdinalIgnoreCase))
return;
_ignoreNextLocationChange = true;
Navigation.NavigateTo(target, replace: true);
}
private async Task LoadPages(int pagesToLoad)
{
_products.Clear();
_hasMore = true;
pagesToLoad = Math.Max(1, pagesToLoad);
var search = string.IsNullOrWhiteSpace(_search) ? null : _search;
for (var page = 1; page <= pagesToLoad; page++)
{
var result = await ProductService.GetProductsAsync(
page: page,
pageSize: PageSize,
search: search,
categoryId: _selectedCategoryId,
sortBy: DefaultSortBy);
if (page == 1)
{
_products = result.Products;
_totalCount = result.TotalCount;
}
else
{
_products.AddRange(result.Products);
}
_currentPage = page;
_hasMore = result.CurrentPage < result.TotalPages;
if (!_hasMore)
break;
}
}
private async Task ReloadFromFilters()
private async Task LoadInitial()
{
_loading = true;
_currentPage = 1;
_products.Clear();
StateHasChanged();
await LoadPages(1);
SyncUrl();
var result = await ProductService.GetProductsAsync(
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;
StateHasChanged();
}
@@ -131,37 +63,37 @@ public partial class Products : ComponentBase, IDisposable
private async Task LoadMore()
{
if (_loadingMore || !_hasMore) return;
_loadingMore = true;
StateHasChanged();
_currentPage++;
var result = await ProductService.GetProductsAsync(
page: _currentPage,
pageSize: PageSize,
search: string.IsNullOrWhiteSpace(_search) ? null : _search,
categoryId: _selectedCategoryId,
sortBy: DefaultSortBy);
categoryId: _selectedCategoryId);
_products.AddRange(result.Products);
_hasMore = result.CurrentPage < result.TotalPages;
SyncUrl();
_loadingMore = false;
StateHasChanged();
}
private async Task SearchProducts() => await ReloadFromFilters();
private async Task SearchProducts() => await LoadInitial();
private async Task OnSearchKeyUp(KeyboardEventArgs e)
{
if (e.Key == "Enter")
await ReloadFromFilters();
{
await LoadInitial();
}
}
private async Task OnCategoryChanged(long? value)
{
_selectedCategoryId = value;
await ReloadFromFilters();
await LoadInitial();
}
private async Task AddToCart(DiscountProductCard p)
@@ -169,44 +101,15 @@ public partial class Products : ComponentBase, IDisposable
await GuestGate.RunAsync(() => DiscountCart.AddAsync(p.Id));
}
private async Task NavigateToProduct(long id)
private void NavigateToProduct(long id)
{
await ShopListScrollRestore.SaveAsync(Js, ShopListScrollRestore.DiscountKey, id);
Navigation.NavigateTo($"{RouteConstants.DiscountStore.ProductDetail}{id}");
}
private void HandleLocationChanged(object? sender, LocationChangedEventArgs args)
{
if (_ignoreNextLocationChange)
{
_ignoreNextLocationChange = false;
return;
}
var uri = Navigation.ToAbsoluteUri(args.Location);
if (!uri.AbsolutePath.Equals(RouteConstants.DiscountStore.Products, StringComparison.OrdinalIgnoreCase))
return;
var incoming = ShopListQueryState.Parse(uri);
if (incoming.Matches(CaptureState()))
return;
_ = InvokeAsync(async () =>
{
ApplyStateFromUri();
_loading = true;
await LoadPages(_currentPage);
_loading = false;
_pendingScrollRestore = true;
StateHasChanged();
});
}
private static string FormatPrice(long price) => $"{price:N0} تومان";
public void Dispose()
{
DiscountCart.OnChange -= StateHasChanged;
Navigation.LocationChanged -= HandleLocationChanged;
}
}
-47
View File
@@ -93,48 +93,9 @@
</section>
}
@* ═══════════════════════════════════════════════
1c. TWENTYCARD CREDIT — promo box
═══════════════════════════════════════════════ *@
<section class="py-4" style="background:var(--mud-palette-surface);">
<MudContainer MaxWidth="MaxWidth.Medium">
<MudPaper Elevation="0" Class="landing-twentycard-box rounded-xl pa-4 pa-md-5">
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Start" Class="flex-wrap">
<MudAvatar Color="Color.Info" Variant="Variant.Filled" Size="Size.Large" Class="rounded-lg" Style="min-width:56px;">
<MudIcon Icon="@Icons.Material.Filled.AccountBalance" />
</MudAvatar>
<MudStack Spacing="2" Style="flex:1; min-width:220px;">
<MudText Typo="Typo.h6">تأمین اعتبار ۲۰کارت</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
با تأمین اعتبار از طریق ۲۰کارت می‌توانید بدون پرداخت نقدی کامل، هزینه خرید پکیج و محصولات در کارا بازار را پوشش دهید.
فرایند درخواست و تأیید اعتبار در پنل ۲۰کارت انجام می‌شود و پس از آن پرداخت از همان مسیر در دسترس است.
این روش برای کسانی مناسب است که می‌خواهند سریع‌تر وارد مسیر عضویت و خرید شوند.
</MudText>
<MudText Typo="Typo.body2">
بعد از دریافت وام، می‌توانید در کارا بازار از باشگاه مشتریان استفاده کنید و با فروش و دعوت، توسعه سرمایه داشته باشید.
</MudText>
<div class="mt-1">
<MudButton Variant="Variant.Filled"
Color="Color.Info"
Class="rounded-pill"
StartIcon="@Icons.Material.Filled.OpenInNew"
Disabled="@string.IsNullOrWhiteSpace(_twentyCardPortalUrl)"
OnClick="OpenTwentyCardPortal">
ورود به سایت ۲۰کارت
</MudButton>
</div>
</MudStack>
</MudStack>
</MudPaper>
</MudContainer>
</section>
@* ═══════════════════════════════════════════════
2. HOW IT WORKS — 3 numbered steps
(appsettings Landing:ShowStepsSection; default false)
═══════════════════════════════════════════════ *@
@if (_showStepsSection)
{
<section class="section-landing">
<MudContainer MaxWidth="MaxWidth.Large">
<div class="text-center mb-8 fade-in-up">
@@ -160,14 +121,10 @@
</MudGrid>
</MudContainer>
</section>
}
@* ═══════════════════════════════════════════════
3. FEATURES — 6 cards with icons
(appsettings Landing:ShowFeaturesSection; default false)
═══════════════════════════════════════════════ *@
@if (_showFeaturesSection)
{
<section class="section-landing" style="background:var(--mud-palette-background-gray);">
<MudContainer MaxWidth="MaxWidth.Large">
<div class="text-center mb-8 fade-in-up">
@@ -195,7 +152,6 @@
</MudGrid>
</MudContainer>
</section>
}
@* ═══════════════════════════════════════════════
4. STATS — Counter cards
@@ -473,8 +429,6 @@ else
@* ═══════════════════════════════════════════════
6. TESTIMONIALS
═══════════════════════════════════════════════ *@
@if (_testimonials.Count > 0)
{
<section class="section-landing">
<MudContainer MaxWidth="MaxWidth.Large">
<div class="text-center mb-8 fade-in-up">
@@ -511,7 +465,6 @@ else
</MudGrid>
</MudContainer>
</section>
}
@* ═══════════════════════════════════════════════
7. FAQ
+3 -21
View File
@@ -1,6 +1,5 @@
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Configuration;
using Microsoft.JSInterop;
using MudBlazor;
@@ -17,12 +16,8 @@ public partial class Index : IDisposable
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
[Inject] private VATService VAT { get; set; } = default!;
[Inject] private IConfiguration Configuration { get; set; } = default!;
private bool _isAuthenticated;
private string _twentyCardPortalUrl = string.Empty;
private bool _showStepsSection;
private bool _showFeaturesSection;
// ── CMS page data ──
private PageSettingsDto? _pageData;
@@ -54,10 +49,6 @@ public partial class Index : IDisposable
{
MainService.OnChangeHandler += OnStateChanged;
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
_twentyCardPortalUrl = Configuration["TwentyCard:CreditPortalUrl"] ?? string.Empty;
// appsettings Landing:* — default false (hidden). Set true to restore sections.
_showStepsSection = Configuration.GetValue("Landing:ShowStepsSection", false);
_showFeaturesSection = Configuration.GetValue("Landing:ShowFeaturesSection", false);
// Load landing page settings from CMS
try
@@ -171,7 +162,7 @@ public partial class Index : IDisposable
(Icons.Material.Outlined.VerifiedUser, "ثبت‌نام سریع و ساده", "در چند گام کوتاه حساب بسازید و شروع کنید.", 0),
(Icons.Material.Outlined.StarBorder, "پاداش‌های شفاف", "قوانین روشن، دسترسی آسان به سوابق و گزارش‌ها.", 100),
(Icons.Material.Outlined.Devices, "طراحی واکنش‌گرا", "تجربه‌ای روان در موبایل و دسکتاپ.", 200),
(Icons.Material.Outlined.Groups, "تیم‌سازی هوشمند", "ساختار درختی سازمان و مدیریت تیم‌های فروش.", 300),
(Icons.Material.Outlined.Groups, "تیم‌سازی هوشمند", "ساختار درختی شبکه و مدیریت تیم‌های فروش.", 300),
(Icons.Material.Outlined.Insights, "گزارش‌های لحظه‌ای", "داشبورد پویا برای مشاهده عملکرد و کمیسیون.", 400),
(Icons.Material.Outlined.Lock, "امنیت بالا", "رمزنگاری اطلاعات و احراز هویت چندمرحله‌ای.", 500),
};
@@ -202,10 +193,9 @@ public partial class Index : IDisposable
}
// ── Testimonials ──
// اگر تنظیمات از CMS لود شده، لیست خالی یعنی ادمین عمداً حذف کرده — بدون fallback hardcode
if (_settings is not null)
if (_settings?.Testimonials?.Any() == true)
{
_testimonials = (_settings.Testimonials ?? [])
_testimonials = _settings.Testimonials
.Select((t, i) => new TestimonialItem(t.Quote ?? "", t.Name ?? "", t.Role ?? "", i * 150))
.ToList();
}
@@ -307,12 +297,6 @@ public partial class Index : IDisposable
Navigation.NavigateTo(RouteConstants.Registration.Wizard);
}
private async Task OpenTwentyCardPortal()
{
if (string.IsNullOrWhiteSpace(_twentyCardPortalUrl)) return;
await JS.InvokeVoidAsync("open", _twentyCardPortalUrl, "_blank");
}
private void NavigateToPost(string slug)
{
Navigation.NavigateTo($"/blog/{slug}");
@@ -349,10 +333,8 @@ public partial class Index : IDisposable
public string? HeroButtonPrimaryText { get; set; }
public string? HeroButtonSecondaryText { get; set; }
public List<TrustBadgeItem>? TrustBadges { get; set; }
public bool? StepsEnabled { get; set; }
public string? StepsTitle { get; set; }
public List<StepItem>? Steps { get; set; }
public bool? FeaturesEnabled { get; set; }
public string? FeaturesTitle { get; set; }
public List<FeatureItem>? Features { get; set; }
public string? StatsTitle { get; set; }
@@ -1,4 +1,4 @@
using FrontOffice.Main.Utilities;
using CMSMicroservice.Protobuf.Protos.UserAddress;
using CMSMicroservice.Protobuf.Protos.UserAddress;
using FrontOffice.Main.Pages.Profile.Components;
using Microsoft.AspNetCore.Components;
@@ -9,7 +9,6 @@ namespace FrontOffice.Main.Pages.Profile;
public partial class Addresses : ComponentBase
{
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!;
private List<CustomerAddressModel> _addresses = new();
private bool _isLoadingAddresses;
@@ -43,10 +42,7 @@ public partial class Addresses : ComponentBase
var dialog = await DialogService.ShowAsync<AddAddressDialog>("افزودن آدرس جدید");
var result = await dialog.Result;
if (result is not null && !result.Canceled)
{
await LoadAddresses();
await AuthService.RefreshTokenAsync();
}
}
private async Task OpenEditAddressDialog(CustomerAddressModel address)
@@ -84,7 +80,6 @@ public partial class Addresses : ComponentBase
await UserAddressContract.DeleteCustomerAddressAsync(new() { Id = id });
Snackbar.Add("آدرس حذف شد.", Severity.Success);
await LoadAddresses();
await AuthService.RefreshTokenAsync();
}
catch (Exception ex)
{
@@ -35,97 +35,12 @@
</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">
مبلغ وارد شده (به تومان) از طریق درگاه پرداخت به موجودی اصلی شما اضافه می‌شود.
از موجودی اصلی برای خرید از فروشگاه عادی و پرداخت سفارش استفاده می‌شود.
@@ -207,6 +122,5 @@
</MudList>
</MudExpansionPanel>
</MudExpansionPanels>
}
</MudStack>
</MudContainer>
@@ -7,18 +7,10 @@ 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 };
@@ -31,13 +23,7 @@ public partial class ChargeCreditWallet : ComponentBase
[SupplyParameterFromQuery(Name = "returnUrl")]
public string? ReturnUrlQueryParam { get; set; }
private bool ShowChargeForm =>
_authLoaded
&& _isClubMemberActive
&& !_redirectingToMagic
&& (!_needsMagicCeilingConsent || _allowNormalDespiteMagic);
protected override async Task OnInitializedAsync()
protected override void OnInitialized()
{
_paymentResult = PaymentQueryParam;
@@ -46,63 +32,20 @@ public partial class ChargeCreditWallet : ComponentBase
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)
if (firstRender && !string.IsNullOrEmpty(_pendingReturnUrl))
{
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;
@@ -141,50 +84,6 @@ public partial class ChargeCreditWallet : ComponentBase
}
}
/// <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);
@@ -7,16 +7,6 @@
<MudStack Spacing="3">
<PageHeader Title="💳 شارژ کیف‌پول اعتباری" BackHref="@RouteConstants.Profile.Wallet" />
@* Gateway self-charge temporarily disabled. To restore: remove the alert and set _gatewayChargeEnabled = true in .cs (or flip the if below). *@
<MudAlert Severity="MudBlazor.Severity.Info" Class="rounded-lg" Variant="Variant.Outlined">
شارژ کیف‌پول اعتباری از طریق درگاه فعلاً غیرفعال است.
<MudButton Size="Size.Small" Variant="Variant.Text" Href="@RouteConstants.Profile.Wallet" Class="mt-1">
بازگشت به کیف پول
</MudButton>
</MudAlert>
@if (false) @* TEMP: set to true to restore gateway charge UI *@
{
@* ─── نتیجه پرداخت (اگه از callback برگشته) ─── *@
@if (_paymentResult != null)
{
@@ -127,6 +117,5 @@
</MudList>
</MudExpansionPanel>
</MudExpansionPanels>
}
</MudStack>
</MudContainer>
@@ -52,47 +52,29 @@
<div id="org-chart-container" class="org-chart-container"></div>
}
@* Stats Bar — فقط آمار خود کاربر (نه نودهای درخت) *@
@* Stats Bar *@
@if (_statistics != null && !_isLoading)
{
<div class="org-chart-stats">
<div class="org-chart-stats-row">
<div class="stat-item">
<span class="stat-label">کل اعضا:</span>
<span class="stat-value">@_statistics.TotalMembers</span>
</div>
<div class="stat-item">
<span class="stat-label">سازمان چپ:</span>
<span class="stat-value left">@_statistics.LeftLegCount</span>
</div>
<div class="stat-item">
<span class="stat-label">سازمان راست:</span>
<span class="stat-value right">@_statistics.RightLegCount</span>
</div>
<div class="stat-item">
<span class="stat-label">عمق:</span>
<span class="stat-value">@_statistics.TreeDepth</span>
</div>
<div class="stat-item">
<span class="stat-label">پای ضعیف:</span>
<span class="stat-value">@(_statistics.WeakerLeg == "Left" ? "چپ" : "راست")</span>
</div>
<div class="stat-item">
<span class="stat-label">کل اعضا:</span>
<span class="stat-value">@_statistics.TotalMembers</span>
</div>
<div class="org-chart-stats-row leg-scores">
<div class="stat-item leg-score-gold">
<span class="stat-label">پکیج طلایی:</span>
<span class="stat-value left">چپ @_statistics.GoldLeftLegTotal</span>
<span class="stat-sep">·</span>
<span class="stat-value right">راست @_statistics.GoldRightLegTotal</span>
</div>
<div class="stat-item">
<span class="stat-label">پای چپ:</span>
<span class="stat-value left">@_statistics.LeftLegCount</span>
</div>
<div class="org-chart-stats-row leg-scores">
<div class="stat-item leg-score-silver">
<span class="stat-label">پکیج نقره‌ای:</span>
<span class="stat-value left">چپ @_statistics.SilverLeftLegTotal</span>
<span class="stat-sep">·</span>
<span class="stat-value right">راست @_statistics.SilverRightLegTotal</span>
</div>
<div class="stat-item">
<span class="stat-label">پای راست:</span>
<span class="stat-value right">@_statistics.RightLegCount</span>
</div>
<div class="stat-item">
<span class="stat-label">عمق:</span>
<span class="stat-value">@_statistics.TreeDepth</span>
</div>
<div class="stat-item">
<span class="stat-label">پای ضعیف:</span>
<span class="stat-value">@(_statistics.WeakerLeg == "Left" ? "چپ" : "راست")</span>
</div>
</div>
}
@@ -1,5 +1,4 @@
using DateTimeConverterCL;
using FrontOffice.Main.Utilities;
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
@@ -109,9 +108,7 @@ public partial class OrganizationChart : IAsyncDisposable
isActive = n.IsActive,
isClubActive = n.IsClubActive,
activationWeekNumber = n.ActivationWeekNumber,
registeredAt = n.JoinedAt.HasValue
? n.JoinedAt.Value.ToLocalTime().MiladiToJalali()
: "",
joinedAt = n.JoinedAt,
referralCode = n.ReferralCode,
packageName = n.PackageName ?? string.Empty
}).ToArray();
@@ -46,21 +46,6 @@
</MudPaper>
</MudLink>
<MudLink Href="@RouteConstants.Profile.Invitees" Underline="Underline.None">
<MudPaper Elevation="0" Class="pa-4 rounded-xl profile-hub-item">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
<MudIcon Icon="@Icons.Material.Outlined.GroupAdd" Color="Color.Info" />
<MudStack Spacing="0">
<MudText Typo="Typo.subtitle2">دعوت‌شدگان من</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">کسانی که با موبایل شما ثبت‌نام کرده‌اند</MudText>
</MudStack>
</MudStack>
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Class="mud-text-secondary" />
</MudStack>
</MudPaper>
</MudLink>
<MudLink Href="@RouteConstants.Profile.Settings" Underline="Underline.None">
<MudPaper Elevation="0" Class="pa-4 rounded-xl profile-hub-item">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
@@ -109,13 +109,6 @@
{
<MudText Typo="Typo.caption" Color="Color.Success" Class="mt-2">@(_copyMessage)</MudText>
}
<MudButton Variant="Variant.Text"
Color="Color.Primary"
Class="mt-3"
Href="@RouteConstants.Profile.Invitees"
StartIcon="@Icons.Material.Filled.GroupAdd">
مشاهده دعوت‌شدگان من
</MudButton>
</MudPaper>
}
else
@@ -128,13 +121,6 @@
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-1" Style="max-width:560px; margin:0 auto;">
برای فعال‌سازی لینک دعوت، ابتدا یکی از <strong>پکیج‌ها</strong> را تهیه کنید و سپس در <strong>باشگاه مشتریان</strong> عضو شوید.
</MudText>
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
Class="mt-3"
Href="@RouteConstants.Profile.Invitees"
StartIcon="@Icons.Material.Filled.GroupAdd">
دعوت‌شدگان من
</MudButton>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-4" Style="max-width:560px; margin:0 auto;">
از مزایای ویژه عضویت بهره‌مند شده و با فعالیت در زمینه توسعه فروشگاه‌ها پاداش دریافت کنید.
</MudText>
@@ -333,7 +333,6 @@ public partial class Index
if (!result.Canceled)
{
await LoadAddresses();
await AuthService.RefreshTokenAsync();
}
}
@@ -386,7 +385,6 @@ public partial class Index
});
Snackbar.Add("آدرس با موفقیت حذف شد.", Severity.Success);
await LoadAddresses();
await AuthService.RefreshTokenAsync();
}
catch (Exception ex)
{
@@ -421,10 +419,7 @@ public partial class Index
switch (purchase.Method)
{
case Shared.PackagePurchaseDialog.PaymentMethodType.DirectPayment:
await ProcessDirectPayment(purchase.Package.Id, gatewayProvider: null);
break;
case Shared.PackagePurchaseDialog.PaymentMethodType.TwentyCard:
await ProcessTwentyCardPayment(purchase.Package.Id);
await ProcessDirectPayment(purchase.Package.Id);
break;
case Shared.PackagePurchaseDialog.PaymentMethodType.DayaLoan:
await ProcessDayaLoanPayment();
@@ -433,25 +428,7 @@ public partial class Index
}
}
private async Task ProcessTwentyCardPayment(long packageId)
{
var options = new DialogOptions
{
MaxWidth = MaxWidth.Small,
FullWidth = true,
CloseOnEscapeKey = true,
BackdropClick = true
};
var dialog = await DialogService.ShowAsync<Shared.TwentyCardCreditDialog>(
string.Empty, options);
var result = await dialog.Result;
if (result.Canceled) return;
await ProcessDirectPayment(packageId, gatewayProvider: "20card");
}
private async Task ProcessDirectPayment(long packageId, string? gatewayProvider = null)
private async Task ProcessDirectPayment(long packageId)
{
if (_isProcessingPayment) return;
@@ -460,15 +437,11 @@ public partial class Index
try
{
var request = new CustomerPurchasePackageRequest
var response = await PackageContract.CustomerPurchasePackageAsync(new CustomerPurchasePackageRequest
{
PackageId = packageId,
PurchaseMethod = PurchaseMethodEnum.PurchaseMethodGateway
};
if (!string.IsNullOrWhiteSpace(gatewayProvider))
request.GatewayProvider = gatewayProvider;
var response = await PackageContract.CustomerPurchasePackageAsync(request);
});
if (!response.Success)
{
@@ -518,6 +491,11 @@ public partial class Index
Snackbar.Add("لطفا اطلاعات شخصی خود را تکمیل کنید. (تاریخ تولد وارد نشده)", Severity.Error);
return;
}
if (!_addresses.Any())
{
Snackbar.Add("آدرس محل سکونت شما الزامی است!", Severity.Error);
return;
}
var url = "https://dayadiamond.ir/profile/creditpurchase/?merchantcode=56146364";
await JSRuntime.InvokeVoidAsync("open", url, "_blank");
@@ -1,157 +0,0 @@
@attribute [Authorize]
@attribute [Route(RouteConstants.Profile.Invitees)]
@using CMSMicroservice.Protobuf.Protos.User
@using CMSMicroservice.Protobuf.Protos.City
@inject UserContract.UserContractClient UserClient
@inject ISnackbar Snackbar
<PageTitle>دعوت‌شدگان من | کارا بازار سلامت</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<PageHeader Title="دعوت‌شدگان من" BackHref="@RouteConstants.Profile.Index" />
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-4">
افرادی که با شماره موبایل شما به‌عنوان دعوت‌کننده ثبت‌نام کرده‌اند (متفاوت از سازمان فروش).
</MudText>
@if (_loading)
{
<MudStack AlignItems="AlignItems.Center" Class="py-8">
<MudProgressCircular Indeterminate="true" Color="Color.Primary" />
</MudStack>
}
else
{
<MudGrid Spacing="3" Class="mb-4">
<MudItem xs="12" sm="4">
<MudPaper Class="pa-4 rounded-xl" Elevation="1">
<MudText Typo="Typo.caption" Class="mud-text-secondary">کل دعوت‌ها</MudText>
<MudText Typo="Typo.h5">@_stats.TotalInvitees</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="4">
<MudPaper Class="pa-4 rounded-xl" Elevation="1">
<MudText Typo="Typo.caption" Class="mud-text-secondary">فعال در باشگاه</MudText>
<MudText Typo="Typo.h5" Color="Color.Success">@_stats.ClubActiveCount</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="4">
<MudPaper Class="pa-4 rounded-xl" Elevation="1">
<MudText Typo="Typo.caption" Class="mud-text-secondary">غیرفعال باشگاه</MudText>
<MudText Typo="Typo.h5" Color="Color.Warning">@_stats.ClubInactiveCount</MudText>
</MudPaper>
</MudItem>
</MudGrid>
<MudPaper Class="pa-3 rounded-xl mb-3" Elevation="0">
<MudSelect T="string" Label="فیلتر وضعیت باشگاه" Value="_clubFilter" ValueChanged="OnFilterChanged"
Variant="Variant.Outlined" Dense="true" Margin="Margin.Dense">
<MudSelectItem Value="@("ALL")">همه</MudSelectItem>
<MudSelectItem Value="@("ACTIVE")">فعال باشگاه</MudSelectItem>
<MudSelectItem Value="@("INACTIVE")">غیرفعال باشگاه</MudSelectItem>
</MudSelect>
</MudPaper>
@if (!_invitees.Any())
{
<MudAlert Severity="Severity.Info" Dense="true">هنوز کسی با کد دعوت شما ثبت‌نام نکرده است.</MudAlert>
}
else
{
<MudTable Items="_invitees" Hover="true" Dense="true" Breakpoint="Breakpoint.Sm" Class="rounded-xl">
<HeaderContent>
<MudTh>نام</MudTh>
<MudTh>تاریخ ثبت‌نام</MudTh>
<MudTh>باشگاه</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="نام">
@(string.IsNullOrWhiteSpace($"{context.FirstName} {context.LastName}".Trim())
? "—"
: $"{context.FirstName} {context.LastName}".Trim())
</MudTd>
<MudTd DataLabel="تاریخ ثبت‌نام">
@FormatDate(context.RegisteredAt)
</MudTd>
<MudTd DataLabel="باشگاه">
@if (context.IsClubActive)
{
<MudChip T="string" Size="Size.Small" Color="Color.Success" Variant="Variant.Filled">فعال</MudChip>
}
else
{
<MudChip T="string" Size="Size.Small" Color="Color.Default" Variant="Variant.Outlined">غیرفعال</MudChip>
}
</MudTd>
</RowTemplate>
</MudTable>
@if (_totalPages > 1)
{
<MudStack AlignItems="AlignItems.Center" Class="mt-4">
<MudPagination Count="_totalPages" Selected="_page" SelectedChanged="OnPageChanged" Color="Color.Primary" />
</MudStack>
}
}
}
</MudContainer>
@code {
private bool _loading = true;
private string _clubFilter = "ALL";
private int _page = 1;
private const int PageSize = 10;
private int _totalPages;
private InviteeStats _stats = new();
private List<InviteeModel> _invitees = new();
protected override async Task OnInitializedAsync()
{
await LoadAsync();
}
private async Task OnFilterChanged(string value)
{
_clubFilter = value;
_page = 1;
await LoadAsync();
}
private async Task OnPageChanged(int page)
{
_page = page;
await LoadAsync();
}
private async Task LoadAsync()
{
_loading = true;
try
{
var response = await UserClient.GetMyInviteesAsync(new GetMyInviteesRequest
{
PaginationState = new PaginationState { PageNumber = _page, PageSize = PageSize },
ClubStatusFilter = _clubFilter
});
_stats = response.Stats ?? new InviteeStats();
_invitees = response.Invitees.ToList();
_totalPages = (int)(response.MetaData?.TotalPage ?? 0);
if (_totalPages < 1 && _invitees.Count > 0) _totalPages = 1;
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری دعوت‌شدگان: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
private static string FormatDate(Google.Protobuf.WellKnownTypes.Timestamp? ts)
{
if (ts == null) return "—";
return ts.ToDateTime().ToLocalTime().MiladiToJalali();
}
}
@@ -166,23 +166,8 @@
else
{
<MudAlert Severity="MudBlazor.Severity.Warning" Class="rounded-lg">
<MudStack Spacing="2">
<MudText>
سقف شارژ جادویی در این دور پر شده است.
با خرج کردن موجودی و خرید مجدد پکیج، سقف ریست می‌شود.
</MudText>
<MudText Typo="Typo.body2">
اگر برای تکمیل خرید به موجودی بیشتری نیاز دارید، می‌توانید به‌صورت استثنایی کیف اصلی را بدون ضریب (۱:۱) شارژ کنید.
</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Small"
StartIcon="@Icons.Material.Filled.AccountBalanceWallet"
Href="@RouteConstants.Profile.ChargeCreditWallet"
Class="rounded-lg align-self-start">
شارژ عادی بدون ضریب
</MudButton>
</MudStack>
سقف شارژ جادویی در این دور پر شده است.
با خرج کردن موجودی و خرید مجدد پکیج، سقف ریست می‌شود.
</MudAlert>
}
@@ -31,7 +31,7 @@
<MudDivider Class="my-4" />
<MudText Typo="Typo.body2" Color="Color.Secondary">
موجودی کیف پول: @FormatPrice(_walletBalance)
موجودی کیف پول اصلی: @FormatPrice(_walletBalance)
</MudText>
<MudButton Color="Color.Primary" Variant="Variant.Filled" Class="mt-6"
@@ -85,10 +85,6 @@
[SupplyParameterFromQuery(Name = "Status")]
private string? Status { get; set; }
/// <summary>۲۰کارت callback — هم‌معنی Status زرین‌پال</summary>
[SupplyParameterFromQuery(Name = "PaymentStatus")]
private string? PaymentStatus { get; set; }
private bool _isLoading = true;
private bool _isSuccess;
@@ -136,10 +132,6 @@
{
try
{
// ۲۰کارت: PaymentStatus؛ زرین‌پال: Status
if (string.IsNullOrWhiteSpace(Status) && !string.IsNullOrWhiteSpace(PaymentStatus))
Status = PaymentStatus;
// تعیین نوع پرداخت — اگر type نباشد، پیش‌فرض خرید پکیج
var type = PaymentType?.ToLowerInvariant() ?? "package";
+12 -90
View File
@@ -38,97 +38,20 @@
درخواست‌های برداشت
</MudButton>
<!-- دکمه شارژ کیف‌پول اصلی / جادویی -->
@if (_isClubMemberActive)
{
@if (_isMagicWallet)
{
<MudButton Variant="Variant.Filled"
Color="Color.Secondary"
Size="Size.Large"
FullWidth="true"
StartIcon="@Icons.Material.Filled.AutoAwesome"
Href="@RouteConstants.Profile.MagicWallet"
Class="rounded-lg">
شارژ کیف‌پول جادویی
</MudButton>
@if (_magicCeilingFull)
{
<MudAlert Severity="Severity.Warning" Dense="true" Variant="Variant.Outlined">
سقف شارژ جادویی این دور پر است. در صورت نیاز می‌توانید به‌صورت استثنایی بدون ضریب شارژ کنید.
</MudAlert>
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Large"
FullWidth="true"
StartIcon="@Icons.Material.Filled.AccountBalanceWallet"
Href="@RouteConstants.Profile.ChargeCreditWallet"
Class="rounded-lg">
شارژ عادی بدون ضریب
</MudButton>
}
else
{
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined">
کیف پول شما در حالت جادویی است؛ شارژ از مسیر جادویی با ضریب چندبرابر اعمال می‌شود.
</MudAlert>
}
}
else
{
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
FullWidth="true"
StartIcon="@Icons.Material.Filled.AccountBalanceWallet"
Href="@RouteConstants.Profile.ChargeCreditWallet"
Class="rounded-lg">
شارژ کیف‌پول اصلی
</MudButton>
}
}
else
{
<MudPaper Elevation="0" Class="pa-3 rounded-lg" Outlined="true">
<MudStack Spacing="2">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
FullWidth="true"
StartIcon="@Icons.Material.Filled.AccountBalanceWallet"
Disabled="true"
Class="rounded-lg">
شارژ کیف‌پول اصلی
</MudButton>
<MudAlert Severity="Severity.Warning" Dense="true" Variant="Variant.Outlined">
برای شارژ کیف پول اصلی ابتدا باید پکیج را خریداری کرده و قرارداد باشگاه مشتریان را امضا کنید.
</MudAlert>
<MudStack Row="true" Spacing="1" Class="flex-wrap">
@if (!_hasPurchasedPackage)
{
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
Href="@RouteConstants.Package.List"
StartIcon="@Icons.Material.Filled.CardGiftcard">
مشاهده پکیج‌ها
</MudButton>
}
else
{
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
Href="@RouteConstants.Club.Membership"
StartIcon="@Icons.Material.Filled.Handshake">
امضای قرارداد باشگاه
</MudButton>
}
</MudStack>
</MudStack>
</MudPaper>
}
<!-- دکمه شارژ کیف‌پول اصلی -->
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
FullWidth="true"
StartIcon="@Icons.Material.Filled.AccountBalanceWallet"
Href="@RouteConstants.Profile.ChargeCreditWallet"
Class="rounded-lg">
شارژ کیف‌پول اصلی
</MudButton>
@* ── Temporarily hidden: discount-wallet gateway charge (restore when needed)
<!-- دکمه شارژ کیف‌پول اعتباری -->
<MudButton Variant="Variant.Filled"
Color="Color.Info"
<MudButton Variant="Variant.Filled"
Color="Color.Info"
Size="Size.Large"
FullWidth="true"
StartIcon="@Icons.Material.Filled.CreditCard"
@@ -136,7 +59,6 @@
Class="rounded-lg">
شارژ کیف‌پول اعتباری
</MudButton>
── *@
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-2">تراکنش‌ها و مسیرهای شارژ</MudText>
@@ -6,43 +6,13 @@ namespace FrontOffice.Main.Pages.Profile;
public partial class Wallet : ComponentBase
{
[Inject] private AuthService AuthService { get; set; } = default!;
private (string Credit, string Discount, string Network) _balances = ("-", "-", "-");
private List<WalletTransaction> _txs = new();
private string? _filterReferenceId;
private string _filterType = "all";
private bool _isClubMemberActive;
private bool _hasPurchasedPackage;
private bool _isMagicWallet;
private bool _magicCeilingFull;
protected override async Task OnInitializedAsync()
{
try
{
var userInfo = await AuthService.GetUserAuthInfo();
_isClubMemberActive = userInfo.IsClubMemberActive;
_hasPurchasedPackage = userInfo.HasPurchasedPackage;
}
catch
{
_isClubMemberActive = false;
_hasPurchasedPackage = false;
}
try
{
var magicStatus = await WalletService.GetMagicWalletStatusAsync();
_isMagicWallet = magicStatus.WalletMode == 1;
_magicCeilingFull = _isMagicWallet && magicStatus.MagicRemainingDeposit <= 0;
}
catch
{
_isMagicWallet = false;
_magicCeilingFull = false;
}
var b = await WalletService.GetBalancesAsync();
_balances = (FormatPrice(b.CreditBalance), FormatPrice(b.DiscountBalance), FormatPrice(b.NetworkBalance));
_txs = await WalletService.GetTransactionsAsync();
@@ -58,19 +58,8 @@ public partial class WithdrawalRequests : ComponentBase
try
{
_isSubmittingWithdrawal = true;
string? normalizedIban = null;
if (_withdrawMethod == WithdrawalMethodClient.Cash)
{
normalizedIban = NormalizeIranianIban(_withdrawIban);
if (normalizedIban is null)
{
Snackbar.Add("فرمت شماره شبا معتبر نیست. باید IR و ۲۴ رقم باشد (خط تیره اختیاری است).", Severity.Warning);
return;
}
}
await WalletService.RequestWithdrawalAsync(_selectedPayout.Id, _withdrawMethod, normalizedIban);
_withdrawIban=_withdrawIban!.Trim().ToUpper().Replace("IR", "").Replace(" ", "");
await WalletService.RequestWithdrawalAsync(_selectedPayout.Id, _withdrawMethod, "IR"+_withdrawIban);
Snackbar.Add("درخواست برداشت ثبت شد.", Severity.Success);
// بروزرسانی لیست
@@ -136,26 +125,4 @@ public partial class WithdrawalRequests : ComponentBase
1 => "الماس",
_ => "-"
};
/// <summary>
/// فاصله و خط تیره را حذف می‌کند و شبا را به شکل IR + ۲۴ رقم برمی‌گرداند.
/// </summary>
private static string? NormalizeIranianIban(string? iban)
{
if (string.IsNullOrWhiteSpace(iban))
return null;
var normalized = iban.Trim().ToUpperInvariant()
.Replace(" ", "", StringComparison.Ordinal)
.Replace("-", "", StringComparison.Ordinal);
if (normalized.StartsWith("IR", StringComparison.Ordinal))
normalized = normalized[2..];
normalized = "IR" + normalized;
return System.Text.RegularExpressions.Regex.IsMatch(normalized, @"^IR\d{24}$")
? normalized
: null;
}
}
@@ -319,17 +319,17 @@ public partial class RegisterWizard
$"<p><span class='field-label'>شماره ملی:</span> {_model.NationalCode?.PersianToEnglish()}</p>" +
$"<p><span class='field-label'>شماره تماس:</span> {_userAuthInfo.MobileNumber?.PersianToEnglish()}</p></div>" +
"<p>کارابازار و کاربر که در این قرارداد جداگانه &laquo;طرف&raquo; و مجموعاً &laquo;طرفین&raquo; نامیده می‌شوند، با علم و آگاهی کامل، مفاد زیر را پذیرفته و امضا می‌نمایند.</p></div><div><p class='section-title'>ماده ۱ - موضوع قرارداد</p>" +
"<p>موضوع این قرارداد، تعیین حدود و چارچوب تعهدات و سلب مسئولیت کارابازار در قبال استفاده کاربر ازخدمات، باشگاه مشتریان، مزایا، طرح‌های همکاری، سازمان معرفی، خرید از فروشگاه‌های طرف‌قراردادو هرگونه امکانات و تسهیلات مرتبط است که حسب مورد توسط کارابازار یا از طریق پلتفرم‌ها و شرکت‌هایهمکار در اختیار کاربر قرار می‌گیرد، بدون اینکه جزئیات مدل کسب‌وکار، شیوه تأمین منابع مالی،سازمان همکاری یا ساختار داخلی آن در این قرارداد ذکر یا افشا شود.</p>" +
"<p>موضوع این قرارداد، تعیین حدود و چارچوب تعهدات و سلب مسئولیت کارابازار در قبال استفاده کاربر ازخدمات، باشگاه مشتریان، مزایا، طرح‌های همکاری، شبکه معرفی، خرید از فروشگاه‌های طرف‌قراردادو هرگونه امکانات و تسهیلات مرتبط است که حسب مورد توسط کارابازار یا از طریق پلتفرم‌ها و شرکت‌هایهمکار در اختیار کاربر قرار می‌گیرد، بدون اینکه جزئیات مدل کسب‌وکار، شیوه تأمین منابع مالی،شبکه همکاری یا ساختار داخلی آن در این قرارداد ذکر یا افشا شود.</p>" +
"<p class='section-title'>ماده ۲ - اقرار و آگاهی کاربر</p><p>۲-۱. کاربر اقرار می‌نماید که کلیه اطلاعات کلی لازم در خصوص نحوه فعالیت، مزایا، ریسک‌های احتمالی," +
"سازوکار کلی استفاده از خدمات، و چارچوب کلی همکاری را از مراجع رسمی معرفی‌شده توسط کارابازاردریافت کرده، سؤالات خود را مطرح نموده و پاسخ مناسب را دریافت نموده است.</p><p>۲-۲. کاربر با امضای این قرارداد اقرار می‌نماید که:</p><ul><li>با اراده آزاد و بدون هرگونه اجبار، اکراه، فریب یا سوءاستفاده از ناآگاهی وارد این همکاری شده است.</li><li>از احتمال وجود ریسک‌های مالی، اعتباری، قانونی و شبکه‌ای مرتبط با فعالیت خود آگاه بوده و آن را می‌پذیرد.</li>" +
"<li>هرگونه تصمیم برای استفاده از تسهیلات، مزایا، خدمات باشگاه مشتریان، فعالیت در سازمان معرفی، خرید از فروشگاه‌ها و جذب افراد جدید، تصمیمی شخصی و آگاهانه است و به‌عنوان تعهد یا ضمانت بازده مشخص از سوی کارابازار تلقی نمی‌شود.</li></ul>" +
"<li>هرگونه تصمیم برای استفاده از تسهیلات، مزایا، خدمات باشگاه مشتریان، فعالیت در شبکه معرفی، خرید از فروشگاه‌ها و جذب افراد جدید، تصمیمی شخصی و آگاهانه است و به‌عنوان تعهد یا ضمانت بازده مشخص از سوی کارابازار تلقی نمی‌شود.</li></ul>" +
"<p class='section-title'>ماده ۳ - سلب مسئولیت کارابازار</p><p>۳-۱. کاربر صراحتاً می‌پذیرد که کارابازار صرفاً در چارچوب قوانین و مقررات حاکم و مطابق قراردادها و تفاهم‌نامه‌های خود با شرکای تجاری و پلتفرم‌های همکار، خدمات و امکانات مشخصی را در اختیار کاربر قرار می‌دهد و هیچ‌گونه تعهدی نسبت به:</p><ul><li>تحقق سود یا بازده مالی مشخص،</li><li>ثبات ارزش هرگونه اعتبار، امتیاز، یا منافع غیرنقدی که ممکن است در قالب طرح‌ها به کاربر تخصیص یابد،</li>" +
"<li>عملکرد اشخاص ثالث (از جمله پلتفرم‌ها، فروشگاه‌ها، شرکت‌های بیمه، ارائه‌دهندگان خدمات سلامت و هر شخص حقیقی یا حقوقی همکار)،</li><li>تصمیمات و اقدامات کاربر در جذب افراد جدید و ایجاد زیرمجموعه،</li></ul><p>نداشته و نخواهد داشت مگر در حدودی که به‌صراحت در قرارداد یا قوانین آمره بر عهده کارابازار گذاشته شده است.</p><p>" +
"۳-۲. هرگونه خسارت، دعوی، تهدید، مطالبه، ادعا، شکایت یا پیگیری حقوقی که ناشی از تصمیمات شخصی کاربر،نحوه استفاده وی از تسهیلات و مزایا، جذب دیگران، خرید کالا و خدمات، یا تعاملات او با اشخاص ثالث باشد،مستقیماً متوجه خود کاربر است و کارابازار نسبت به آن هیچ‌گونه مسئولیت مدنی، کیفری، مالی یا قراردادی ندارد," +
"مگر در مواردی که طبق قوانین آمره، مسئولیت صریح و غیرقابل‌اسقاط بر عهده کارابازار قرار گرفته باشد.</p><p>۳-۳. کاربر می‌پذیرد که هیچ‌گونه ادعایی مبنی بر فریب، اغفال، ارائه اطلاعات نادرست یا وعده‌های خارج ازچهارچوب رسمی از سوی کارابازار یا نمایندگان رسمی آن، پس از امضای این قرارداد، از وی مسموع نخواهد بود،مگر آنکه طبق رأی قطعی مرجع قضایی صالح، خلاف آن ثابت شود.</p><p class='section-title'>ماده ۴ - مسئولیت کاربر در قبال تهدید، ادعا و شکایت</p>" +
"<p>۴-۱. کاربر متعهد است در صورت طرح هرگونه تهدید، ادعا، مطالبه، شکایت یا پیگیری حقوقی از سوی اشخاص ثالث(از جمله افراد جذب‌شده توسط کاربر، زیرمجموعه‌ها، خریداران معرفی‌شده یا سایر اشخاص)، که ناشی از عملکردشخصی کاربر، معرفی‌های وی، شیوه تبلیغ، وعده‌های خارج از چارچوب رسمی کارابازار، یا نقض قوانین و مقرراتباشد، خود رأساً پاسخگو باشد و کارابازار را از هرگونه مسئولیت و هزینه احتمالی در این خصوص مصون بدارد.</p><p>۴-۲. کاربر می‌پذیرد که:</p><ul><li>هرگونه تهدید یا ادعای بی‌جا، غیرمنطقی، غیرمستند یا مغایر با مفاد این قرارداد، متوجه شخص کاربر است.</li>" +
"<li>در صورت وارد آمدن خسارت مادی یا معنوی به کارابازار ناشی از چنین تهدیدات یا ادعاهایی، کارابازار حق دارد نسبت به مطالبه خسارت از کاربر، طبق قوانین و از طریق مراجع صالح اقدام نماید.</li></ul>" +
"<p class='section-title'>ماده ۵ - محرمانگی و عدم افشای جزئیات کسب‌وکار</p><p>۵-۱. کاربر می‌پذیرد که جزئیات فنی، مالی، ساختار سازمان، شیوه توزیع مزایا، قراردادهای داخلی کارابازار باشرکای تجاری و پلتفرم‌های همکار، اطلاعات محرمانه تجاری محسوب شده و خارج از این قرارداد است و افشای آن‌هاتوسط کاربر بدون مجوز کتبی کارابازار، مجاز نمی‌باشد.</p><p>۵-۲. کاربر حق استناد به عدم درج جزئیات ساختار کسب‌وکار در این قرارداد را به‌عنوان دلیل بطلان، فسخ یاادعای فریب علیه کارابازار از خود سلب می‌نماید.</p>" +
"<p class='section-title'>ماده ۵ - محرمانگی و عدم افشای جزئیات کسب‌وکار</p><p>۵-۱. کاربر می‌پذیرد که جزئیات فنی، مالی، ساختار شبکه، شیوه توزیع مزایا، قراردادهای داخلی کارابازار باشرکای تجاری و پلتفرم‌های همکار، اطلاعات محرمانه تجاری محسوب شده و خارج از این قرارداد است و افشای آن‌هاتوسط کاربر بدون مجوز کتبی کارابازار، مجاز نمی‌باشد.</p><p>۵-۲. کاربر حق استناد به عدم درج جزئیات ساختار کسب‌وکار در این قرارداد را به‌عنوان دلیل بطلان، فسخ یاادعای فریب علیه کارابازار از خود سلب می‌نماید.</p>" +
"<p class='section-title'>ماده ۶ - کد تأیید یکبارمصرف (OTP) و شناسه قرارداد</p><p>۶-۱. به منظور تأیید هویت کاربر و جایگزین کردن امضا در محیط غیرحضوری، برای این قرارداد یک شناسه یکتا(GUID) تولید شده و از طریق سامانه کارابازار، کد تأیید یکبارمصرف (OTP) مرتبط با آن برای کاربر ارسالمی‌گردد.</p>" +
"<p>۶-۲. کاربر با وارد کردن صحیح کد تأیید (OTP) در سامانه، اقرار می‌نماید که:</p><ul><li>مالک شماره تماس ثبت‌شده در این قرارداد است یا با اجازه قانونی مالک از آن استفاده می‌کند،</li><li>از محتوای قرارداد به‌طور کامل آگاه است و آن را بدون ابهام و با رضایت کامل می‌پذیرد،</li><li>شناسه یکتا و OTP ارسال‌شده، معادل امضای الکترونیکی معتبر وی بر روی این قرارداد است.</li></ul>" +
"<p class='section-title'>ماده ۷ - مدت، اصلاح و حاکمیت قانون</p>" +
@@ -21,11 +21,9 @@
else if (_addresses.Count == 0)
{
<MudAlert Severity="Severity.Warning">
هیچ آدرسی ثبت نشده است. می‌توانید همین‌جا آدرس جدید اضافه کنید.
هیچ آدرسی ثبت نشده است. لطفاً از بخش پروفایل آدرس خود را اضافه کنید.
</MudAlert>
<MudButton Class="mt-2" Variant="Variant.Outlined" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="OpenAddAddressDialog">افزودن آدرس</MudButton>
<MudButton Class="mt-2" Variant="Variant.Outlined" Href="/profile/addresses">افزودن آدرس</MudButton>
}
else
{
@@ -42,25 +40,13 @@
<MudText Typo="Typo.subtitle2">@address.Title</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@address.Address</MudText>
</MudStack>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
@if (address.IsDefault)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Outlined" Size="Size.Small">پیش‌فرض</MudChip>
}
<span @onclick:stopPropagation="true">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Size="Size.Small"
Color="Color.Primary"
aria-label="ویرایش آدرس"
OnClick="@(() => OpenEditAddressDialog(address))" />
</span>
</MudStack>
@if (address.IsDefault)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Outlined">پیش‌فرض</MudChip>
}
</MudStack>
</MudPaper>
}
<MudButton Class="mt-2" Variant="Variant.Text" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="OpenAddAddressDialog">افزودن آدرس جدید</MudButton>
</MudStack>
}
</MudPaper>
@@ -1,6 +1,5 @@
using CMSMicroservice.Protobuf.Protos.UserAddress;
using CMSMicroservice.Protobuf.Protos.UserOrder;
using FrontOffice.Main.Pages.Profile.Components;
using FrontOffice.Main.Shared;
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
@@ -36,11 +35,7 @@ public partial class CheckoutSummary : ComponentBase
await AuthDialogService.ShowAuthDialogAsync();
}
await Cart.EnsureInitializedAsync();
var userInfo = await AuthService.GetUserAuthInfo();
if (userInfo.HasAddress)
await LoadAddresses();
else
_addresses = new();
await LoadAddresses();
await LoadWalletBalance();
}
@@ -73,14 +68,12 @@ public partial class CheckoutSummary : ComponentBase
else
{
_addresses = new();
_selectedAddress = null;
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری آدرس‌ها: {ex.Message}", Severity.Error);
_addresses = new();
_selectedAddress = null;
}
finally
{
@@ -89,30 +82,6 @@ public partial class CheckoutSummary : ComponentBase
}
}
private async Task OpenAddAddressDialog()
{
var dialog = await DialogService.ShowAsync<AddAddressDialog>("افزودن آدرس جدید");
var result = await dialog.Result;
if (result is { Canceled: false })
{
await AuthService.RefreshTokenAsync();
await LoadAddresses();
}
}
private async Task OpenEditAddressDialog(CustomerAddressModel address)
{
var dialog = await DialogService.ShowAsync<EditAddressDialog>("ویرایش آدرس", new DialogParameters<EditAddressDialog>
{
{ x => x.Model, address }
});
var result = await dialog.Result;
if (result is { Canceled: false })
{
await LoadAddresses();
}
}
private async Task PlaceOrder()
{
if (!CanPlaceOrder || _selectedAddress is null)
@@ -156,42 +125,11 @@ public partial class CheckoutSummary : ComponentBase
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 }
{ x => x.ShortfallAmount, shortfall }
};
var dialog = await DialogService.ShowAsync<InsufficientCreditDialog>(
@@ -205,7 +143,7 @@ public partial class CheckoutSummary : ComponentBase
});
var result = await dialog.Result;
if (allowCharge && result is { Canceled: false } && result.Data is long chargeShortfall)
if (result is { Canceled: false } && result.Data is long chargeShortfall)
{
var chargeAmount = CreditChargeNavigation.NormalizeChargeAmount(chargeShortfall);
Navigation.NavigateTo(CreditChargeNavigation.BuildChargeUrl(
@@ -112,6 +112,30 @@ else
</MudStack>
}
</MudStack>
@* نمایش PV سفارش *@
@if (_pvData != null && _pvData.TotalPv > 0)
{
<MudDivider Class="my-2" />
<MudStack Spacing="1" Class="pa-2" Style="background-color: var(--mud-palette-background-grey); border-radius: 8px;">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.TrendingUp" Color="Color.Info" Size="Size.Small" />
<MudText Typo="Typo.subtitle2" Color="Color.Info">امتیاز فروش (PV)</MudText>
</MudStack>
@foreach (var product in _pvData.Products)
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.caption" Class="mud-text-secondary">@product.ProductTitle × @product.Quantity</MudText>
<MudText Typo="Typo.caption">@string.Format("{0:N0}", product.TotalPv) PV</MudText>
</MudStack>
}
<MudDivider Class="my-1" />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="fw-bold">جمع PV:</MudText>
<MudText Typo="Typo.body2" Color="Color.Info" Class="fw-bold">@string.Format("{0:N0}", _pvData.TotalPv) PV</MudText>
</MudStack>
</MudStack>
}
</MudStack>
</MudPaper>
</MudContainer>
@@ -12,12 +12,17 @@ public partial class OrderDetail : ComponentBase
[Parameter] public long id { get; set; }
private GetUserOrderResponse? _order;
private CalculateOrderPVResponse? _pvData;
private bool _loading;
protected override async Task OnParametersSetAsync()
{
_loading = true;
_order = await OrderService.GetOrderAsync(id);
if (_order != null)
{
_pvData = await OrderService.CalculateOrderPVAsync(id);
}
_loading = false;
}
@@ -47,3 +52,4 @@ public partial class OrderDetail : ComponentBase
private static string GetProductImageUrl(string? imageUrl)
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/');
}
@@ -46,19 +46,12 @@ public partial class OrderTracking : ComponentBase
private void BuildTrackingSteps()
{
if (_order is null) return;
// Build tracking steps based on PaymentStatus
var isPaid = _order.PaymentStatus == Messages.PaymentStatus.Success;
// Domain/public_messages: None=0, Pending=1, InTransit=2, Delivered=3, Returned=4, Cancelled=5, ReadyForOfficePickup=6
var delivery = (int)_order.DeliveryStatus;
var isCancelled = delivery == 5;
var isReturned = delivery == 4;
var isOfficePickup = delivery == 6;
var isInTransit = delivery == 2;
var isDelivered = delivery == 3;
var isPendingDelivery = delivery is 0 or 1;
_trackingSteps =
[
_trackingSteps = new List<TrackingStep>
{
new()
{
Title = "ثبت سفارش",
@@ -68,77 +61,45 @@ public partial class OrderTracking : ComponentBase
},
new()
{
Title = "پرداخت",
Description = isPaid ? "پرداخت شما تایید شد" : "منتظر تایید پرداخت هستیم",
Title = "در انتظار پرداخت",
Description = "منتظر تایید پرداخت هستیم",
IsCompleted = isPaid,
IsCurrent = !isPaid && !isCancelled,
IsCurrent = !isPaid,
Date = isPaid ? FormatDate(_order.PaymentDate) : ""
},
new()
{
Title = "تایید پرداخت",
Description = "پرداخت شما تایید شد",
IsCompleted = isPaid,
IsCurrent = false,
Date = isPaid ? FormatDate(_order.PaymentDate) : ""
},
new()
{
Title = "در حال پردازش",
Description = "سفارش شما در حال آماده‌سازی است",
IsCompleted = false,
IsCurrent = isPaid,
Date = ""
},
new()
{
Title = "ارسال شده",
Description = "سفارش شما به پست تحویل داده شد",
IsCompleted = false,
IsCurrent = false,
Date = ""
},
new()
{
Title = "تحویل داده شده",
Description = "سفارش به دست شما رسید",
IsCompleted = false,
IsCurrent = false,
Date = ""
}
];
if (isCancelled)
{
_trackingSteps.Add(new()
{
Title = "لغو شده",
Description = "ارسال این سفارش لغو شده است",
IsCompleted = true,
IsCurrent = true,
Date = ""
});
return;
}
if (isReturned)
{
_trackingSteps.Add(new()
{
Title = "مرجوع شده",
Description = "سفارش مرجوع شده است",
IsCompleted = true,
IsCurrent = true,
Date = ""
});
return;
}
if (isOfficePickup)
{
_trackingSteps.Add(new()
{
Title = "آماده تحویل در دفتر",
Description = "سفارش برای تحویل حضوری در دفتر آماده است",
IsCompleted = true,
IsCurrent = true,
Date = ""
});
return;
}
_trackingSteps.Add(new()
{
Title = "در حال آماده‌سازی",
Description = "سفارش شما در حال آماده‌سازی است",
IsCompleted = isInTransit || isDelivered,
IsCurrent = isPaid && isPendingDelivery,
Date = ""
});
_trackingSteps.Add(new()
{
Title = "ارسال شده",
Description = "سفارش شما به پست تحویل داده شد",
IsCompleted = isDelivered,
IsCurrent = isInTransit,
Date = ""
});
_trackingSteps.Add(new()
{
Title = "تحویل داده شده",
Description = "سفارش به دست شما رسید",
IsCompleted = isDelivered,
IsCurrent = false,
Date = ""
});
};
}
private static string FormatDate(Google.Protobuf.WellKnownTypes.Timestamp? timestamp)
+72 -94
View File
@@ -1,102 +1,80 @@
@attribute [Route(RouteConstants.Store.Orders)]
@* Injection is handled in code-behind *@
<PageTitle>سفارشات من</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="سفارشات من" BackHref="@RouteConstants.Store.Products" />
@if (_loading)
{
<LoadingState />
}
else if (_orders.Count == 0)
{
<MudAlert Severity="Severity.Info">هنوز سفارشی ثبت نشده است.</MudAlert>
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Store"
Href="@RouteConstants.Store.Products">
مشاهده فروشگاه
</MudButton>
}
else
{
<!-- Desktop Table -->
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudPaper Elevation="1" Class="pa-4 rounded-lg">
<MudTable Items="_orders">
<HeaderContent>
<MudTh>شماره سفارش</MudTh>
<MudTh>تعداد اقلام</MudTh>
<MudTh>مبلغ کل</MudTh>
<MudTh>وضعیت پرداخت</MudTh>
<MudTh>وضعیت ارسال</MudTh>
<MudTh>تاریخ</MudTh>
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Id</MudTd>
<MudTd>@GetItemsCount(context)</MudTd>
<MudTd>@FormatPrice(GetTotalAmount(context))</MudTd>
<MudTd>
<MudChip T="string" Size="Size.Small"
Color="@GetPaymentStatusColor(context.PaymentStatus)"
Variant="Variant.Filled">
@GetPaymentStatusText(context.PaymentStatus)
</MudChip>
</MudTd>
<MudTd>
<MudChip T="string" Size="Size.Small"
Color="@GetDeliveryStatusColor((int)context.DeliveryStatus)"
Variant="Variant.Outlined">
@GetDeliveryStatusText((int)context.DeliveryStatus)
</MudChip>
</MudTd>
<MudTd>@FormatDate(context)</MudTd>
<MudTd>
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
OnClick="() => ViewOrder(context.Id)">
جزئیات
</MudButton>
</MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
</MudHidden>
<!-- Mobile Cards -->
<MudHidden Breakpoint="Breakpoint.MdAndUp">
<MudStack Spacing="2">
@foreach (var order in _orders)
<PageHeader Title="سفارشات من" BackHref="@RouteConstants.Store.Products" />
@if (_loading)
{
<LoadingState />
}
else if (_orders.Count == 0)
{
<MudAlert Severity="Severity.Info">هنوز سفارشی ثبت نکرده‌اید.</MudAlert>
}
else
{
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudTable Items="_orders" Dense="true">
<HeaderContent>
<MudTh>شناسه</MudTh>
<MudTh>تاریخ</MudTh>
<MudTh>وضعیت</MudTh>
<MudTh>مبلغ</MudTh>
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Id</MudTd>
<MudTd>
@if (context.PaymentDate != null)
{
<MudPaper Class="pa-3 rounded-lg cursor-pointer" Outlined="true"
@onclick="() => ViewOrder(order.Id)">
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.subtitle2">سفارش #@order.Id</MudText>
<MudChip T="string" Size="Size.Small"
Color="@GetPaymentStatusColor(order.PaymentStatus)"
Variant="Variant.Filled">
@GetPaymentStatusText(order.PaymentStatus)
</MudChip>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.caption" Class="mud-text-secondary">@FormatDate(order)</MudText>
<MudChip T="string" Size="Size.Small"
Color="@GetDeliveryStatusColor((int)order.DeliveryStatus)"
Variant="Variant.Outlined">
@GetDeliveryStatusText((int)order.DeliveryStatus)
</MudChip>
</MudStack>
<MudDivider />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">@GetItemsCount(order) قلم</MudText>
<MudText Typo="Typo.body2" Class="fw-semibold">@FormatPrice(GetTotalAmount(order)) تومان</MudText>
</MudStack>
</MudStack>
</MudPaper>
@context.PaymentDate.ToDateTime().MiladiToJalaliWithTime()
}
</MudStack>
</MudHidden>
}
</MudStack>
else
{
<text>در انتظار پرداخت</text>
}
</MudTd>
<MudTd>@GetStatusText(context.PaymentStatus)</MudTd>
<MudTd>@FormatPrice(context.FactorDetails.Sum(s=>(s.UnitPrice ?? 0) * (s.Count ?? 0)))</MudTd>
<MudTd>
<MudButton Variant="Variant.Text" Href="@($"{RouteConstants.Store.OrderDetail}{context.Id}")" StartIcon="@Icons.Material.Filled.Receipt">جزئیات</MudButton>
</MudTd>
</RowTemplate>
</MudTable>
</MudHidden>
<MudHidden Breakpoint="Breakpoint.MdAndUp">
<MudStack Spacing="2">
@foreach (var o in _orders)
{
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText>سفارش #@o.Id</MudText>
<MudText Class="mud-text-secondary">
@if (o.PaymentDate != null)
{
@o.PaymentDate.ToDateTime().MiladiToJalaliWithTime()
}
else
{
<text>در انتظار پرداخت</text>
}
</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText>وضعیت: @GetStatusText(o.PaymentStatus)</MudText>
<MudText Color="Color.Primary">@FormatPrice(o.FactorDetails.Sum(s=>(s.UnitPrice ?? 0) * (s.Count ?? 0)))</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.FlexEnd">
<MudButton Size="Size.Small" Variant="Variant.Outlined" Href="@($"{RouteConstants.Store.OrderDetail}{o.Id}")" StartIcon="@Icons.Material.Filled.Receipt">جزئیات</MudButton>
</MudStack>
</MudStack>
</MudPaper>
}
</MudStack>
</MudHidden>
}
</MudContainer>
@@ -1,5 +1,5 @@
using System;
using CMSMicroservice.Protobuf.Protos.UserOrder;
using DateTimeConverterCL;
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using Messages = CMSMicroservice.Protobuf.Protos;
@@ -10,6 +10,7 @@ namespace FrontOffice.Main.Pages.Store;
public partial class Orders : ComponentBase
{
[Inject] private OrderService OrderService { get; set; } = default!;
[Inject] private VATService VAT { get; set; } = default!;
private List<GetUserOrderResponse> _orders = new();
private bool _loading;
@@ -32,81 +33,28 @@ public partial class Orders : ComponentBase
}
}
private void ViewOrder(long orderId)
protected override async Task OnAfterRenderAsync(bool firstRender)
{
Navigation.NavigateTo($"{RouteConstants.Store.OrderDetail}{orderId}");
}
private static string FormatPrice(long price) => $"{price:N0}";
private static string FormatDate(GetUserOrderResponse order)
{
try
if (firstRender)
{
var timestamp = order.PaymentDate;
if (timestamp is null)
return "در انتظار پرداخت";
return timestamp.ToDateTime().ToLocalTime().MiladiToJalaliWithTime();
// بارگذاری نرخ VAT
await VAT.LoadAsync();
}
catch
await base.OnAfterRenderAsync(firstRender);
}
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
private string GetStatusText(Messages.PaymentStatus contextPaymentStatus)
{
return contextPaymentStatus switch
{
return "—";
}
Messages.PaymentStatus.Pending => "در انتظار پرداخت",
Messages.PaymentStatus.Success => "پرداخت شده",
Messages.PaymentStatus.Reject => "پرداخت ناموفق",
_ => "نامشخص",
};
}
private static int GetItemsCount(GetUserOrderResponse order) =>
order.FactorDetails.Sum(f => f.Count ?? 0);
private static long GetTotalAmount(GetUserOrderResponse order)
{
if (order.VatInfo is not null && order.VatInfo.TotalAmount > 0)
return order.VatInfo.TotalAmount;
if (order.Amount > 0)
return order.Amount;
return order.FactorDetails.Sum(f => (f.UnitPrice ?? 0) * (f.Count ?? 0));
}
private static string GetPaymentStatusText(Messages.PaymentStatus status) => status switch
{
Messages.PaymentStatus.Pending => "در انتظار پرداخت",
Messages.PaymentStatus.Success => "پرداخت شده",
Messages.PaymentStatus.Reject => "پرداخت ناموفق",
_ => "نامشخص"
};
private static Color GetPaymentStatusColor(Messages.PaymentStatus status) => status switch
{
Messages.PaymentStatus.Pending => Color.Warning,
Messages.PaymentStatus.Success => Color.Success,
Messages.PaymentStatus.Reject => Color.Error,
_ => Color.Default
};
// Domain/public_messages: None=0, Pending=1, InTransit=2, Delivered=3, Returned=4, Cancelled=5, ReadyForOfficePickup=6
private static string GetDeliveryStatusText(int status) => status switch
{
0 => "بدون ارسال",
1 => "در انتظار ارسال",
2 => "ارسال شده",
3 => "تحویل داده شده",
4 => "مرجوع شده",
5 => "لغو شده",
6 => "آماده تحویل در دفتر",
_ => "نامشخص"
};
private static Color GetDeliveryStatusColor(int status) => status switch
{
0 => Color.Default,
1 => Color.Warning,
2 => Color.Primary,
3 => Color.Success,
4 => Color.Error,
5 => Color.Dark,
6 => Color.Primary,
_ => Color.Default
};
}
@@ -118,8 +118,7 @@
@foreach (var p in _products)
{
<MudItem xs="6" sm="6" md="3"
onclick="@(() => OpenProduct(p.Id))">
<div id="@($"shop-product-{p.Id}")" class="h-100">
onclick="@(() => Navigation.NavigateTo($"{RouteConstants.Store.ProductDetail}{p.Id}"))">
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden"
Style="cursor:pointer;">
@@ -165,7 +164,6 @@
</MudButton>
</MudCardActions>
</MudCard>
</div>
</MudItem>
}
</MudGrid>
@@ -2,11 +2,19 @@ using System.Linq;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;
using Microsoft.AspNetCore.WebUtilities;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.Store;
public enum ProductSortOption
{
PriceDesc, // گران‌ترین (پیش‌فرض)
PriceAsc, // ارزان‌ترین
Newest, // جدیدترین
Title // الفبایی
}
public partial class Products : ComponentBase, IDisposable
{
[Inject] private ProductService ProductService { get; set; } = default!;
@@ -15,7 +23,6 @@ public partial class Products : ComponentBase, IDisposable
[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;
@@ -28,9 +35,8 @@ public partial class Products : ComponentBase, IDisposable
private List<Product> _products = new();
private long? _activeCategoryId;
private string? _activeCategoryTitle;
private ProductSortOption _sortOption = ProductSortOption.PriceDesc;
private bool _ignoreNextLocationChange;
private bool _pendingScrollRestore;
private ProductSortOption _sortOption = ProductSortOption.PriceDesc; // پیش‌فرض: گران‌ترین
protected override async Task OnInitializedAsync()
{
@@ -38,132 +44,80 @@ public partial class Products : ComponentBase, IDisposable
await Cart.EnsureInitializedAsync();
Cart.OnChange += StateHasChanged;
Navigation.LocationChanged += HandleLocationChanged;
ApplyStateFromUri();
await LoadPages(_currentPage);
_pendingScrollRestore = true;
await LoadInitial();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
await VAT.LoadAsync();
if (_pendingScrollRestore && !_loading && _products.Count > 0)
{
_pendingScrollRestore = false;
var payload = await ShopListScrollRestore.TakeAsync(Js, ShopListScrollRestore.StoreKey);
if (payload is not null)
await ShopListScrollRestore.RestoreAsync(Js, payload);
// بارگذاری نرخ VAT
await VAT.LoadAsync();
}
await base.OnAfterRenderAsync(firstRender);
}
private void ApplyStateFromUri()
{
var state = ShopListQueryState.Parse(Navigation.ToAbsoluteUri(Navigation.Uri));
_query = state.Query;
_sortOption = ShopListQueryState.ParseSortOption(state.Sort);
_activeCategoryId = state.CategoryId;
_currentPage = state.Pages;
}
private ShopListQueryState CaptureState() => new()
{
Query = _query,
Sort = ShopListQueryState.ToSortKey(_sortOption),
CategoryId = _activeCategoryId,
Pages = Math.Max(1, _currentPage)
};
private void SyncUrl()
{
var target = CaptureState().ToRelativeUrl(RouteConstants.Store.Products);
var currentPathAndQuery = Navigation.ToAbsoluteUri(Navigation.Uri).PathAndQuery;
if (string.Equals(currentPathAndQuery, target, StringComparison.OrdinalIgnoreCase)
|| string.Equals(currentPathAndQuery, target + "/", StringComparison.OrdinalIgnoreCase))
return;
_ignoreNextLocationChange = true;
Navigation.NavigateTo(target, replace: true);
}
private async Task LoadPages(int pagesToLoad)
private async Task LoadInitial()
{
_loading = true;
_currentPage = 1;
_products.Clear();
_hasMore = true;
pagesToLoad = Math.Max(1, pagesToLoad);
var sortBy = ShopListQueryState.ToApiSortBy(_sortOption);
for (var page = 1; page <= pagesToLoad; page++)
{
var result = await ProductService.GetProductsPagedAsync(
_query, _activeCategoryId, sortBy, page, PageSize);
if (page == 1)
{
_products = result.Products;
_totalCount = result.TotalCount;
}
else
{
_products.AddRange(result.Products);
}
_currentPage = page;
_hasMore = result.HasNext;
if (!_hasMore)
break;
}
UpdateCategoryFilterFromUri();
var sortBy = GetSortByValue();
var result = await ProductService.GetProductsPagedAsync(_query, _activeCategoryId, sortBy, _currentPage, PageSize);
_products = result.Products;
_hasMore = result.HasNext;
_totalCount = result.TotalCount;
_activeCategoryTitle = _activeCategoryId is { } categoryId
? (await CategoryService.GetByIdAsync(categoryId))?.Title
: null;
_loading = false;
}
private async Task ReloadFromFilters()
{
_currentPage = 1;
await LoadPages(1);
SyncUrl();
StateHasChanged();
}
private async Task LoadMore()
{
if (_loadingMore || !_hasMore) return;
_loadingMore = true;
StateHasChanged();
_currentPage++;
var sortBy = ShopListQueryState.ToApiSortBy(_sortOption);
var result = await ProductService.GetProductsPagedAsync(
_query, _activeCategoryId, sortBy, _currentPage, PageSize);
var sortBy = GetSortByValue();
var result = await ProductService.GetProductsPagedAsync(_query, _activeCategoryId, sortBy, _currentPage, PageSize);
_products.AddRange(result.Products);
_hasMore = result.HasNext;
SyncUrl();
_loadingMore = false;
StateHasChanged();
}
private async Task OnSortChanged() => await ReloadFromFilters();
private string GetSortByValue()
{
return _sortOption switch
{
ProductSortOption.PriceDesc => "price desc",
ProductSortOption.PriceAsc => "price asc",
ProductSortOption.Newest => "id desc",
ProductSortOption.Title => "title asc",
_ => "price desc"
};
}
private async Task OnQueryChanged(KeyboardEventArgs _) => await ReloadFromFilters();
private async Task OnSortChanged()
{
await LoadInitial();
}
private async Task OnQueryChanged(KeyboardEventArgs _)
{
await LoadInitial();
}
private async Task AddToCart(Product p)
{
await GuestGate.RunAsync(() => Cart.Add(p, 1));
}
private async Task OpenProduct(long productId)
{
await ShopListScrollRestore.SaveAsync(Js, ShopListScrollRestore.StoreKey, productId);
Navigation.NavigateTo($"{RouteConstants.Store.ProductDetail}{productId}");
}
private string FormatPrice(long price) => $"{VAT.AddVAT(price):N0} تومان";
public void Dispose()
@@ -174,33 +128,24 @@ public partial class Products : ComponentBase, IDisposable
private void HandleLocationChanged(object? sender, LocationChangedEventArgs args)
{
if (_ignoreNextLocationChange)
_ = InvokeAsync(LoadInitial);
}
private void UpdateCategoryFilterFromUri()
{
var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("category", out var values) &&
long.TryParse(values.FirstOrDefault(), out var categoryId))
{
_ignoreNextLocationChange = false;
_activeCategoryId = categoryId;
return;
}
var uri = Navigation.ToAbsoluteUri(args.Location);
if (!uri.AbsolutePath.Equals(RouteConstants.Store.Products, StringComparison.OrdinalIgnoreCase))
return;
var incoming = ShopListQueryState.Parse(uri);
if (incoming.Matches(CaptureState()))
return;
_ = InvokeAsync(async () =>
{
ApplyStateFromUri();
await LoadPages(_currentPage);
_pendingScrollRestore = true;
StateHasChanged();
});
_activeCategoryId = null;
}
private async Task ClearCategoryFilter()
private void ClearCategoryFilter()
{
_activeCategoryId = null;
_activeCategoryTitle = null;
await ReloadFromFilters();
Navigation.NavigateTo(RouteConstants.Store.Products);
}
}
+2 -6
View File
@@ -60,14 +60,10 @@ 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");
var xml = await sitemapGenerator.GenerateAsync(cancellationToken);
return Results.Content(xml, "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");
+2 -6
View File
@@ -24,9 +24,7 @@
OnChangePhone="ChangePhoneAsync"
OnResendOtp="ResendOtpAsync"
ReferralCode="@_referralCode"
ReferralCodeChanged="OnReferralCodeChanged"
InviterMobile="@_inviterMobile"
InviterMobileChanged="OnInviterMobileChanged" />
ReferralCodeChanged="OnReferralCodeChanged" />
</div>
}
else
@@ -57,9 +55,7 @@ else
OnChangePhone="ChangePhoneAsync"
OnResendOtp="ResendOtpAsync"
ReferralCode="@_referralCode"
ReferralCodeChanged="OnReferralCodeChanged"
InviterMobile="@_inviterMobile"
InviterMobileChanged="OnInviterMobileChanged" />
ReferralCodeChanged="OnReferralCodeChanged" />
<MudStack Class="mt-4" Spacing="2">
@if (_currentStep == AuthStep.Phone)
@@ -44,8 +44,6 @@ public partial class AuthDialog : IDisposable
// Referral code field
private string? _referralCode;
// Inviter mobile (دعوت‌کننده)
private string? _inviterMobile;
[Inject] private ILocalStorageService LocalStorage { get; set; } = default!;
[Inject] private UserContract.UserContractClient UserClient { get; set; } = default!;
@@ -73,7 +71,6 @@ public partial class AuthDialog : IDisposable
}
_referralCode = await LocalStorage.GetItemAsync<string>("referral:code");
_inviterMobile = await LocalStorage.GetItemAsync<string>("inviter:mobile");
// await LocalStorage.RemoveItemAsync(TokenStorageKey);
}
@@ -86,15 +83,6 @@ public partial class AuthDialog : IDisposable
await LocalStorage.RemoveItemAsync("referral:code");
}
private async Task OnInviterMobileChanged(string? value)
{
_inviterMobile = value;
if (!string.IsNullOrWhiteSpace(value))
await LocalStorage.SetItemAsync("inviter:mobile", value);
else
await LocalStorage.RemoveItemAsync("inviter:mobile");
}
private void GenerateCaptcha()
{
_captchaCode = Guid.NewGuid().ToString("N")[..6].ToUpperInvariant();
@@ -216,15 +204,6 @@ public partial class AuthDialog : IDisposable
_verifyRequest.ParentReferralCode = effectiveReferralCode;
}
var storedInviterMobile = await LocalStorage.GetItemAsync<string>("inviter:mobile");
var effectiveInviterMobile = !string.IsNullOrWhiteSpace(_inviterMobile)
? _inviterMobile
: storedInviterMobile;
if (!string.IsNullOrWhiteSpace(effectiveInviterMobile))
{
_verifyRequest.InviterMobile = effectiveInviterMobile;
}
var validationResult = true; // _verifyRequestValidator.Validate(_verifyRequest);
if (!validationResult)
{
@@ -257,7 +236,6 @@ public partial class AuthDialog : IDisposable
await LocalStorage.RemoveItemAsync(PhoneStorageKey);
await LocalStorage.RemoveItemAsync(RedirectStorageKey);
await LocalStorage.RemoveItemAsync("referral:code");
await LocalStorage.RemoveItemAsync("inviter:mobile");
_attemptsLeft = MaxVerificationAttempts;
_verifyRequest.Code = string.Empty;
+3
View File
@@ -45,6 +45,9 @@
Href="https://wa.me/989123456789"
Target="_blank" />
</MudStack>
<MudStack Row="true" Spacing="2" Class="enamad">
<a referrerpolicy='origin' target='_blank' href='https://trustseal.enamad.ir/?id=688255&Code=uoMRGkyUYHAtMxkpBLB0KyPL5Ac66ACX'><img referrerpolicy='origin' src='https://trustseal.enamad.ir/logo.aspx?id=688255&Code=uoMRGkyUYHAtMxkpBLB0KyPL5Ac66ACX' alt='' style='cursor:pointer' code='uoMRGkyUYHAtMxkpBLB0KyPL5Ac66ACX'></a>
</MudStack>
</MudStack>
</MudItem>
@@ -1,5 +1,3 @@
@using FrontOffice.Main.Utilities
<MudDialog>
<TitleContent>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
@@ -9,34 +7,9 @@
</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>
}
<MudText Typo="Typo.body2" Class="mud-text-secondary">
برای تکمیل این سفارش، موجودی اصلی شما کافی نیست. می‌توانید کیف پول را شارژ کنید و سپس سفارش را ثبت کنید.
</MudText>
<MudPaper Outlined="true" Class="pa-3 rounded-lg">
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween">
@@ -58,33 +31,12 @@
</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>
}
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Payment"
OnClick="GoToCharge">
شارژ حساب اصلی
</MudButton>
</DialogActions>
</MudDialog>
@@ -101,52 +53,9 @@
[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 void GoToCharge() => MudDialog.Close(DialogResult.Ok(ShortfallAmount));
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
}
@@ -52,7 +52,7 @@
روش پرداخت خود را انتخاب کنید:
</MudText>
@* Direct Payment (ZarinPal) — always available *@
@* Direct Payment — always available *@
<MudPaper Elevation="0" Class="pa-4 rounded-lg pkg-payment-option"
Style="border:2px solid var(--mud-palette-primary); cursor:pointer;"
@onclick="() => SelectPaymentMethod(PaymentMethodType.DirectPayment)">
@@ -61,29 +61,13 @@
<MudIcon Icon="@Icons.Material.Filled.CreditCard" Color="Color.Primary" Size="Size.Medium" />
<MudStack Spacing="0" Class="flex-grow-1">
<MudText Typo="Typo.subtitle1" Color="Color.Primary"><b>پرداخت مستقیم</b></MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">پرداخت آنی از طریق درگاه بانکی (زرین‌پال)</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">پرداخت آنی از طریق درگاه بانکی</MudText>
</MudStack>
<MudIcon Icon="@Icons.Material.Filled.ArrowBack" Color="Color.Primary" Size="Size.Small" />
</MudStack>
</MudStack>
</MudPaper>
@* TwentyCard IPG *@
<MudPaper Elevation="0" Class="pa-4 rounded-lg pkg-payment-option"
Style="border:2px solid var(--mud-palette-info); cursor:pointer;"
@onclick="() => SelectPaymentMethod(PaymentMethodType.TwentyCard)">
<MudStack Spacing="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.AccountBalance" Color="Color.Info" Size="Size.Medium" />
<MudStack Spacing="0" Class="flex-grow-1">
<MudText Typo="Typo.subtitle1" Color="Color.Info"><b>پرداخت با ۲۰کارت</b></MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">پرداخت از طریق درگاه ۲۰کارت (نیاز به تأمین اعتبار)</MudText>
</MudStack>
<MudIcon Icon="@Icons.Material.Filled.ArrowBack" Color="Color.Info" Size="Size.Small" />
</MudStack>
</MudStack>
</MudPaper>
@* Daya Loan — only for base package AND first purchase cycle *@
@if (_selectedPackage.SupportsDayaPurchase && PurchaseCycleCount == 0)
{
@@ -245,6 +229,6 @@
: "background:rgba(16,185,129,.12); color:#10b981;";
}
public enum PaymentMethodType { DirectPayment, TwentyCard, DayaLoan }
public enum PaymentMethodType { DirectPayment, DayaLoan }
public record PackagePurchaseResult(PackageDto Package, PaymentMethodType Method);
}
@@ -38,21 +38,11 @@
<MudTextField Value="ReferralCode"
ValueChanged="@((string? v) => ReferralCodeChanged.InvokeAsync(v))"
Label="کد معرف سازمان (اختیاری)"
Label="کد معرف (اختیاری)"
Variant="Variant.Outlined"
Immediate="true"
Placeholder="کد معرف ۱۰ رقمی"
Class="mb-3"
HelperText="جایگاه شما در سازمان — با لینک معرف پر می‌شود" />
<MudTextField Value="InviterMobile"
ValueChanged="@((string? v) => InviterMobileChanged.InvokeAsync(v))"
Label="شماره موبایل دعوت‌کننده (اختیاری)"
Variant="Variant.Outlined"
Immediate="true"
Placeholder="مثلاً 0912xxxxxxx"
Class="mb-3"
HelperText="کسی که شما را به کارابازار معرفی کرده — متفاوت از معرف سازمان" />
Placeholder="کد معرف را وارد کنید"
Class="mb-3" />
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
{
@@ -33,10 +33,6 @@ public partial class PhoneVerifyForm
[Parameter] public string? ReferralCode { get; set; }
[Parameter] public EventCallback<string?> ReferralCodeChanged { get; set; }
// ── Inviter (دعوت‌کننده) ──
[Parameter] public string? InviterMobile { get; set; }
[Parameter] public EventCallback<string?> InviterMobileChanged { get; set; }
// ── Verify actions ──
[Parameter] public EventCallback OnChangePhone { get; set; }
[Parameter] public EventCallback OnResendOtp { get; set; }
@@ -1,60 +0,0 @@
@inject IConfiguration Configuration
@inject IJSRuntime JSRuntime
<MudDialog>
<TitleContent>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.AccountBalance" Color="Color.Primary" />
<MudText Typo="Typo.h6">تأمین اعتبار ۲۰کارت</MudText>
</MudStack>
</TitleContent>
<DialogContent>
<MudStack Spacing="3">
<MudText Typo="Typo.body2" Class="mud-text-secondary">
برای پرداخت از طریق درگاه ۲۰کارت، ابتدا باید اعتبار حساب ۲۰کارت خود را تأمین کنید.
پس از تأمین اعتبار، روی «ادامه پرداخت» بزنید تا به درگاه هدایت شوید.
</MudText>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined">
اگر قبلاً اعتبار را تأمین کرده‌اید، می‌توانید مستقیم ادامه دهید.
</MudAlert>
</MudStack>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel" Variant="Variant.Text">انصراف</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.OpenInNew"
OnClick="OpenCreditPortal"
Disabled="@string.IsNullOrWhiteSpace(_creditPortalUrl)">
تأمین اعتبار
</MudButton>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Payment"
OnClick="ContinuePayment">
ادامه پرداخت
</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = default!;
private string _creditPortalUrl = string.Empty;
protected override void OnInitialized()
{
_creditPortalUrl = Configuration["TwentyCard:CreditPortalUrl"] ?? string.Empty;
}
private async Task OpenCreditPortal()
{
if (string.IsNullOrWhiteSpace(_creditPortalUrl)) return;
await JSRuntime.InvokeVoidAsync("open", _creditPortalUrl, "_blank");
}
private void ContinuePayment() => MudDialog.Close(DialogResult.Ok(true));
private void Cancel() => MudDialog.Cancel();
}
@@ -79,7 +79,6 @@ public class AuthService
var isSignMainContractStr = claims.FirstOrDefault(c => c.key == "IsSignMainContract").value;
var hasPurchasedPackageStr = claims.FirstOrDefault(c => c.key == "HasPurchasedPackage").value;
var isClubMemberActiveStr = claims.FirstOrDefault(c => c.key == "IsClubMemberActive").value;
var hasAddressStr = claims.FirstOrDefault(c => c.key == "HasAddress").value;
_userAuthInfo.UserId = long.TryParse(userIdStr, out var userId) ? userId : 0;
_userAuthInfo.FirstName = firstName ?? string.Empty;
@@ -89,7 +88,6 @@ public class AuthService
_userAuthInfo.IsSignMainContract = bool.TryParse(isSignMainContractStr, out var isSignMainContract) && isSignMainContract;
_userAuthInfo.HasPurchasedPackage = bool.TryParse(hasPurchasedPackageStr, out var hasPurchased) && hasPurchased;
_userAuthInfo.IsClubMemberActive = bool.TryParse(isClubMemberActiveStr, out var isClubActive) && isClubActive;
_userAuthInfo.HasAddress = bool.TryParse(hasAddressStr, out var hasAddress) && hasAddress;
}
private async Task<List<(string key, string value)>?> GetTokenClaimsAsync()
@@ -219,8 +219,6 @@ public class DiscountOrderService
2 => "ارسال شده",
3 => "تحویل داده شده",
4 => "لغو شده",
5 => "آماده تحویل در دفتر",
6 => "مرجوع شده",
_ => "نامشخص"
};
@@ -230,9 +228,7 @@ public class DiscountOrderService
1 => Color.Info,
2 => Color.Primary,
3 => Color.Success,
4 => Color.Dark,
5 => Color.Primary,
6 => Color.Error,
4 => Color.Error,
_ => Color.Default
};
@@ -14,7 +14,7 @@ public class NetworkNodeDto
public NetworkNodeDto? RightChild { get; set; }
public int Level { get; set; }
public bool IsActive { get; set; } = true;
public DateTime? JoinedAt { get; set; } // تاریخ ثبت‌نام (UserCreated)
public DateTime? JoinedAt { get; set; }
public bool IsClubActive { get; set; }
public string? ActivationWeekNumber { get; set; }
/// <summary>
@@ -26,18 +26,6 @@ public class NetworkNodeDto
/// نام پکیج موثر (مثلاً پکیج طلایی / نقره‌ای)
/// </summary>
public string? PackageName { get; set; }
/// <summary>تعداد اعضای فعال با پکیج طلایی جاری در پای چپ</summary>
public int GoldLeftLegTotal { get; set; }
/// <summary>تعداد اعضای فعال با پکیج طلایی جاری در پای راست</summary>
public int GoldRightLegTotal { get; set; }
/// <summary>تعداد اعضای فعال با پکیج نقره‌ای جاری در پای چپ</summary>
public int SilverLeftLegTotal { get; set; }
/// <summary>تعداد اعضای فعال با پکیج نقره‌ای جاری در پای راست</summary>
public int SilverRightLegTotal { get; set; }
}
/// <summary>
@@ -65,11 +53,6 @@ public class FlatNetworkNodeDto
/// نام پکیج موثر (مثلاً پکیج طلایی / نقره‌ای)
/// </summary>
public string? PackageName { get; set; }
public int GoldLeftLegTotal { get; set; }
public int GoldRightLegTotal { get; set; }
public int SilverLeftLegTotal { get; set; }
public int SilverRightLegTotal { get; set; }
}
/// <summary>
@@ -109,11 +92,7 @@ public class NetworkTreeDto
ActivationWeekNumber = node.ActivationWeekNumber,
JoinedAt = node.JoinedAt,
ReferralCode = node.ReferralCode,
PackageName = node.PackageName,
GoldLeftLegTotal = node.GoldLeftLegTotal,
GoldRightLegTotal = node.GoldRightLegTotal,
SilverLeftLegTotal = node.SilverLeftLegTotal,
SilverRightLegTotal = node.SilverRightLegTotal
PackageName = node.PackageName
};
result.Add(flatNode);
@@ -146,19 +125,10 @@ public class LastMemberDto
/// </summary>
public class NetworkStatisticsDto
{
/// <summary>اعضای فعال باشگاه در پای چپ</summary>
public int LeftLegCount { get; set; }
/// <summary>اعضای فعال باشگاه در پای راست</summary>
public int RightLegCount { get; set; }
/// <summary>LeftLegCount + RightLegCount</summary>
public int TotalMembers { get; set; }
public int TreeDepth { get; set; }
public string WeakerLeg { get; set; } = string.Empty; // "Left" or "Right"
public LastMemberDto? LastMember { get; set; }
/// <summary>تعداد اعضای فعال با پکیج طلایی/نقره‌ای جاری در هر پا — نوار پایین شجره</summary>
public int GoldLeftLegTotal { get; set; }
public int GoldRightLegTotal { get; set; }
public int SilverLeftLegTotal { get; set; }
public int SilverRightLegTotal { get; set; }
}
@@ -96,10 +96,6 @@ public class NetworkMembershipService
TotalMembers = response.TotalMembers,
TreeDepth = response.MaxDepth,
WeakerLeg = response.WeakerLeg,
GoldLeftLegTotal = response.GoldLeftLegTotal,
GoldRightLegTotal = response.GoldRightLegTotal,
SilverLeftLegTotal = response.SilverLeftLegTotal,
SilverRightLegTotal = response.SilverRightLegTotal,
LastMember = response.LastMember != null ? new LastMemberDto
{
UserId = response.LastMember.UserId,
@@ -142,13 +138,8 @@ public class NetworkMembershipService
IsClubActive = node.IsClubActive,
ReferralCode = node.ReferralCode,
PackageName = string.IsNullOrWhiteSpace(node.PackageName) ? null : node.PackageName,
GoldLeftLegTotal = node.GoldLeftLegTotal,
GoldRightLegTotal = node.GoldRightLegTotal,
SilverLeftLegTotal = node.SilverLeftLegTotal,
SilverRightLegTotal = node.SilverRightLegTotal,
ActivationWeekNumber = node.ActivationWeekDefinitionId?.ToString(),
JoinedAt = node.UserCreated?.ToDateTime()
?? node.JoinedAt?.ToDateTime(),
JoinedAt = node.JoinedAt?.ToDateTime(),
LeftChild = MapNodeFromProto(node.LeftChild),
RightChild = MapNodeFromProto(node.RightChild)
};
@@ -97,4 +97,22 @@ public class OrderService
return 0.10;
}
}
/// <summary>
/// محاسبه PV سفارش
/// </summary>
public async Task<CalculateOrderPVResponse?> CalculateOrderPVAsync(long orderId)
{
try
{
return await _userOrderContractClient.CalculateOrderPVAsync(new CalculateOrderPVRequest
{
OrderId = orderId
});
}
catch
{
return null;
}
}
}
@@ -21,7 +21,6 @@ public static class RouteConstants
public const string Settings = "/profile/settings";
public const string ChangePassword = "/profile/change-password";
public const string Tree = "/profile/tree";
public const string Invitees = "/profile/invitees";
public const string Wallet = "/profile/wallet";
public const string MagicWallet = "/profile/magic-wallet";
public const string ChargeDiscountWallet = "/profile/charge-discount-wallet";
@@ -1,4 +1,3 @@
using System.Text.RegularExpressions;
using Microsoft.Extensions.Options;
namespace FrontOffice.Main.Utilities.Seo;
@@ -31,9 +30,6 @@ public class SeoMetadataProvider
[RouteConstants.Store.Products] = (
"محصولات | کارابازار سلامت",
"فروشگاه محصولات سلامت کارابازار — خرید آنلاین با پاداش باشگاه مشتریان."),
[RouteConstants.Store.Categories] = (
"دسته‌بندی‌ها | کارابازار سلامت",
"دسته‌بندی محصولات فروشگاه کارابازار سلامت."),
[RouteConstants.Package.List] = (
"پکیج‌ها | کارابازار سلامت",
"پکیج‌های عضویت باشگاه مشتریان کارابازار سلامت."),
@@ -49,18 +45,6 @@ public class SeoMetadataProvider
[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)
@@ -78,20 +62,8 @@ public class SeoMetadataProvider
"/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;
@@ -149,31 +121,14 @@ public class SeoMetadataProvider
};
}
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
CanonicalUrl = $"{baseUrl}{normalizedPath}"
};
}
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))
@@ -191,9 +146,6 @@ public class SeoMetadataProvider
private static bool IsNoIndexPath(string path)
{
if (NoIndexPaths.Contains(path))
return true;
foreach (var prefix in NoIndexPrefixes)
{
if (path.Equals(prefix, StringComparison.OrdinalIgnoreCase)
@@ -208,6 +160,6 @@ public class SeoMetadataProvider
private static string GetHomeCrawlableContent() =>
"کارابازار سلامت (KBS) — باشگاه مشتریان و فروشگاه محصولات سلامت. " +
"با عضویت در باشگاه مشتریان KBS از پاداش شفاف، کیف پول جادویی، فروشگاه اعتباری و سازمان فروش بهره‌مند شوید. " +
"با عضویت در باشگاه مشتریان KBS از پاداش شفاف، کیف پول جادویی، فروشگاه اعتباری و شبکه فروش بهره‌مند شوید. " +
"ثبت‌نام رایگان در kbs2.ir.";
}
@@ -7,5 +7,5 @@ public class SeoSettings
public string SiteUrl { get; set; } = "https://kbs2.ir";
public string SiteName { get; set; } = "کارابازار سلامت";
public string DefaultDescription { get; set; } =
"باشگاه مشتریان کارابازار سلامت (KBS) — فروشگاه محصولات سلامت، پاداش شفاف، کیف پول و سازمان فروش.";
"باشگاه مشتریان کارابازار سلامت (KBS) — فروشگاه محصولات سلامت، پاداش شفاف، کیف پول و شبکه فروش.";
}
@@ -91,12 +91,6 @@ public class SitemapGenerator
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,
@@ -1,94 +0,0 @@
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;
}
@@ -1,69 +0,0 @@
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);
}
@@ -11,5 +11,4 @@ public class UserAuthInfo
public bool IsSignMainContract { get; set; }
public bool HasPurchasedPackage { get; set; }
public bool IsClubMemberActive { get; set; }
public bool HasAddress { get; set; }
}
@@ -1,13 +1,6 @@
{
"GwUrl": "https://cms.kbs1.ir",
"GwUrl": "https://cms.kbs2.ir",
"DownloadUrl": "",
"TwentyCard": {
"CreditPortalUrl": "https://userpanel.20card.ir/"
},
"Landing": {
"ShowStepsSection": false,
"ShowFeaturesSection": false
},
"EncryptionSettings": {
"Key": "kmcQ3XTmH4mrdh8VHziuscyf8LLYjG//Kyni81nH/0E=",
"IV": "1wyF3Tt142MOkCpIyCxh/g=="
@@ -21,5 +14,10 @@
"Microsoft.AspNetCore": "Warning"
}
},
"Seo": {
"SiteUrl": "https://kbs2.ir",
"SiteName": "کارابازار سلامت",
"DefaultDescription": "باشگاه مشتریان کارابازار سلامت (KBS) — فروشگاه محصولات سلامت، پاداش شفاف، کیف پول و شبکه فروش."
},
"AllowedHosts": "*"
}
@@ -2,17 +2,10 @@
"Seo": {
"SiteUrl": "https://kbs1.ir",
"SiteName": "کارابازار سلامت",
"DefaultDescription": "باشگاه مشتریان کارابازار سلامت (KBS) — فروشگاه محصولات سلامت، پاداش شفاف، کیف پول و سازمان فروش."
"DefaultDescription": "باشگاه مشتریان کارابازار سلامت (KBS) — فروشگاه محصولات سلامت، پاداش شفاف، کیف پول و شبکه فروش."
},
"GwUrl": "https://cms.se.kbs1.ir",
"DownloadUrl": "",
"TwentyCard": {
"CreditPortalUrl": "https://userpanel.20card.ir/"
},
"Landing": {
"ShowStepsSection": false,
"ShowFeaturesSection": false
},
"EncryptionSettings": {
"Key": "kmcQ3XTmH4mrdh8VHziuscyf8LLYjG//Kyni81nH/0E=",
"IV": "1wyF3Tt142MOkCpIyCxh/g=="
+1 -8
View File
@@ -2,17 +2,10 @@
"Seo": {
"SiteUrl": "http://localhost:5268",
"SiteName": "کارابازار سلامت",
"DefaultDescription": "باشگاه مشتریان کارابازار سلامت (KBS) — فروشگاه محصولات سلامت، پاداش شفاف، کیف پول و سازمان فروش."
"DefaultDescription": "باشگاه مشتریان کارابازار سلامت (KBS) — فروشگاه محصولات سلامت، پاداش شفاف، کیف پول و شبکه فروش."
},
"GwUrl": "https://cms.se.kbs1.ir",
"DownloadUrl": "",
"TwentyCard": {
"CreditPortalUrl": "https://userpanel.20card.ir/"
},
"Landing": {
"ShowStepsSection": false,
"ShowFeaturesSection": false
},
"EncryptionSettings": {
"Key": "kmcQ3XTmH4mrdh8VHziuscyf8LLYjG//Kyni81nH/0E=",
"IV": "1wyF3Tt142MOkCpIyCxh/g=="
+5 -47
View File
@@ -118,16 +118,6 @@
line-height: 1.2;
}
.node-registered-at {
font-size: 8px;
color: #6c757d;
margin-top: 1px;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.node-level-sm {
font-size: 9px;
color: #6c757d;
@@ -151,7 +141,6 @@
max-width: 120px;
}
/* D3: تعداد پکیج طلایی/نقره‌ای فقط در نوار پایین شجره (.org-chart-stats) */
/* Referral Code - only for club active members */
.node-referral {
display: flex;
@@ -381,25 +370,12 @@
/* Stats Bar */
.org-chart-stats {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 16px;
background: #f8f9fa;
border-top: 1px solid #e9ecef;
}
.org-chart-stats-row {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 24px;
}
.org-chart-stats-row.leg-scores {
gap: 12px;
padding: 12px 16px;
background: #f8f9fa;
border-top: 1px solid #e9ecef;
}
.stat-item {
@@ -409,21 +385,6 @@
font-size: 13px;
}
.stat-item.leg-score-gold .stat-label {
color: #b8860b;
font-weight: 600;
}
.stat-item.leg-score-silver .stat-label {
color: #607d8b;
font-weight: 600;
}
.stat-sep {
color: #adb5bd;
margin: 0 2px;
}
.stat-label {
color: #6c757d;
}
@@ -487,12 +448,9 @@
}
.org-chart-stats {
gap: 8px;
padding: 10px;
}
.org-chart-stats-row {
flex-wrap: wrap;
gap: 12px;
padding: 10px;
}
.stat-item {
@@ -1617,12 +1617,6 @@ html, body {
overflow: hidden;
}
/* ── Landing TwentyCard credit promo ── */
.landing-twentycard-box {
background: var(--ds-info-soft);
border: 1px solid rgba(59, 130, 246, 0.22);
}
/* ── Register Wizard Unified ── */
.wizard-section {
min-height: 80vh;
+2 -3
View File
@@ -38,7 +38,7 @@ window.OrgChart = {
.container('#' + containerId)
.data(data)
.nodeWidth((d) => 140)
.nodeHeight((d) => 98)
.nodeHeight((d) => 85)
.childrenMargin((d) => 50)
.compactMarginBetween((d) => 15)
.compactMarginPair((d) => 15)
@@ -86,7 +86,6 @@ window.OrgChart = {
<div class="node-avatar-sm">${firstChar}</div>
<div class="node-info">
<div class="node-name-sm">${data.fullName || 'بدون نام'}</div>
${data.registeredAt ? `<div class="node-registered-at">${data.registeredAt}</div>` : ''}
<div class="node-level-sm">L${data.level || 0}${!isRoot ? ' • ' + (data.position === 'Left' ? 'چپ' : 'راست') : ''}</div>
${packageBadge}
${referralCodeHtml}
@@ -297,7 +296,7 @@ window.OrgChart = {
isActive: node.isActive !== false,
isClubActive: node.isClubActive || false,
activationWeekNumber: node.activationWeekNumber,
registeredAt: node.registeredAt || node.joinedAt || ''
joinedAt: node.joinedAt
};
result.push(flatNode);
-4
View File
@@ -13,9 +13,5 @@ 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