Compare commits
49 Commits
7a5a2110c6
...
production
| Author | SHA1 | Date | |
|---|---|---|---|
| f743121eb2 | |||
| 76d96c2544 | |||
| b60d1da4b7 | |||
| 016b259da0 | |||
| bb789e443e | |||
| 0c1842cf02 | |||
| 392e038913 | |||
| d7f25efce2 | |||
| 5246d8c1ba | |||
| f5bfdba124 | |||
| c2c07c6b89 | |||
| 764742dbe7 | |||
| 955736d345 | |||
| ea357e9865 | |||
| a4061c041b | |||
| a108f89213 | |||
| c71062decf | |||
| c815f8d358 | |||
| 27a3ea76c5 | |||
| 0d0ca252a6 | |||
| aedf312812 | |||
| 690aaa714b | |||
| 8252e408b6 | |||
| 186abe042f | |||
| c6089f8dc3 | |||
| 101687f9db | |||
| 307302e33d | |||
| c243b59113 | |||
| 58037f0b49 | |||
| aae4b2693d | |||
| 61fd805c75 | |||
| ef6d233bbc | |||
| 3d84e726db | |||
| 13091849cf | |||
| 8e98c50bdf | |||
| 6123e47968 | |||
| acad1a719f | |||
| c7e6d01c11 | |||
| c764614d26 | |||
| 9203dc7924 | |||
| b64d99c5b3 | |||
| 4989cd00e0 | |||
| a708727241 | |||
| bed35935ad | |||
| 2339ca99b4 | |||
| ceaaac3e23 | |||
| 7f5a4f2c37 | |||
| db35814ce3 | |||
| 06100ca6dd |
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
# Keep production config from current branch during merges — never overwrite
|
# Keep production config from current branch during merges — never ov erwrite
|
||||||
src/FrontOffice.Main/appsettings.Production.json merge=ours
|
src/FrontOffice.Main/appsettings.Production.json merge=ours
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ on:
|
|||||||
- kub-stage
|
- kub-stage
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: 194.5.195.53:30080
|
REGISTRY: 185.252.31.44:30080
|
||||||
IMAGE_NAME: admin/frontoffice
|
IMAGE_NAME: admin/frontoffice
|
||||||
K8S_SERVER: 194.5.195.53
|
K8S_SERVER: 185.252.31.44
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: 194.5.195.53:32082/docker-sshpass:latest
|
image: 185.252.31.44:32082/docker-sshpass:latest
|
||||||
options: --privileged
|
options: --privileged
|
||||||
steps:
|
steps:
|
||||||
- name: Start Docker daemon
|
- name: Start Docker daemon
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
mkdir -p /etc/docker
|
mkdir -p /etc/docker
|
||||||
cat > /etc/docker/daemon.json << 'DAEMON'
|
cat > /etc/docker/daemon.json << 'DAEMON'
|
||||||
{
|
{
|
||||||
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
|
"insecure-registries": ["185.252.31.44:30080", "185.252.31.44:32500", "185.252.31.44:32082"]
|
||||||
}
|
}
|
||||||
DAEMON
|
DAEMON
|
||||||
echo "🚀 Starting Docker daemon..."
|
echo "🚀 Starting Docker daemon..."
|
||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Login to Docker registries
|
- name: Login to Docker registries
|
||||||
run: |
|
run: |
|
||||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 194.5.195.53:32082 -u admin --password-stdin
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 185.252.31.44:32082 -u admin --password-stdin
|
||||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
|
||||||
|
|
||||||
- name: Build Docker Image
|
- name: Build Docker Image
|
||||||
@@ -73,10 +73,14 @@ jobs:
|
|||||||
|
|
||||||
# Apply manifests and restart
|
# Apply manifests and restart
|
||||||
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
|
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
|
||||||
kubectl apply -f /tmp/frontoffice-deployment.yaml &&
|
kubectl apply -n kbs-stage -f /tmp/frontoffice-deployment.yaml &&
|
||||||
crictl rmi ${REGISTRY}/${IMAGE_NAME}:latest 2>/dev/null || true &&
|
# Drop cached :latest so IfNotPresent/Always both pick the new digest
|
||||||
kubectl rollout restart deployment/frontoffice &&
|
# (crictl is broken on this host — use containerd ctr)
|
||||||
kubectl rollout status deployment/frontoffice --timeout=180s &&
|
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 &&
|
||||||
rm -f /tmp/frontoffice-deployment.yaml
|
rm -f /tmp/frontoffice-deployment.yaml
|
||||||
"
|
"
|
||||||
echo "✅ Deployed!"
|
echo "✅ Deployed!"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ on:
|
|||||||
- production
|
- production
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: 194.5.195.53:30080
|
REGISTRY: 185.252.31.44:30080
|
||||||
IMAGE_NAME: admin/frontoffice
|
IMAGE_NAME: admin/frontoffice
|
||||||
K8S_SERVER: 45.149.79.127
|
K8S_SERVER: 45.149.79.127
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: 194.5.195.53:32082/docker-sshpass:latest
|
image: 185.252.31.44:32082/docker-sshpass:latest
|
||||||
options: --privileged
|
options: --privileged
|
||||||
steps:
|
steps:
|
||||||
- name: Start Docker daemon
|
- name: Start Docker daemon
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
mkdir -p /etc/docker
|
mkdir -p /etc/docker
|
||||||
cat > /etc/docker/daemon.json << 'DAEMON'
|
cat > /etc/docker/daemon.json << 'DAEMON'
|
||||||
{
|
{
|
||||||
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
|
"insecure-registries": ["185.252.31.44:30080", "185.252.31.44:32500", "185.252.31.44:32082"]
|
||||||
}
|
}
|
||||||
DAEMON
|
DAEMON
|
||||||
echo "🚀 Starting Docker daemon..."
|
echo "🚀 Starting Docker daemon..."
|
||||||
@@ -50,7 +50,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Login to Docker registries
|
- name: Login to Docker registries
|
||||||
run: |
|
run: |
|
||||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 194.5.195.53:32082 -u admin --password-stdin
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 185.252.31.44:32082 -u admin --password-stdin
|
||||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
|
||||||
|
|
||||||
- name: Build Docker Image
|
- name: Build Docker Image
|
||||||
|
|||||||
+5
-5
@@ -1,10 +1,10 @@
|
|||||||
# ---> VisualStudio
|
# ---> VisualStudio
|
||||||
## Ignore Visual Studio temporary files, build results, and
|
## 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
|
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
|
||||||
|
|
||||||
# User-specific files
|
# User-specific files
|
||||||
*.rsuser
|
*.rsuser
|
||||||
*.suo
|
*.suo
|
||||||
*.user
|
*.user
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
*.userprefs
|
*.userprefs
|
||||||
|
|
||||||
# Mono auto generated files
|
# Mono auto generated files
|
||||||
mono_crash.*
|
mono_crash.*
|
||||||
|
|
||||||
# Build results
|
# Build results
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
# Docs moved to totalDoc
|
# Docs moved to totalDoc
|
||||||
|
|
||||||
See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation.
|
See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation.
|
||||||
|
|
||||||
@@ -101,7 +101,6 @@
|
|||||||
| 6.6 | `UserOrderContract.SubmitShopBuyOrder` | `UserOrderContract` | `/checkout-summary` | ثبت سفارش و پرداخت از کیف پول |
|
| 6.6 | `UserOrderContract.SubmitShopBuyOrder` | `UserOrderContract` | `/checkout-summary` | ثبت سفارش و پرداخت از کیف پول |
|
||||||
| 6.7 | `OrderService.GetOrdersAsync` | `UserOrderContract.GetCustomerOrders` | `/orders` | لیست سفارشهای کاربر |
|
| 6.7 | `OrderService.GetOrdersAsync` | `UserOrderContract.GetCustomerOrders` | `/orders` | لیست سفارشهای کاربر |
|
||||||
| 6.8 | `OrderService.GetOrderAsync` | `UserOrderContract.GetCustomerOrder` | `/order/{id}`, `/order-tracking/{id}` | جزئیات یک سفارش |
|
| 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.10 | `VATService.GetRateAsync` | `UserOrderContract` (via scope) | Store/Discount صفحات قیمت | نرخ مالیات بر ارزش افزوده (کش روزانه) |
|
||||||
| 6.11 | `WalletService.GetBalancesAsync` | `UserWalletContract` | `/checkout-summary` | بررسی موجودی برای پرداخت سفارش |
|
| 6.11 | `WalletService.GetBalancesAsync` | `UserWalletContract` | `/checkout-summary` | بررسی موجودی برای پرداخت سفارش |
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: frontoffice
|
- name: frontoffice
|
||||||
image: 194.5.195.53:30080/admin/frontoffice:prod
|
image: 185.252.31.44:30080/admin/frontoffice:prod
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 80
|
- containerPort: 80
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ apiVersion: apps/v1
|
|||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
name: frontoffice
|
name: frontoffice
|
||||||
namespace: default
|
namespace: kbs-stage
|
||||||
labels:
|
labels:
|
||||||
app: frontoffice
|
app: frontoffice
|
||||||
environment: staging
|
environment: staging
|
||||||
@@ -20,7 +20,8 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: frontoffice
|
- name: frontoffice
|
||||||
image: 194.5.195.53:30080/admin/frontoffice:latest
|
image: 185.252.31.44:30080/admin/frontoffice:latest
|
||||||
|
# Staging uses :latest — Always avoids stale node cache after registry retag
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 80
|
- containerPort: 80
|
||||||
@@ -43,7 +44,7 @@ apiVersion: v1
|
|||||||
kind: Service
|
kind: Service
|
||||||
metadata:
|
metadata:
|
||||||
name: frontoffice-svc
|
name: frontoffice-svc
|
||||||
namespace: default
|
namespace: kbs-stage
|
||||||
labels:
|
labels:
|
||||||
app: frontoffice
|
app: frontoffice
|
||||||
spec:
|
spec:
|
||||||
@@ -54,3 +55,31 @@ spec:
|
|||||||
targetPort: 80
|
targetPort: 80
|
||||||
name: http
|
name: http
|
||||||
type: ClusterIP
|
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
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM 194.5.195.53:32082/dotnet/sdk:9.0 AS build
|
FROM 185.252.31.44:32082/dotnet/sdk:9.0 AS build
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
# Copy NuGet config and project file
|
# 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
|
RUN dotnet publish "FrontOffice.Main.csproj" -c Release -o /app/publish
|
||||||
|
|
||||||
# Runtime stage - aspnet for Blazor Server
|
# Runtime stage - aspnet for Blazor Server
|
||||||
FROM 194.5.195.53:32082/dotnet/aspnet:9.0 AS runtime
|
FROM 185.252.31.44:32082/dotnet/aspnet:9.0 AS runtime
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=build /app/publish .
|
COPY --from=build /app/publish .
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
@@ -10,9 +10,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
|
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
|
||||||
<!-- Replace all FrontOffice.BFF protobuf packages with CMS protobuf -->
|
|
||||||
<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="MudBlazor" Version="8.14.0" />
|
||||||
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
|
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
|
||||||
<PackageReference Include="Mapster" Version="7.4.0" />
|
<PackageReference Include="Mapster" Version="7.4.0" />
|
||||||
@@ -24,6 +22,19 @@
|
|||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.0" />
|
||||||
</ItemGroup>
|
</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 -->
|
<!-- Local Proto Project Reference for development -->
|
||||||
<!-- <ItemGroup>-->
|
<!-- <ItemGroup>-->
|
||||||
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.Package.Protobuf\FrontOffice.BFF.Package.Protobuf.csproj" />-->
|
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.Package.Protobuf\FrontOffice.BFF.Package.Protobuf.csproj" />-->
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<packageSources>
|
<packageSources>
|
||||||
<clear />
|
<clear />
|
||||||
<!-- Nexus (hosts FourSat packages + proxies nuget.org) -->
|
<!-- Nexus (hosts FourSat packages + proxies nuget.org) -->
|
||||||
<add key="Nexus" value="http://194.5.195.53:32081/repository/nuget-all/index.json" allowInsecureConnections="true" />
|
<add key="Nexus" value="http://185.252.31.44:32081/repository/nuget-all/index.json" allowInsecureConnections="true" />
|
||||||
</packageSources>
|
</packageSources>
|
||||||
<packageSourceCredentials>
|
<packageSourceCredentials>
|
||||||
<Nexus>
|
<Nexus>
|
||||||
|
|||||||
@@ -71,80 +71,6 @@
|
|||||||
</MudStack>
|
</MudStack>
|
||||||
}
|
}
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
<!-- Address Selection -->
|
|
||||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
|
||||||
<MudText Typo="Typo.h5" Class="mb-4">انتخاب آدرس</MudText>
|
|
||||||
|
|
||||||
@if (_isLoadingAddresses)
|
|
||||||
{
|
|
||||||
<MudStack AlignItems="AlignItems.Center" Class="py-4">
|
|
||||||
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Medium" />
|
|
||||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-2">بارگذاری آدرسها...</MudText>
|
|
||||||
</MudStack>
|
|
||||||
}
|
|
||||||
else if (_addresses.Any())
|
|
||||||
{
|
|
||||||
<MudStack Spacing="2">
|
|
||||||
@foreach (var address in _addresses)
|
|
||||||
{
|
|
||||||
<MudPaper Outlined="@(_selectedAddress?.Id != address.Id)"
|
|
||||||
Elevation="@(_selectedAddress?.Id == address.Id ? 4 : 0)"
|
|
||||||
Class="pa-3 rounded-xl cursor-pointer"
|
|
||||||
Style="@(_selectedAddress?.Id == address.Id ? "border: 2px solid var(--mud-palette-primary);" : "")"
|
|
||||||
@onclick="() => SetAddressAsDefault(address.Id)">
|
|
||||||
<MudStack Spacing="1">
|
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
|
||||||
<MudCheckBox @bind-Value="address.IsDefault"
|
|
||||||
Disabled="true"
|
|
||||||
Color="Color.Primary" />
|
|
||||||
<MudText Typo="Typo.subtitle2">@(address.Title)</MudText>
|
|
||||||
@if (address.IsDefault)
|
|
||||||
{
|
|
||||||
<MudChip T="string" Color="Color.Success" Variant="Variant.Filled" Size="Size.Small">پیشفرض</MudChip>
|
|
||||||
}
|
|
||||||
@if (!address.IsDefault)
|
|
||||||
{
|
|
||||||
@if (_isSettingDefaultAddress && _settingDefaultAddressId == address.Id)
|
|
||||||
{
|
|
||||||
<MudProgressCircular Size="Size.Small" Color="Color.Primary" Indeterminate="true" />
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<MudButton Variant="Variant.Text"
|
|
||||||
Color="Color.Primary"
|
|
||||||
Size="Size.Small"
|
|
||||||
OnClick="() => SetAddressAsDefault(address.Id)">
|
|
||||||
تنظیم به عنوان پیشفرض
|
|
||||||
</MudButton>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</MudStack>
|
|
||||||
<MudStack Row="true">
|
|
||||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">@(address.Address)</MudText>
|
|
||||||
<MudSpacer />
|
|
||||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">کد پستی: @(address.PostalCode)</MudText>
|
|
||||||
</MudStack>
|
|
||||||
</MudStack>
|
|
||||||
</MudPaper>
|
|
||||||
}
|
|
||||||
</MudStack>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<MudStack AlignItems="AlignItems.Center" Class="py-4">
|
|
||||||
<MudIcon Icon="@Icons.Material.Filled.LocationOff" Size="Size.Large" Color="Color.Default" />
|
|
||||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-2">آدرسی ثبت نشده است.</MudText>
|
|
||||||
<MudButton Variant="Variant.Outlined"
|
|
||||||
Color="Color.Primary"
|
|
||||||
StartIcon="@Icons.Material.Filled.Add"
|
|
||||||
OnClick="() => Navigation.NavigateTo(RouteConstants.Profile.Index)"
|
|
||||||
Class="mt-2">
|
|
||||||
افزودن آدرس
|
|
||||||
</MudButton>
|
|
||||||
</MudStack>
|
|
||||||
}
|
|
||||||
</MudPaper>
|
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
|
||||||
@@ -214,7 +140,18 @@
|
|||||||
OnClick="ProcessPayment"
|
OnClick="ProcessPayment"
|
||||||
Disabled="@(!CanProceedToPayment || _isProcessingPayment || !_selectedPackage.SupportsDirectPurchase)"
|
Disabled="@(!CanProceedToPayment || _isProcessingPayment || !_selectedPackage.SupportsDirectPurchase)"
|
||||||
Class="mt-2">
|
Class="mt-2">
|
||||||
@(_isProcessingPayment ? "در حال پردازش..." : "پرداخت آنلاین")
|
@(_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">
|
||||||
|
پرداخت با ۲۰کارت
|
||||||
</MudButton>
|
</MudButton>
|
||||||
|
|
||||||
@if (_selectedPackage.SupportsDayaPurchase)
|
@if (_selectedPackage.SupportsDayaPurchase)
|
||||||
@@ -241,7 +178,7 @@
|
|||||||
@if (!CanProceedToPayment)
|
@if (!CanProceedToPayment)
|
||||||
{
|
{
|
||||||
<MudText Typo="Typo.caption" Color="Color.Error" Align="Align.Center">
|
<MudText Typo="Typo.caption" Color="Color.Error" Align="Align.Center">
|
||||||
لطفاً پکیج و آدرس را انتخاب کنید.
|
لطفاً پکیج را انتخاب کنید.
|
||||||
</MudText>
|
</MudText>
|
||||||
}
|
}
|
||||||
</MudStack>
|
</MudStack>
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
|
|
||||||
using CMSMicroservice.Protobuf.Protos.Package;
|
using CMSMicroservice.Protobuf.Protos.Package;
|
||||||
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
|
||||||
using FrontOffice.Main.Utilities;
|
using FrontOffice.Main.Utilities;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
@@ -13,14 +11,10 @@ public partial class Checkout
|
|||||||
{
|
{
|
||||||
[Inject] private PackageService PackageService { get; set; } = default!;
|
[Inject] private PackageService PackageService { get; set; } = default!;
|
||||||
[Inject] private PackageContract.PackageContractClient PackageClient { get; set; } = default!;
|
[Inject] private PackageContract.PackageContractClient PackageClient { get; set; } = default!;
|
||||||
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
|
||||||
|
|
||||||
[Parameter] public long? PackageId { get; set; }
|
[Parameter] public long? PackageId { get; set; }
|
||||||
|
|
||||||
private Pack? _selectedPackage;
|
private Pack? _selectedPackage;
|
||||||
private List<CustomerAddressModel> _addresses = new();
|
|
||||||
private CustomerAddressModel? _selectedAddress;
|
|
||||||
private bool _isLoadingAddresses;
|
|
||||||
private bool _isProcessingPayment;
|
private bool _isProcessingPayment;
|
||||||
|
|
||||||
// Discount code
|
// Discount code
|
||||||
@@ -31,16 +25,11 @@ public partial class Checkout
|
|||||||
private long _discountAmount;
|
private long _discountAmount;
|
||||||
private long _finalPrice;
|
private long _finalPrice;
|
||||||
|
|
||||||
// Address management
|
private bool CanProceedToPayment => _selectedPackage != null;
|
||||||
private bool _isSettingDefaultAddress;
|
|
||||||
private long? _settingDefaultAddressId;
|
|
||||||
|
|
||||||
private bool CanProceedToPayment => _selectedPackage != null && _selectedAddress != null;
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
await LoadPackageDetails();
|
await LoadPackageDetails();
|
||||||
await LoadAddresses();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadPackageDetails()
|
private async Task LoadPackageDetails()
|
||||||
@@ -72,60 +61,6 @@ public partial class Checkout
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadAddresses()
|
|
||||||
{
|
|
||||||
_isLoadingAddresses = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var response = await UserAddressContract.GetCustomerAddressesAsync(request: new());
|
|
||||||
if (response?.Models?.Any() == true)
|
|
||||||
{
|
|
||||||
_addresses = response.Models.ToList();
|
|
||||||
// Select default address if available
|
|
||||||
_selectedAddress = _addresses.FirstOrDefault(a => a.IsDefault) ?? _addresses.First();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_addresses = new List<CustomerAddressModel>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Snackbar.Add($"خطا در بارگذاری آدرسها: {ex.Message}", Severity.Error);
|
|
||||||
_addresses = new List<CustomerAddressModel>();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_isLoadingAddresses = false;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SetAddressAsDefault(long addressId)
|
|
||||||
{
|
|
||||||
if (_isSettingDefaultAddress) return;
|
|
||||||
|
|
||||||
_isSettingDefaultAddress = true;
|
|
||||||
_settingDefaultAddressId = addressId;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await UserAddressContract.SetAddressAsDefaultAsync(new() { Id = addressId });
|
|
||||||
await LoadAddresses(); // Reload addresses to reflect the change
|
|
||||||
Snackbar.Add("آدرس پیشفرض با موفقیت تغییر یافت.", Severity.Success);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Snackbar.Add($"خطا در تغییر آدرس پیشفرض: {ex.Message}", Severity.Error);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_isSettingDefaultAddress = false;
|
|
||||||
_settingDefaultAddressId = null;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ApplyDiscountCode()
|
private async Task ApplyDiscountCode()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(_discountCode))
|
if (string.IsNullOrWhiteSpace(_discountCode))
|
||||||
@@ -157,7 +92,7 @@ public partial class Checkout
|
|||||||
_finalPrice = _selectedPackage!.Price;
|
_finalPrice = _selectedPackage!.Price;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
_discountMessage = "خطا در اعمال کد تخفیف.";
|
_discountMessage = "خطا در اعمال کد تخفیف.";
|
||||||
_discountApplied = false;
|
_discountApplied = false;
|
||||||
@@ -172,12 +107,39 @@ public partial class Checkout
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task ProcessPayment()
|
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 (_isProcessingPayment) return;
|
||||||
|
|
||||||
if (!CanProceedToPayment || _selectedPackage == null || _selectedAddress == null)
|
if (!CanProceedToPayment || _selectedPackage == null)
|
||||||
{
|
{
|
||||||
Snackbar.Add("لطفاً پکیج و آدرس را انتخاب کنید.", Severity.Warning);
|
Snackbar.Add("لطفاً پکیج را انتخاب کنید.", Severity.Warning);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,11 +147,15 @@ public partial class Checkout
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var response = await PackageClient.CustomerPurchasePackageAsync(new CustomerPurchasePackageRequest
|
var request = new CustomerPurchasePackageRequest
|
||||||
{
|
{
|
||||||
PackageId = _selectedPackage.Id,
|
PackageId = _selectedPackage.Id,
|
||||||
PurchaseMethod = PurchaseMethodEnum.PurchaseMethodGateway
|
PurchaseMethod = PurchaseMethodEnum.PurchaseMethodGateway
|
||||||
});
|
};
|
||||||
|
if (!string.IsNullOrWhiteSpace(gatewayProvider))
|
||||||
|
request.GatewayProvider = gatewayProvider;
|
||||||
|
|
||||||
|
var response = await PackageClient.CustomerPurchasePackageAsync(request);
|
||||||
|
|
||||||
if (!response.Success)
|
if (!response.Success)
|
||||||
{
|
{
|
||||||
@@ -219,9 +185,9 @@ public partial class Checkout
|
|||||||
|
|
||||||
private async Task DayaLoanPayment()
|
private async Task DayaLoanPayment()
|
||||||
{
|
{
|
||||||
if (_selectedAddress == null)
|
if (_selectedPackage == null)
|
||||||
{
|
{
|
||||||
Snackbar.Add("لطفاً آدرس را انتخاب کنید.", Severity.Warning);
|
Snackbar.Add("لطفاً پکیج را انتخاب کنید.", Severity.Warning);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,4 +196,4 @@ public partial class Checkout
|
|||||||
}
|
}
|
||||||
|
|
||||||
private record Pack(long Id, string Title, string Body, string Image, long Price, bool SupportsDirectPurchase = true, bool SupportsDayaPurchase = false);
|
private record Pack(long Id, string Title, string Body, string Image, long Price, bool SupportsDirectPurchase = true, bool SupportsDayaPurchase = false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,11 @@
|
|||||||
else if (_addresses.Count == 0)
|
else if (_addresses.Count == 0)
|
||||||
{
|
{
|
||||||
<MudAlert Severity="Severity.Warning">
|
<MudAlert Severity="Severity.Warning">
|
||||||
هیچ آدرسی ثبت نشده است. لطفاً از بخش پروفایل آدرس خود را اضافه کنید.
|
هیچ آدرسی ثبت نشده است. میتوانید همینجا آدرس جدید اضافه کنید.
|
||||||
</MudAlert>
|
</MudAlert>
|
||||||
<MudButton Class="mt-2" Variant="Variant.Outlined" Href="/profile/addresses">افزودن آدرس</MudButton>
|
<MudButton Class="mt-2" Variant="Variant.Outlined" Color="Color.Primary"
|
||||||
|
StartIcon="@Icons.Material.Filled.Add"
|
||||||
|
OnClick="OpenAddAddressDialog">افزودن آدرس</MudButton>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -41,13 +43,25 @@
|
|||||||
<MudText Typo="Typo.subtitle2">@address.Title</MudText>
|
<MudText Typo="Typo.subtitle2">@address.Title</MudText>
|
||||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">@address.Address</MudText>
|
<MudText Typo="Typo.body2" Class="mud-text-secondary">@address.Address</MudText>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
@if (address.IsDefault)
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||||
{
|
@if (address.IsDefault)
|
||||||
<MudChip T="string" Color="Color.Success" Variant="Variant.Outlined">پیشفرض</MudChip>
|
{
|
||||||
}
|
<MudChip T="string" Color="Color.Success" Variant="Variant.Outlined" Size="Size.Small">پیشفرض</MudChip>
|
||||||
|
}
|
||||||
|
<span @onclick:stopPropagation="true">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||||
|
Size="Size.Small"
|
||||||
|
Color="Color.Primary"
|
||||||
|
aria-label="ویرایش آدرس"
|
||||||
|
OnClick="@(() => OpenEditAddressDialog(address))" />
|
||||||
|
</span>
|
||||||
|
</MudStack>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
}
|
}
|
||||||
|
<MudButton Class="mt-2" Variant="Variant.Text" Color="Color.Primary"
|
||||||
|
StartIcon="@Icons.Material.Filled.Add"
|
||||||
|
OnClick="OpenAddAddressDialog">افزودن آدرس جدید</MudButton>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
}
|
}
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
||||||
|
using FrontOffice.Main.Pages.Profile.Components;
|
||||||
using FrontOffice.Main.Utilities;
|
using FrontOffice.Main.Utilities;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
|
|
||||||
@@ -40,7 +41,11 @@ public partial class Checkout
|
|||||||
}
|
}
|
||||||
await VAT.LoadAsync();
|
await VAT.LoadAsync();
|
||||||
await DiscountCart.EnsureInitializedAsync();
|
await DiscountCart.EnsureInitializedAsync();
|
||||||
await LoadAddresses();
|
var userInfo = await AuthService.GetUserAuthInfo();
|
||||||
|
if (userInfo.HasAddress)
|
||||||
|
await LoadAddresses();
|
||||||
|
else
|
||||||
|
_addresses = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadAddresses()
|
private async Task LoadAddresses()
|
||||||
@@ -57,12 +62,14 @@ public partial class Checkout
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
_addresses = new();
|
_addresses = new();
|
||||||
|
_selectedAddress = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Snackbar.Add($"خطا در بارگذاری آدرسها: {ex.Message}", Severity.Error);
|
Snackbar.Add($"خطا در بارگذاری آدرسها: {ex.Message}", Severity.Error);
|
||||||
_addresses = new();
|
_addresses = new();
|
||||||
|
_selectedAddress = null;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -71,6 +78,30 @@ 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()
|
private async Task PlaceOrder()
|
||||||
{
|
{
|
||||||
if (!CanPlaceOrder || _selectedAddress is null)
|
if (!CanPlaceOrder || _selectedAddress is null)
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
{
|
{
|
||||||
<MudItem xs="6" sm="6" md="3"
|
<MudItem xs="6" sm="6" md="3"
|
||||||
onclick="@(() => NavigateToProduct(p.Id))">
|
onclick="@(() => NavigateToProduct(p.Id))">
|
||||||
|
<div id="@($"shop-product-{p.Id}")" class="h-100">
|
||||||
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden"
|
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden"
|
||||||
Style="cursor:pointer;">
|
Style="cursor:pointer;">
|
||||||
<MudCardContent Class="d-flex flex-column pa-1 h-100">
|
<MudCardContent Class="d-flex flex-column pa-1 h-100">
|
||||||
@@ -127,6 +128,7 @@
|
|||||||
</MudButton>
|
</MudButton>
|
||||||
</MudCardActions>
|
</MudCardActions>
|
||||||
</MudCard>
|
</MudCard>
|
||||||
|
</div>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
}
|
}
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.AspNetCore.Components.Routing;
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
using FrontOffice.Main.Utilities;
|
using FrontOffice.Main.Utilities;
|
||||||
|
|
||||||
namespace FrontOffice.Main.Pages.DiscountStore;
|
namespace FrontOffice.Main.Pages.DiscountStore;
|
||||||
@@ -10,6 +12,7 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
|
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
|
||||||
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
||||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||||
|
[Inject] private IJSRuntime Js { get; set; } = default!;
|
||||||
|
|
||||||
private bool _isAuthenticated;
|
private bool _isAuthenticated;
|
||||||
private string _search = string.Empty;
|
private string _search = string.Empty;
|
||||||
@@ -20,42 +23,107 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
private bool _hasMore = true;
|
private bool _hasMore = true;
|
||||||
private int _totalCount;
|
private int _totalCount;
|
||||||
private const int PageSize = 12;
|
private const int PageSize = 12;
|
||||||
|
private const string DefaultSortBy = "price desc";
|
||||||
|
|
||||||
private List<DiscountProductCard> _products = new();
|
private List<DiscountProductCard> _products = new();
|
||||||
private List<DiscountCategoryNode> _categories = new();
|
private List<DiscountCategoryNode> _categories = new();
|
||||||
|
private bool _ignoreNextLocationChange;
|
||||||
|
private bool _pendingScrollRestore;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
|
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
|
||||||
_loading = true;
|
|
||||||
await DiscountCart.EnsureInitializedAsync();
|
await DiscountCart.EnsureInitializedAsync();
|
||||||
DiscountCart.OnChange += StateHasChanged;
|
DiscountCart.OnChange += StateHasChanged;
|
||||||
var categoriesTask = ProductService.GetCategoriesAsync();
|
Navigation.LocationChanged += HandleLocationChanged;
|
||||||
var productsTask = ProductService.GetProductsAsync(page: 1, pageSize: PageSize);
|
|
||||||
await Task.WhenAll(categoriesTask, productsTask);
|
_categories = await ProductService.GetCategoriesAsync();
|
||||||
_categories = categoriesTask.Result;
|
ApplyStateFromUri();
|
||||||
var result = productsTask.Result;
|
_loading = true;
|
||||||
_products = result.Products;
|
await LoadPages(_currentPage);
|
||||||
_totalCount = result.TotalCount;
|
|
||||||
_hasMore = result.CurrentPage < result.TotalPages;
|
|
||||||
_loading = false;
|
_loading = false;
|
||||||
|
_pendingScrollRestore = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadInitial()
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (_pendingScrollRestore && !_loading && _products.Count > 0)
|
||||||
|
{
|
||||||
|
_pendingScrollRestore = false;
|
||||||
|
var payload = await ShopListScrollRestore.TakeAsync(Js, ShopListScrollRestore.DiscountKey);
|
||||||
|
if (payload is not null)
|
||||||
|
await ShopListScrollRestore.RestoreAsync(Js, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
await base.OnAfterRenderAsync(firstRender);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyStateFromUri()
|
||||||
|
{
|
||||||
|
var state = ShopListQueryState.Parse(Navigation.ToAbsoluteUri(Navigation.Uri));
|
||||||
|
_search = state.Query;
|
||||||
|
_selectedCategoryId = state.CategoryId;
|
||||||
|
_currentPage = state.Pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ShopListQueryState CaptureState() => new()
|
||||||
|
{
|
||||||
|
Query = _search,
|
||||||
|
CategoryId = _selectedCategoryId,
|
||||||
|
Pages = Math.Max(1, _currentPage)
|
||||||
|
};
|
||||||
|
|
||||||
|
private void SyncUrl()
|
||||||
|
{
|
||||||
|
var target = CaptureState().ToRelativeUrl(RouteConstants.DiscountStore.Products);
|
||||||
|
var currentPathAndQuery = Navigation.ToAbsoluteUri(Navigation.Uri).PathAndQuery;
|
||||||
|
if (string.Equals(currentPathAndQuery, target, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_ignoreNextLocationChange = true;
|
||||||
|
Navigation.NavigateTo(target, replace: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadPages(int pagesToLoad)
|
||||||
|
{
|
||||||
|
_products.Clear();
|
||||||
|
_hasMore = true;
|
||||||
|
pagesToLoad = Math.Max(1, pagesToLoad);
|
||||||
|
var search = string.IsNullOrWhiteSpace(_search) ? null : _search;
|
||||||
|
|
||||||
|
for (var page = 1; page <= pagesToLoad; page++)
|
||||||
|
{
|
||||||
|
var result = await ProductService.GetProductsAsync(
|
||||||
|
page: page,
|
||||||
|
pageSize: PageSize,
|
||||||
|
search: search,
|
||||||
|
categoryId: _selectedCategoryId,
|
||||||
|
sortBy: DefaultSortBy);
|
||||||
|
|
||||||
|
if (page == 1)
|
||||||
|
{
|
||||||
|
_products = result.Products;
|
||||||
|
_totalCount = result.TotalCount;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_products.AddRange(result.Products);
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentPage = page;
|
||||||
|
_hasMore = result.CurrentPage < result.TotalPages;
|
||||||
|
if (!_hasMore)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReloadFromFilters()
|
||||||
{
|
{
|
||||||
_loading = true;
|
_loading = true;
|
||||||
_currentPage = 1;
|
_currentPage = 1;
|
||||||
_products.Clear();
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
|
await LoadPages(1);
|
||||||
var result = await ProductService.GetProductsAsync(
|
SyncUrl();
|
||||||
page: 1,
|
|
||||||
pageSize: PageSize,
|
|
||||||
search: string.IsNullOrWhiteSpace(_search) ? null : _search,
|
|
||||||
categoryId: _selectedCategoryId);
|
|
||||||
_products = result.Products;
|
|
||||||
_totalCount = result.TotalCount;
|
|
||||||
_hasMore = result.CurrentPage < result.TotalPages;
|
|
||||||
_loading = false;
|
_loading = false;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
@@ -63,37 +131,37 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
private async Task LoadMore()
|
private async Task LoadMore()
|
||||||
{
|
{
|
||||||
if (_loadingMore || !_hasMore) return;
|
if (_loadingMore || !_hasMore) return;
|
||||||
|
|
||||||
_loadingMore = true;
|
_loadingMore = true;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
|
|
||||||
_currentPage++;
|
_currentPage++;
|
||||||
var result = await ProductService.GetProductsAsync(
|
var result = await ProductService.GetProductsAsync(
|
||||||
page: _currentPage,
|
page: _currentPage,
|
||||||
pageSize: PageSize,
|
pageSize: PageSize,
|
||||||
search: string.IsNullOrWhiteSpace(_search) ? null : _search,
|
search: string.IsNullOrWhiteSpace(_search) ? null : _search,
|
||||||
categoryId: _selectedCategoryId);
|
categoryId: _selectedCategoryId,
|
||||||
|
sortBy: DefaultSortBy);
|
||||||
_products.AddRange(result.Products);
|
_products.AddRange(result.Products);
|
||||||
_hasMore = result.CurrentPage < result.TotalPages;
|
_hasMore = result.CurrentPage < result.TotalPages;
|
||||||
|
SyncUrl();
|
||||||
|
|
||||||
_loadingMore = false;
|
_loadingMore = false;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SearchProducts() => await LoadInitial();
|
private async Task SearchProducts() => await ReloadFromFilters();
|
||||||
|
|
||||||
private async Task OnSearchKeyUp(KeyboardEventArgs e)
|
private async Task OnSearchKeyUp(KeyboardEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Key == "Enter")
|
if (e.Key == "Enter")
|
||||||
{
|
await ReloadFromFilters();
|
||||||
await LoadInitial();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnCategoryChanged(long? value)
|
private async Task OnCategoryChanged(long? value)
|
||||||
{
|
{
|
||||||
_selectedCategoryId = value;
|
_selectedCategoryId = value;
|
||||||
await LoadInitial();
|
await ReloadFromFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task AddToCart(DiscountProductCard p)
|
private async Task AddToCart(DiscountProductCard p)
|
||||||
@@ -101,15 +169,44 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
await GuestGate.RunAsync(() => DiscountCart.AddAsync(p.Id));
|
await GuestGate.RunAsync(() => DiscountCart.AddAsync(p.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void NavigateToProduct(long id)
|
private async Task NavigateToProduct(long id)
|
||||||
{
|
{
|
||||||
|
await ShopListScrollRestore.SaveAsync(Js, ShopListScrollRestore.DiscountKey, id);
|
||||||
Navigation.NavigateTo($"{RouteConstants.DiscountStore.ProductDetail}{id}");
|
Navigation.NavigateTo($"{RouteConstants.DiscountStore.ProductDetail}{id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HandleLocationChanged(object? sender, LocationChangedEventArgs args)
|
||||||
|
{
|
||||||
|
if (_ignoreNextLocationChange)
|
||||||
|
{
|
||||||
|
_ignoreNextLocationChange = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var uri = Navigation.ToAbsoluteUri(args.Location);
|
||||||
|
if (!uri.AbsolutePath.Equals(RouteConstants.DiscountStore.Products, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var incoming = ShopListQueryState.Parse(uri);
|
||||||
|
if (incoming.Matches(CaptureState()))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_ = InvokeAsync(async () =>
|
||||||
|
{
|
||||||
|
ApplyStateFromUri();
|
||||||
|
_loading = true;
|
||||||
|
await LoadPages(_currentPage);
|
||||||
|
_loading = false;
|
||||||
|
_pendingScrollRestore = true;
|
||||||
|
StateHasChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static string FormatPrice(long price) => $"{price:N0} تومان";
|
private static string FormatPrice(long price) => $"{price:N0} تومان";
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
DiscountCart.OnChange -= StateHasChanged;
|
DiscountCart.OnChange -= StateHasChanged;
|
||||||
|
Navigation.LocationChanged -= HandleLocationChanged;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,8 +94,47 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@* ═══════════════════════════════════════════════
|
@* ═══════════════════════════════════════════════
|
||||||
2. HOW IT WORKS — 3 numbered steps
|
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">
|
<section class="section-landing">
|
||||||
<MudContainer MaxWidth="MaxWidth.Large">
|
<MudContainer MaxWidth="MaxWidth.Large">
|
||||||
<div class="text-center mb-8 fade-in-up">
|
<div class="text-center mb-8 fade-in-up">
|
||||||
@@ -121,10 +160,14 @@
|
|||||||
</MudGrid>
|
</MudGrid>
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
</section>
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
@* ═══════════════════════════════════════════════
|
@* ═══════════════════════════════════════════════
|
||||||
3. FEATURES — 6 cards with icons
|
3. FEATURES — 6 cards with icons
|
||||||
|
(appsettings Landing:ShowFeaturesSection; default false)
|
||||||
═══════════════════════════════════════════════ *@
|
═══════════════════════════════════════════════ *@
|
||||||
|
@if (_showFeaturesSection)
|
||||||
|
{
|
||||||
<section class="section-landing" style="background:var(--mud-palette-background-gray);">
|
<section class="section-landing" style="background:var(--mud-palette-background-gray);">
|
||||||
<MudContainer MaxWidth="MaxWidth.Large">
|
<MudContainer MaxWidth="MaxWidth.Large">
|
||||||
<div class="text-center mb-8 fade-in-up">
|
<div class="text-center mb-8 fade-in-up">
|
||||||
@@ -152,6 +195,7 @@
|
|||||||
</MudGrid>
|
</MudGrid>
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
</section>
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
@* ═══════════════════════════════════════════════
|
@* ═══════════════════════════════════════════════
|
||||||
4. STATS — Counter cards
|
4. STATS — Counter cards
|
||||||
@@ -429,6 +473,8 @@ else
|
|||||||
@* ═══════════════════════════════════════════════
|
@* ═══════════════════════════════════════════════
|
||||||
6. TESTIMONIALS
|
6. TESTIMONIALS
|
||||||
═══════════════════════════════════════════════ *@
|
═══════════════════════════════════════════════ *@
|
||||||
|
@if (_testimonials.Count > 0)
|
||||||
|
{
|
||||||
<section class="section-landing">
|
<section class="section-landing">
|
||||||
<MudContainer MaxWidth="MaxWidth.Large">
|
<MudContainer MaxWidth="MaxWidth.Large">
|
||||||
<div class="text-center mb-8 fade-in-up">
|
<div class="text-center mb-8 fade-in-up">
|
||||||
@@ -465,6 +511,7 @@ else
|
|||||||
</MudGrid>
|
</MudGrid>
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
</section>
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
@* ═══════════════════════════════════════════════
|
@* ═══════════════════════════════════════════════
|
||||||
7. FAQ
|
7. FAQ
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using FrontOffice.Main.Utilities;
|
using FrontOffice.Main.Utilities;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
|
|
||||||
@@ -16,8 +17,12 @@ public partial class Index : IDisposable
|
|||||||
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
|
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
|
||||||
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
||||||
[Inject] private VATService VAT { get; set; } = default!;
|
[Inject] private VATService VAT { get; set; } = default!;
|
||||||
|
[Inject] private IConfiguration Configuration { get; set; } = default!;
|
||||||
|
|
||||||
private bool _isAuthenticated;
|
private bool _isAuthenticated;
|
||||||
|
private string _twentyCardPortalUrl = string.Empty;
|
||||||
|
private bool _showStepsSection;
|
||||||
|
private bool _showFeaturesSection;
|
||||||
|
|
||||||
// ── CMS page data ──
|
// ── CMS page data ──
|
||||||
private PageSettingsDto? _pageData;
|
private PageSettingsDto? _pageData;
|
||||||
@@ -49,6 +54,10 @@ public partial class Index : IDisposable
|
|||||||
{
|
{
|
||||||
MainService.OnChangeHandler += OnStateChanged;
|
MainService.OnChangeHandler += OnStateChanged;
|
||||||
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
|
_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
|
// Load landing page settings from CMS
|
||||||
try
|
try
|
||||||
@@ -162,7 +171,7 @@ public partial class Index : IDisposable
|
|||||||
(Icons.Material.Outlined.VerifiedUser, "ثبتنام سریع و ساده", "در چند گام کوتاه حساب بسازید و شروع کنید.", 0),
|
(Icons.Material.Outlined.VerifiedUser, "ثبتنام سریع و ساده", "در چند گام کوتاه حساب بسازید و شروع کنید.", 0),
|
||||||
(Icons.Material.Outlined.StarBorder, "پاداشهای شفاف", "قوانین روشن، دسترسی آسان به سوابق و گزارشها.", 100),
|
(Icons.Material.Outlined.StarBorder, "پاداشهای شفاف", "قوانین روشن، دسترسی آسان به سوابق و گزارشها.", 100),
|
||||||
(Icons.Material.Outlined.Devices, "طراحی واکنشگرا", "تجربهای روان در موبایل و دسکتاپ.", 200),
|
(Icons.Material.Outlined.Devices, "طراحی واکنشگرا", "تجربهای روان در موبایل و دسکتاپ.", 200),
|
||||||
(Icons.Material.Outlined.Groups, "تیمسازی هوشمند", "ساختار درختی شبکه و مدیریت تیمهای فروش.", 300),
|
(Icons.Material.Outlined.Groups, "تیمسازی هوشمند", "ساختار درختی سازمان و مدیریت تیمهای فروش.", 300),
|
||||||
(Icons.Material.Outlined.Insights, "گزارشهای لحظهای", "داشبورد پویا برای مشاهده عملکرد و کمیسیون.", 400),
|
(Icons.Material.Outlined.Insights, "گزارشهای لحظهای", "داشبورد پویا برای مشاهده عملکرد و کمیسیون.", 400),
|
||||||
(Icons.Material.Outlined.Lock, "امنیت بالا", "رمزنگاری اطلاعات و احراز هویت چندمرحلهای.", 500),
|
(Icons.Material.Outlined.Lock, "امنیت بالا", "رمزنگاری اطلاعات و احراز هویت چندمرحلهای.", 500),
|
||||||
};
|
};
|
||||||
@@ -193,9 +202,10 @@ public partial class Index : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Testimonials ──
|
// ── Testimonials ──
|
||||||
if (_settings?.Testimonials?.Any() == true)
|
// اگر تنظیمات از CMS لود شده، لیست خالی یعنی ادمین عمداً حذف کرده — بدون fallback hardcode
|
||||||
|
if (_settings is not null)
|
||||||
{
|
{
|
||||||
_testimonials = _settings.Testimonials
|
_testimonials = (_settings.Testimonials ?? [])
|
||||||
.Select((t, i) => new TestimonialItem(t.Quote ?? "", t.Name ?? "", t.Role ?? "", i * 150))
|
.Select((t, i) => new TestimonialItem(t.Quote ?? "", t.Name ?? "", t.Role ?? "", i * 150))
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
@@ -297,6 +307,12 @@ public partial class Index : IDisposable
|
|||||||
Navigation.NavigateTo(RouteConstants.Registration.Wizard);
|
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)
|
private void NavigateToPost(string slug)
|
||||||
{
|
{
|
||||||
Navigation.NavigateTo($"/blog/{slug}");
|
Navigation.NavigateTo($"/blog/{slug}");
|
||||||
@@ -333,8 +349,10 @@ public partial class Index : IDisposable
|
|||||||
public string? HeroButtonPrimaryText { get; set; }
|
public string? HeroButtonPrimaryText { get; set; }
|
||||||
public string? HeroButtonSecondaryText { get; set; }
|
public string? HeroButtonSecondaryText { get; set; }
|
||||||
public List<TrustBadgeItem>? TrustBadges { get; set; }
|
public List<TrustBadgeItem>? TrustBadges { get; set; }
|
||||||
|
public bool? StepsEnabled { get; set; }
|
||||||
public string? StepsTitle { get; set; }
|
public string? StepsTitle { get; set; }
|
||||||
public List<StepItem>? Steps { get; set; }
|
public List<StepItem>? Steps { get; set; }
|
||||||
|
public bool? FeaturesEnabled { get; set; }
|
||||||
public string? FeaturesTitle { get; set; }
|
public string? FeaturesTitle { get; set; }
|
||||||
public List<FeatureItem>? Features { get; set; }
|
public List<FeatureItem>? Features { get; set; }
|
||||||
public string? StatsTitle { get; set; }
|
public string? StatsTitle { get; set; }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
using FrontOffice.Main.Utilities;
|
||||||
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
||||||
using FrontOffice.Main.Pages.Profile.Components;
|
using FrontOffice.Main.Pages.Profile.Components;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
@@ -9,6 +9,7 @@ namespace FrontOffice.Main.Pages.Profile;
|
|||||||
public partial class Addresses : ComponentBase
|
public partial class Addresses : ComponentBase
|
||||||
{
|
{
|
||||||
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
||||||
|
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||||
|
|
||||||
private List<CustomerAddressModel> _addresses = new();
|
private List<CustomerAddressModel> _addresses = new();
|
||||||
private bool _isLoadingAddresses;
|
private bool _isLoadingAddresses;
|
||||||
@@ -42,7 +43,10 @@ public partial class Addresses : ComponentBase
|
|||||||
var dialog = await DialogService.ShowAsync<AddAddressDialog>("افزودن آدرس جدید");
|
var dialog = await DialogService.ShowAsync<AddAddressDialog>("افزودن آدرس جدید");
|
||||||
var result = await dialog.Result;
|
var result = await dialog.Result;
|
||||||
if (result is not null && !result.Canceled)
|
if (result is not null && !result.Canceled)
|
||||||
|
{
|
||||||
await LoadAddresses();
|
await LoadAddresses();
|
||||||
|
await AuthService.RefreshTokenAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OpenEditAddressDialog(CustomerAddressModel address)
|
private async Task OpenEditAddressDialog(CustomerAddressModel address)
|
||||||
@@ -80,6 +84,7 @@ public partial class Addresses : ComponentBase
|
|||||||
await UserAddressContract.DeleteCustomerAddressAsync(new() { Id = id });
|
await UserAddressContract.DeleteCustomerAddressAsync(new() { Id = id });
|
||||||
Snackbar.Add("آدرس حذف شد.", Severity.Success);
|
Snackbar.Add("آدرس حذف شد.", Severity.Success);
|
||||||
await LoadAddresses();
|
await LoadAddresses();
|
||||||
|
await AuthService.RefreshTokenAsync();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -35,12 +35,97 @@
|
|||||||
</MudAlert>
|
</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">
|
<MudPaper Elevation="2" Class="pa-5 rounded-lg">
|
||||||
<MudText Typo="Typo.h6" Class="mb-3">
|
<MudText Typo="Typo.h6" Class="mb-3">
|
||||||
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Class="ml-1" />
|
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Class="ml-1" />
|
||||||
شارژ موجودی اصلی
|
شارژ موجودی اصلی
|
||||||
</MudText>
|
</MudText>
|
||||||
|
|
||||||
|
@if (_allowNormalDespiteMagic)
|
||||||
|
{
|
||||||
|
<MudAlert Severity="MudBlazor.Severity.Warning" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Warning">
|
||||||
|
این شارژ بدون ضریب جادویی است (۱:۱). سقف واریز جادویی شما در این دور پر شده است.
|
||||||
|
</MudAlert>
|
||||||
|
}
|
||||||
|
|
||||||
<MudAlert Severity="MudBlazor.Severity.Info" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info">
|
<MudAlert Severity="MudBlazor.Severity.Info" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info">
|
||||||
مبلغ وارد شده (به تومان) از طریق درگاه پرداخت به موجودی اصلی شما اضافه میشود.
|
مبلغ وارد شده (به تومان) از طریق درگاه پرداخت به موجودی اصلی شما اضافه میشود.
|
||||||
از موجودی اصلی برای خرید از فروشگاه عادی و پرداخت سفارش استفاده میشود.
|
از موجودی اصلی برای خرید از فروشگاه عادی و پرداخت سفارش استفاده میشود.
|
||||||
@@ -122,5 +207,6 @@
|
|||||||
</MudList>
|
</MudList>
|
||||||
</MudExpansionPanel>
|
</MudExpansionPanel>
|
||||||
</MudExpansionPanels>
|
</MudExpansionPanels>
|
||||||
|
}
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
|
|||||||
@@ -7,10 +7,18 @@ namespace FrontOffice.Main.Pages.Profile;
|
|||||||
|
|
||||||
public partial class ChargeCreditWallet : ComponentBase
|
public partial class ChargeCreditWallet : ComponentBase
|
||||||
{
|
{
|
||||||
|
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||||
|
|
||||||
private bool _isProcessing;
|
private bool _isProcessing;
|
||||||
private long _chargeAmount;
|
private long _chargeAmount;
|
||||||
private string? _paymentResult;
|
private string? _paymentResult;
|
||||||
private string? _pendingReturnUrl;
|
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 };
|
private readonly long[] _presetAmounts = { 500_000, 1_000_000, 5_000_000, 10_000_000, 20_000_000, 50_000_000 };
|
||||||
|
|
||||||
@@ -23,7 +31,13 @@ public partial class ChargeCreditWallet : ComponentBase
|
|||||||
[SupplyParameterFromQuery(Name = "returnUrl")]
|
[SupplyParameterFromQuery(Name = "returnUrl")]
|
||||||
public string? ReturnUrlQueryParam { get; set; }
|
public string? ReturnUrlQueryParam { get; set; }
|
||||||
|
|
||||||
protected override void OnInitialized()
|
private bool ShowChargeForm =>
|
||||||
|
_authLoaded
|
||||||
|
&& _isClubMemberActive
|
||||||
|
&& !_redirectingToMagic
|
||||||
|
&& (!_needsMagicCeilingConsent || _allowNormalDespiteMagic);
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
_paymentResult = PaymentQueryParam;
|
_paymentResult = PaymentQueryParam;
|
||||||
|
|
||||||
@@ -32,20 +46,63 @@ public partial class ChargeCreditWallet : ComponentBase
|
|||||||
|
|
||||||
if (CreditChargeNavigation.IsValidReturnUrl(ReturnUrlQueryParam))
|
if (CreditChargeNavigation.IsValidReturnUrl(ReturnUrlQueryParam))
|
||||||
_pendingReturnUrl = 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)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
if (firstRender && !string.IsNullOrEmpty(_pendingReturnUrl))
|
if (firstRender && !string.IsNullOrEmpty(_pendingReturnUrl) && _isClubMemberActive && !_redirectingToMagic)
|
||||||
{
|
|
||||||
await CreditChargeNavigation.SaveReturnUrlAsync(JSRuntime, _pendingReturnUrl);
|
await CreditChargeNavigation.SaveReturnUrlAsync(JSRuntime, _pendingReturnUrl);
|
||||||
}
|
|
||||||
|
|
||||||
await base.OnAfterRenderAsync(firstRender);
|
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()
|
private async Task StartCreditCharge()
|
||||||
{
|
{
|
||||||
|
if (!_isClubMemberActive)
|
||||||
|
{
|
||||||
|
Snackbar.Add(
|
||||||
|
"برای شارژ کیف پول اصلی ابتدا باید پکیج را خریداری کرده و قرارداد باشگاه مشتریان را امضا کنید.",
|
||||||
|
Severity.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await EvaluateMagicWalletGateAsync())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_needsMagicCeilingConsent && !_allowNormalDespiteMagic)
|
||||||
|
return;
|
||||||
|
|
||||||
if (_chargeAmount <= 0 || _isProcessing) return;
|
if (_chargeAmount <= 0 || _isProcessing) return;
|
||||||
|
|
||||||
_isProcessing = true;
|
_isProcessing = true;
|
||||||
@@ -84,6 +141,50 @@ 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()
|
private async Task ContinueShoppingAsync()
|
||||||
{
|
{
|
||||||
var returnUrl = await CreditChargeNavigation.GetReturnUrlAsync(JSRuntime);
|
var returnUrl = await CreditChargeNavigation.GetReturnUrlAsync(JSRuntime);
|
||||||
|
|||||||
@@ -7,6 +7,16 @@
|
|||||||
<MudStack Spacing="3">
|
<MudStack Spacing="3">
|
||||||
<PageHeader Title="💳 شارژ کیفپول اعتباری" BackHref="@RouteConstants.Profile.Wallet" />
|
<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 برگشته) ─── *@
|
@* ─── نتیجه پرداخت (اگه از callback برگشته) ─── *@
|
||||||
@if (_paymentResult != null)
|
@if (_paymentResult != null)
|
||||||
{
|
{
|
||||||
@@ -117,5 +127,6 @@
|
|||||||
</MudList>
|
</MudList>
|
||||||
</MudExpansionPanel>
|
</MudExpansionPanel>
|
||||||
</MudExpansionPanels>
|
</MudExpansionPanels>
|
||||||
|
}
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
|
|||||||
@@ -52,29 +52,47 @@
|
|||||||
<div id="org-chart-container" class="org-chart-container"></div>
|
<div id="org-chart-container" class="org-chart-container"></div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@* Stats Bar *@
|
@* Stats Bar — فقط آمار خود کاربر (نه نودهای درخت) *@
|
||||||
@if (_statistics != null && !_isLoading)
|
@if (_statistics != null && !_isLoading)
|
||||||
{
|
{
|
||||||
<div class="org-chart-stats">
|
<div class="org-chart-stats">
|
||||||
<div class="stat-item">
|
<div class="org-chart-stats-row">
|
||||||
<span class="stat-label">کل اعضا:</span>
|
<div class="stat-item">
|
||||||
<span class="stat-value">@_statistics.TotalMembers</span>
|
<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>
|
</div>
|
||||||
<div class="stat-item">
|
<div class="org-chart-stats-row leg-scores">
|
||||||
<span class="stat-label">پای چپ:</span>
|
<div class="stat-item leg-score-gold">
|
||||||
<span class="stat-value left">@_statistics.LeftLegCount</span>
|
<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>
|
</div>
|
||||||
<div class="stat-item">
|
<div class="org-chart-stats-row leg-scores">
|
||||||
<span class="stat-label">پای راست:</span>
|
<div class="stat-item leg-score-silver">
|
||||||
<span class="stat-value right">@_statistics.RightLegCount</span>
|
<span class="stat-label">پکیج نقرهای:</span>
|
||||||
</div>
|
<span class="stat-value left">چپ @_statistics.SilverLeftLegTotal</span>
|
||||||
<div class="stat-item">
|
<span class="stat-sep">·</span>
|
||||||
<span class="stat-label">عمق:</span>
|
<span class="stat-value right">راست @_statistics.SilverRightLegTotal</span>
|
||||||
<span class="stat-value">@_statistics.TreeDepth</span>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="stat-item">
|
|
||||||
<span class="stat-label">پای ضعیف:</span>
|
|
||||||
<span class="stat-value">@(_statistics.WeakerLeg == "Left" ? "چپ" : "راست")</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using FrontOffice.Main.Utilities;
|
using DateTimeConverterCL;
|
||||||
|
using FrontOffice.Main.Utilities;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
@@ -108,7 +109,9 @@ public partial class OrganizationChart : IAsyncDisposable
|
|||||||
isActive = n.IsActive,
|
isActive = n.IsActive,
|
||||||
isClubActive = n.IsClubActive,
|
isClubActive = n.IsClubActive,
|
||||||
activationWeekNumber = n.ActivationWeekNumber,
|
activationWeekNumber = n.ActivationWeekNumber,
|
||||||
joinedAt = n.JoinedAt,
|
registeredAt = n.JoinedAt.HasValue
|
||||||
|
? n.JoinedAt.Value.ToLocalTime().MiladiToJalali()
|
||||||
|
: "",
|
||||||
referralCode = n.ReferralCode,
|
referralCode = n.ReferralCode,
|
||||||
packageName = n.PackageName ?? string.Empty
|
packageName = n.PackageName ?? string.Empty
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
|
|||||||
@@ -46,6 +46,21 @@
|
|||||||
</MudPaper>
|
</MudPaper>
|
||||||
</MudLink>
|
</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">
|
<MudLink Href="@RouteConstants.Profile.Settings" Underline="Underline.None">
|
||||||
<MudPaper Elevation="0" Class="pa-4 rounded-xl profile-hub-item">
|
<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" Justify="Justify.SpaceBetween">
|
||||||
|
|||||||
@@ -109,6 +109,13 @@
|
|||||||
{
|
{
|
||||||
<MudText Typo="Typo.caption" Color="Color.Success" Class="mt-2">@(_copyMessage)</MudText>
|
<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>
|
</MudPaper>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -121,6 +128,13 @@
|
|||||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-1" Style="max-width:560px; margin:0 auto;">
|
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-1" Style="max-width:560px; margin:0 auto;">
|
||||||
برای فعالسازی لینک دعوت، ابتدا یکی از <strong>پکیجها</strong> را تهیه کنید و سپس در <strong>باشگاه مشتریان</strong> عضو شوید.
|
برای فعالسازی لینک دعوت، ابتدا یکی از <strong>پکیجها</strong> را تهیه کنید و سپس در <strong>باشگاه مشتریان</strong> عضو شوید.
|
||||||
</MudText>
|
</MudText>
|
||||||
|
<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 Typo="Typo.body2" Class="mud-text-secondary mb-4" Style="max-width:560px; margin:0 auto;">
|
||||||
از مزایای ویژه عضویت بهرهمند شده و با فعالیت در زمینه توسعه فروشگاهها پاداش دریافت کنید.
|
از مزایای ویژه عضویت بهرهمند شده و با فعالیت در زمینه توسعه فروشگاهها پاداش دریافت کنید.
|
||||||
</MudText>
|
</MudText>
|
||||||
|
|||||||
@@ -333,6 +333,7 @@ public partial class Index
|
|||||||
if (!result.Canceled)
|
if (!result.Canceled)
|
||||||
{
|
{
|
||||||
await LoadAddresses();
|
await LoadAddresses();
|
||||||
|
await AuthService.RefreshTokenAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,6 +386,7 @@ public partial class Index
|
|||||||
});
|
});
|
||||||
Snackbar.Add("آدرس با موفقیت حذف شد.", Severity.Success);
|
Snackbar.Add("آدرس با موفقیت حذف شد.", Severity.Success);
|
||||||
await LoadAddresses();
|
await LoadAddresses();
|
||||||
|
await AuthService.RefreshTokenAsync();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -419,7 +421,10 @@ public partial class Index
|
|||||||
switch (purchase.Method)
|
switch (purchase.Method)
|
||||||
{
|
{
|
||||||
case Shared.PackagePurchaseDialog.PaymentMethodType.DirectPayment:
|
case Shared.PackagePurchaseDialog.PaymentMethodType.DirectPayment:
|
||||||
await ProcessDirectPayment(purchase.Package.Id);
|
await ProcessDirectPayment(purchase.Package.Id, gatewayProvider: null);
|
||||||
|
break;
|
||||||
|
case Shared.PackagePurchaseDialog.PaymentMethodType.TwentyCard:
|
||||||
|
await ProcessTwentyCardPayment(purchase.Package.Id);
|
||||||
break;
|
break;
|
||||||
case Shared.PackagePurchaseDialog.PaymentMethodType.DayaLoan:
|
case Shared.PackagePurchaseDialog.PaymentMethodType.DayaLoan:
|
||||||
await ProcessDayaLoanPayment();
|
await ProcessDayaLoanPayment();
|
||||||
@@ -428,7 +433,25 @@ public partial class Index
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ProcessDirectPayment(long packageId)
|
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)
|
||||||
{
|
{
|
||||||
if (_isProcessingPayment) return;
|
if (_isProcessingPayment) return;
|
||||||
|
|
||||||
@@ -437,11 +460,15 @@ public partial class Index
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var response = await PackageContract.CustomerPurchasePackageAsync(new CustomerPurchasePackageRequest
|
var request = new CustomerPurchasePackageRequest
|
||||||
{
|
{
|
||||||
PackageId = packageId,
|
PackageId = packageId,
|
||||||
PurchaseMethod = PurchaseMethodEnum.PurchaseMethodGateway
|
PurchaseMethod = PurchaseMethodEnum.PurchaseMethodGateway
|
||||||
});
|
};
|
||||||
|
if (!string.IsNullOrWhiteSpace(gatewayProvider))
|
||||||
|
request.GatewayProvider = gatewayProvider;
|
||||||
|
|
||||||
|
var response = await PackageContract.CustomerPurchasePackageAsync(request);
|
||||||
|
|
||||||
if (!response.Success)
|
if (!response.Success)
|
||||||
{
|
{
|
||||||
@@ -491,11 +518,6 @@ public partial class Index
|
|||||||
Snackbar.Add("لطفا اطلاعات شخصی خود را تکمیل کنید. (تاریخ تولد وارد نشده)", Severity.Error);
|
Snackbar.Add("لطفا اطلاعات شخصی خود را تکمیل کنید. (تاریخ تولد وارد نشده)", Severity.Error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!_addresses.Any())
|
|
||||||
{
|
|
||||||
Snackbar.Add("آدرس محل سکونت شما الزامی است!", Severity.Error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var url = "https://dayadiamond.ir/profile/creditpurchase/?merchantcode=56146364";
|
var url = "https://dayadiamond.ir/profile/creditpurchase/?merchantcode=56146364";
|
||||||
await JSRuntime.InvokeVoidAsync("open", url, "_blank");
|
await JSRuntime.InvokeVoidAsync("open", url, "_blank");
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
@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,8 +166,23 @@
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<MudAlert Severity="MudBlazor.Severity.Warning" Class="rounded-lg">
|
<MudAlert Severity="MudBlazor.Severity.Warning" Class="rounded-lg">
|
||||||
سقف شارژ جادویی در این دور پر شده است.
|
<MudStack Spacing="2">
|
||||||
با خرج کردن موجودی و خرید مجدد پکیج، سقف ریست میشود.
|
<MudText>
|
||||||
|
سقف شارژ جادویی در این دور پر شده است.
|
||||||
|
با خرج کردن موجودی و خرید مجدد پکیج، سقف ریست میشود.
|
||||||
|
</MudText>
|
||||||
|
<MudText Typo="Typo.body2">
|
||||||
|
اگر برای تکمیل خرید به موجودی بیشتری نیاز دارید، میتوانید بهصورت استثنایی کیف اصلی را بدون ضریب (۱:۱) شارژ کنید.
|
||||||
|
</MudText>
|
||||||
|
<MudButton Variant="Variant.Filled"
|
||||||
|
Color="Color.Primary"
|
||||||
|
Size="Size.Small"
|
||||||
|
StartIcon="@Icons.Material.Filled.AccountBalanceWallet"
|
||||||
|
Href="@RouteConstants.Profile.ChargeCreditWallet"
|
||||||
|
Class="rounded-lg align-self-start">
|
||||||
|
شارژ عادی بدون ضریب
|
||||||
|
</MudButton>
|
||||||
|
</MudStack>
|
||||||
</MudAlert>
|
</MudAlert>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,10 @@
|
|||||||
|
|
||||||
[SupplyParameterFromQuery(Name = "Status")]
|
[SupplyParameterFromQuery(Name = "Status")]
|
||||||
private string? Status { get; set; }
|
private string? Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>۲۰کارت callback — هممعنی Status زرینپال</summary>
|
||||||
|
[SupplyParameterFromQuery(Name = "PaymentStatus")]
|
||||||
|
private string? PaymentStatus { get; set; }
|
||||||
|
|
||||||
private bool _isLoading = true;
|
private bool _isLoading = true;
|
||||||
private bool _isSuccess;
|
private bool _isSuccess;
|
||||||
@@ -132,6 +136,10 @@
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// ۲۰کارت: PaymentStatus؛ زرینپال: Status
|
||||||
|
if (string.IsNullOrWhiteSpace(Status) && !string.IsNullOrWhiteSpace(PaymentStatus))
|
||||||
|
Status = PaymentStatus;
|
||||||
|
|
||||||
// تعیین نوع پرداخت — اگر type نباشد، پیشفرض خرید پکیج
|
// تعیین نوع پرداخت — اگر type نباشد، پیشفرض خرید پکیج
|
||||||
var type = PaymentType?.ToLowerInvariant() ?? "package";
|
var type = PaymentType?.ToLowerInvariant() ?? "package";
|
||||||
|
|
||||||
|
|||||||
@@ -38,20 +38,97 @@
|
|||||||
درخواستهای برداشت
|
درخواستهای برداشت
|
||||||
</MudButton>
|
</MudButton>
|
||||||
|
|
||||||
<!-- دکمه شارژ کیفپول اصلی -->
|
<!-- دکمه شارژ کیفپول اصلی / جادویی -->
|
||||||
<MudButton Variant="Variant.Filled"
|
@if (_isClubMemberActive)
|
||||||
Color="Color.Primary"
|
{
|
||||||
Size="Size.Large"
|
@if (_isMagicWallet)
|
||||||
FullWidth="true"
|
{
|
||||||
StartIcon="@Icons.Material.Filled.AccountBalanceWallet"
|
<MudButton Variant="Variant.Filled"
|
||||||
Href="@RouteConstants.Profile.ChargeCreditWallet"
|
Color="Color.Secondary"
|
||||||
Class="rounded-lg">
|
Size="Size.Large"
|
||||||
شارژ کیفپول اصلی
|
FullWidth="true"
|
||||||
</MudButton>
|
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>
|
||||||
|
}
|
||||||
|
|
||||||
|
@* ── Temporarily hidden: discount-wallet gateway charge (restore when needed)
|
||||||
<!-- دکمه شارژ کیفپول اعتباری -->
|
<!-- دکمه شارژ کیفپول اعتباری -->
|
||||||
<MudButton Variant="Variant.Filled"
|
<MudButton Variant="Variant.Filled"
|
||||||
Color="Color.Info"
|
Color="Color.Info"
|
||||||
Size="Size.Large"
|
Size="Size.Large"
|
||||||
FullWidth="true"
|
FullWidth="true"
|
||||||
StartIcon="@Icons.Material.Filled.CreditCard"
|
StartIcon="@Icons.Material.Filled.CreditCard"
|
||||||
@@ -59,6 +136,7 @@
|
|||||||
Class="rounded-lg">
|
Class="rounded-lg">
|
||||||
شارژ کیفپول اعتباری
|
شارژ کیفپول اعتباری
|
||||||
</MudButton>
|
</MudButton>
|
||||||
|
── *@
|
||||||
|
|
||||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
||||||
<MudText Typo="Typo.h6" Class="mb-2">تراکنشها و مسیرهای شارژ</MudText>
|
<MudText Typo="Typo.h6" Class="mb-2">تراکنشها و مسیرهای شارژ</MudText>
|
||||||
|
|||||||
@@ -6,13 +6,43 @@ namespace FrontOffice.Main.Pages.Profile;
|
|||||||
|
|
||||||
public partial class Wallet : ComponentBase
|
public partial class Wallet : ComponentBase
|
||||||
{
|
{
|
||||||
|
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||||
|
|
||||||
private (string Credit, string Discount, string Network) _balances = ("-", "-", "-");
|
private (string Credit, string Discount, string Network) _balances = ("-", "-", "-");
|
||||||
private List<WalletTransaction> _txs = new();
|
private List<WalletTransaction> _txs = new();
|
||||||
private string? _filterReferenceId;
|
private string? _filterReferenceId;
|
||||||
private string _filterType = "all";
|
private string _filterType = "all";
|
||||||
|
private bool _isClubMemberActive;
|
||||||
|
private bool _hasPurchasedPackage;
|
||||||
|
private bool _isMagicWallet;
|
||||||
|
private bool _magicCeilingFull;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userInfo = await AuthService.GetUserAuthInfo();
|
||||||
|
_isClubMemberActive = userInfo.IsClubMemberActive;
|
||||||
|
_hasPurchasedPackage = userInfo.HasPurchasedPackage;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_isClubMemberActive = false;
|
||||||
|
_hasPurchasedPackage = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var magicStatus = await WalletService.GetMagicWalletStatusAsync();
|
||||||
|
_isMagicWallet = magicStatus.WalletMode == 1;
|
||||||
|
_magicCeilingFull = _isMagicWallet && magicStatus.MagicRemainingDeposit <= 0;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_isMagicWallet = false;
|
||||||
|
_magicCeilingFull = false;
|
||||||
|
}
|
||||||
|
|
||||||
var b = await WalletService.GetBalancesAsync();
|
var b = await WalletService.GetBalancesAsync();
|
||||||
_balances = (FormatPrice(b.CreditBalance), FormatPrice(b.DiscountBalance), FormatPrice(b.NetworkBalance));
|
_balances = (FormatPrice(b.CreditBalance), FormatPrice(b.DiscountBalance), FormatPrice(b.NetworkBalance));
|
||||||
_txs = await WalletService.GetTransactionsAsync();
|
_txs = await WalletService.GetTransactionsAsync();
|
||||||
|
|||||||
@@ -58,8 +58,19 @@ public partial class WithdrawalRequests : ComponentBase
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_isSubmittingWithdrawal = true;
|
_isSubmittingWithdrawal = true;
|
||||||
_withdrawIban=_withdrawIban!.Trim().ToUpper().Replace("IR", "").Replace(" ", "");
|
|
||||||
await WalletService.RequestWithdrawalAsync(_selectedPayout.Id, _withdrawMethod, "IR"+_withdrawIban);
|
string? normalizedIban = null;
|
||||||
|
if (_withdrawMethod == WithdrawalMethodClient.Cash)
|
||||||
|
{
|
||||||
|
normalizedIban = NormalizeIranianIban(_withdrawIban);
|
||||||
|
if (normalizedIban is null)
|
||||||
|
{
|
||||||
|
Snackbar.Add("فرمت شماره شبا معتبر نیست. باید IR و ۲۴ رقم باشد (خط تیره اختیاری است).", Severity.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await WalletService.RequestWithdrawalAsync(_selectedPayout.Id, _withdrawMethod, normalizedIban);
|
||||||
Snackbar.Add("درخواست برداشت ثبت شد.", Severity.Success);
|
Snackbar.Add("درخواست برداشت ثبت شد.", Severity.Success);
|
||||||
|
|
||||||
// بروزرسانی لیست
|
// بروزرسانی لیست
|
||||||
@@ -125,4 +136,26 @@ public partial class WithdrawalRequests : ComponentBase
|
|||||||
1 => "الماس",
|
1 => "الماس",
|
||||||
_ => "-"
|
_ => "-"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// فاصله و خط تیره را حذف میکند و شبا را به شکل IR + ۲۴ رقم برمیگرداند.
|
||||||
|
/// </summary>
|
||||||
|
private static string? NormalizeIranianIban(string? iban)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(iban))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var normalized = iban.Trim().ToUpperInvariant()
|
||||||
|
.Replace(" ", "", StringComparison.Ordinal)
|
||||||
|
.Replace("-", "", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
if (normalized.StartsWith("IR", StringComparison.Ordinal))
|
||||||
|
normalized = normalized[2..];
|
||||||
|
|
||||||
|
normalized = "IR" + normalized;
|
||||||
|
|
||||||
|
return System.Text.RegularExpressions.Regex.IsMatch(normalized, @"^IR\d{24}$")
|
||||||
|
? normalized
|
||||||
|
: null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -319,17 +319,17 @@ public partial class RegisterWizard
|
|||||||
$"<p><span class='field-label'>شماره ملی:</span> {_model.NationalCode?.PersianToEnglish()}</p>" +
|
$"<p><span class='field-label'>شماره ملی:</span> {_model.NationalCode?.PersianToEnglish()}</p>" +
|
||||||
$"<p><span class='field-label'>شماره تماس:</span> {_userAuthInfo.MobileNumber?.PersianToEnglish()}</p></div>" +
|
$"<p><span class='field-label'>شماره تماس:</span> {_userAuthInfo.MobileNumber?.PersianToEnglish()}</p></div>" +
|
||||||
"<p>کارابازار و کاربر که در این قرارداد جداگانه «طرف» و مجموعاً «طرفین» نامیده میشوند، با علم و آگاهی کامل، مفاد زیر را پذیرفته و امضا مینمایند.</p></div><div><p class='section-title'>ماده ۱ - موضوع قرارداد</p>" +
|
"<p>کارابازار و کاربر که در این قرارداد جداگانه «طرف» و مجموعاً «طرفین» نامیده میشوند، با علم و آگاهی کامل، مفاد زیر را پذیرفته و امضا مینمایند.</p></div><div><p class='section-title'>ماده ۱ - موضوع قرارداد</p>" +
|
||||||
"<p>موضوع این قرارداد، تعیین حدود و چارچوب تعهدات و سلب مسئولیت کارابازار در قبال استفاده کاربر ازخدمات، باشگاه مشتریان، مزایا، طرحهای همکاری، شبکه معرفی، خرید از فروشگاههای طرفقراردادو هرگونه امکانات و تسهیلات مرتبط است که حسب مورد توسط کارابازار یا از طریق پلتفرمها و شرکتهایهمکار در اختیار کاربر قرار میگیرد، بدون اینکه جزئیات مدل کسبوکار، شیوه تأمین منابع مالی،شبکه همکاری یا ساختار داخلی آن در این قرارداد ذکر یا افشا شود.</p>" +
|
"<p>موضوع این قرارداد، تعیین حدود و چارچوب تعهدات و سلب مسئولیت کارابازار در قبال استفاده کاربر ازخدمات، باشگاه مشتریان، مزایا، طرحهای همکاری، سازمان معرفی، خرید از فروشگاههای طرفقراردادو هرگونه امکانات و تسهیلات مرتبط است که حسب مورد توسط کارابازار یا از طریق پلتفرمها و شرکتهایهمکار در اختیار کاربر قرار میگیرد، بدون اینکه جزئیات مدل کسبوکار، شیوه تأمین منابع مالی،سازمان همکاری یا ساختار داخلی آن در این قرارداد ذکر یا افشا شود.</p>" +
|
||||||
"<p class='section-title'>ماده ۲ - اقرار و آگاهی کاربر</p><p>۲-۱. کاربر اقرار مینماید که کلیه اطلاعات کلی لازم در خصوص نحوه فعالیت، مزایا، ریسکهای احتمالی," +
|
"<p class='section-title'>ماده ۲ - اقرار و آگاهی کاربر</p><p>۲-۱. کاربر اقرار مینماید که کلیه اطلاعات کلی لازم در خصوص نحوه فعالیت، مزایا، ریسکهای احتمالی," +
|
||||||
"سازوکار کلی استفاده از خدمات، و چارچوب کلی همکاری را از مراجع رسمی معرفیشده توسط کارابازاردریافت کرده، سؤالات خود را مطرح نموده و پاسخ مناسب را دریافت نموده است.</p><p>۲-۲. کاربر با امضای این قرارداد اقرار مینماید که:</p><ul><li>با اراده آزاد و بدون هرگونه اجبار، اکراه، فریب یا سوءاستفاده از ناآگاهی وارد این همکاری شده است.</li><li>از احتمال وجود ریسکهای مالی، اعتباری، قانونی و شبکهای مرتبط با فعالیت خود آگاه بوده و آن را میپذیرد.</li>" +
|
"سازوکار کلی استفاده از خدمات، و چارچوب کلی همکاری را از مراجع رسمی معرفیشده توسط کارابازاردریافت کرده، سؤالات خود را مطرح نموده و پاسخ مناسب را دریافت نموده است.</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>" +
|
"<p class='section-title'>ماده ۳ - سلب مسئولیت کارابازار</p><p>۳-۱. کاربر صراحتاً میپذیرد که کارابازار صرفاً در چارچوب قوانین و مقررات حاکم و مطابق قراردادها و تفاهمنامههای خود با شرکای تجاری و پلتفرمهای همکار، خدمات و امکانات مشخصی را در اختیار کاربر قرار میدهد و هیچگونه تعهدی نسبت به:</p><ul><li>تحقق سود یا بازده مالی مشخص،</li><li>ثبات ارزش هرگونه اعتبار، امتیاز، یا منافع غیرنقدی که ممکن است در قالب طرحها به کاربر تخصیص یابد،</li>" +
|
||||||
"<li>عملکرد اشخاص ثالث (از جمله پلتفرمها، فروشگاهها، شرکتهای بیمه، ارائهدهندگان خدمات سلامت و هر شخص حقیقی یا حقوقی همکار)،</li><li>تصمیمات و اقدامات کاربر در جذب افراد جدید و ایجاد زیرمجموعه،</li></ul><p>نداشته و نخواهد داشت مگر در حدودی که بهصراحت در قرارداد یا قوانین آمره بر عهده کارابازار گذاشته شده است.</p><p>" +
|
"<li>عملکرد اشخاص ثالث (از جمله پلتفرمها، فروشگاهها، شرکتهای بیمه، ارائهدهندگان خدمات سلامت و هر شخص حقیقی یا حقوقی همکار)،</li><li>تصمیمات و اقدامات کاربر در جذب افراد جدید و ایجاد زیرمجموعه،</li></ul><p>نداشته و نخواهد داشت مگر در حدودی که بهصراحت در قرارداد یا قوانین آمره بر عهده کارابازار گذاشته شده است.</p><p>" +
|
||||||
"۳-۲. هرگونه خسارت، دعوی، تهدید، مطالبه، ادعا، شکایت یا پیگیری حقوقی که ناشی از تصمیمات شخصی کاربر،نحوه استفاده وی از تسهیلات و مزایا، جذب دیگران، خرید کالا و خدمات، یا تعاملات او با اشخاص ثالث باشد،مستقیماً متوجه خود کاربر است و کارابازار نسبت به آن هیچگونه مسئولیت مدنی، کیفری، مالی یا قراردادی ندارد," +
|
"۳-۲. هرگونه خسارت، دعوی، تهدید، مطالبه، ادعا، شکایت یا پیگیری حقوقی که ناشی از تصمیمات شخصی کاربر،نحوه استفاده وی از تسهیلات و مزایا، جذب دیگران، خرید کالا و خدمات، یا تعاملات او با اشخاص ثالث باشد،مستقیماً متوجه خود کاربر است و کارابازار نسبت به آن هیچگونه مسئولیت مدنی، کیفری، مالی یا قراردادی ندارد," +
|
||||||
"مگر در مواردی که طبق قوانین آمره، مسئولیت صریح و غیرقابلاسقاط بر عهده کارابازار قرار گرفته باشد.</p><p>۳-۳. کاربر میپذیرد که هیچگونه ادعایی مبنی بر فریب، اغفال، ارائه اطلاعات نادرست یا وعدههای خارج ازچهارچوب رسمی از سوی کارابازار یا نمایندگان رسمی آن، پس از امضای این قرارداد، از وی مسموع نخواهد بود،مگر آنکه طبق رأی قطعی مرجع قضایی صالح، خلاف آن ثابت شود.</p><p class='section-title'>ماده ۴ - مسئولیت کاربر در قبال تهدید، ادعا و شکایت</p>" +
|
"مگر در مواردی که طبق قوانین آمره، مسئولیت صریح و غیرقابلاسقاط بر عهده کارابازار قرار گرفته باشد.</p><p>۳-۳. کاربر میپذیرد که هیچگونه ادعایی مبنی بر فریب، اغفال، ارائه اطلاعات نادرست یا وعدههای خارج ازچهارچوب رسمی از سوی کارابازار یا نمایندگان رسمی آن، پس از امضای این قرارداد، از وی مسموع نخواهد بود،مگر آنکه طبق رأی قطعی مرجع قضایی صالح، خلاف آن ثابت شود.</p><p class='section-title'>ماده ۴ - مسئولیت کاربر در قبال تهدید، ادعا و شکایت</p>" +
|
||||||
"<p>۴-۱. کاربر متعهد است در صورت طرح هرگونه تهدید، ادعا، مطالبه، شکایت یا پیگیری حقوقی از سوی اشخاص ثالث(از جمله افراد جذبشده توسط کاربر، زیرمجموعهها، خریداران معرفیشده یا سایر اشخاص)، که ناشی از عملکردشخصی کاربر، معرفیهای وی، شیوه تبلیغ، وعدههای خارج از چارچوب رسمی کارابازار، یا نقض قوانین و مقرراتباشد، خود رأساً پاسخگو باشد و کارابازار را از هرگونه مسئولیت و هزینه احتمالی در این خصوص مصون بدارد.</p><p>۴-۲. کاربر میپذیرد که:</p><ul><li>هرگونه تهدید یا ادعای بیجا، غیرمنطقی، غیرمستند یا مغایر با مفاد این قرارداد، متوجه شخص کاربر است.</li>" +
|
"<p>۴-۱. کاربر متعهد است در صورت طرح هرگونه تهدید، ادعا، مطالبه، شکایت یا پیگیری حقوقی از سوی اشخاص ثالث(از جمله افراد جذبشده توسط کاربر، زیرمجموعهها، خریداران معرفیشده یا سایر اشخاص)، که ناشی از عملکردشخصی کاربر، معرفیهای وی، شیوه تبلیغ، وعدههای خارج از چارچوب رسمی کارابازار، یا نقض قوانین و مقرراتباشد، خود رأساً پاسخگو باشد و کارابازار را از هرگونه مسئولیت و هزینه احتمالی در این خصوص مصون بدارد.</p><p>۴-۲. کاربر میپذیرد که:</p><ul><li>هرگونه تهدید یا ادعای بیجا، غیرمنطقی، غیرمستند یا مغایر با مفاد این قرارداد، متوجه شخص کاربر است.</li>" +
|
||||||
"<li>در صورت وارد آمدن خسارت مادی یا معنوی به کارابازار ناشی از چنین تهدیدات یا ادعاهایی، کارابازار حق دارد نسبت به مطالبه خسارت از کاربر، طبق قوانین و از طریق مراجع صالح اقدام نماید.</li></ul>" +
|
"<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 class='section-title'>ماده ۶ - کد تأیید یکبارمصرف (OTP) و شناسه قرارداد</p><p>۶-۱. به منظور تأیید هویت کاربر و جایگزین کردن امضا در محیط غیرحضوری، برای این قرارداد یک شناسه یکتا(GUID) تولید شده و از طریق سامانه کارابازار، کد تأیید یکبارمصرف (OTP) مرتبط با آن برای کاربر ارسالمیگردد.</p>" +
|
||||||
"<p>۶-۲. کاربر با وارد کردن صحیح کد تأیید (OTP) در سامانه، اقرار مینماید که:</p><ul><li>مالک شماره تماس ثبتشده در این قرارداد است یا با اجازه قانونی مالک از آن استفاده میکند،</li><li>از محتوای قرارداد بهطور کامل آگاه است و آن را بدون ابهام و با رضایت کامل میپذیرد،</li><li>شناسه یکتا و OTP ارسالشده، معادل امضای الکترونیکی معتبر وی بر روی این قرارداد است.</li></ul>" +
|
"<p>۶-۲. کاربر با وارد کردن صحیح کد تأیید (OTP) در سامانه، اقرار مینماید که:</p><ul><li>مالک شماره تماس ثبتشده در این قرارداد است یا با اجازه قانونی مالک از آن استفاده میکند،</li><li>از محتوای قرارداد بهطور کامل آگاه است و آن را بدون ابهام و با رضایت کامل میپذیرد،</li><li>شناسه یکتا و OTP ارسالشده، معادل امضای الکترونیکی معتبر وی بر روی این قرارداد است.</li></ul>" +
|
||||||
"<p class='section-title'>ماده ۷ - مدت، اصلاح و حاکمیت قانون</p>" +
|
"<p class='section-title'>ماده ۷ - مدت، اصلاح و حاکمیت قانون</p>" +
|
||||||
|
|||||||
@@ -21,9 +21,11 @@
|
|||||||
else if (_addresses.Count == 0)
|
else if (_addresses.Count == 0)
|
||||||
{
|
{
|
||||||
<MudAlert Severity="Severity.Warning">
|
<MudAlert Severity="Severity.Warning">
|
||||||
هیچ آدرسی ثبت نشده است. لطفاً از بخش پروفایل آدرس خود را اضافه کنید.
|
هیچ آدرسی ثبت نشده است. میتوانید همینجا آدرس جدید اضافه کنید.
|
||||||
</MudAlert>
|
</MudAlert>
|
||||||
<MudButton Class="mt-2" Variant="Variant.Outlined" Href="/profile/addresses">افزودن آدرس</MudButton>
|
<MudButton Class="mt-2" Variant="Variant.Outlined" Color="Color.Primary"
|
||||||
|
StartIcon="@Icons.Material.Filled.Add"
|
||||||
|
OnClick="OpenAddAddressDialog">افزودن آدرس</MudButton>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -40,13 +42,25 @@
|
|||||||
<MudText Typo="Typo.subtitle2">@address.Title</MudText>
|
<MudText Typo="Typo.subtitle2">@address.Title</MudText>
|
||||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">@address.Address</MudText>
|
<MudText Typo="Typo.body2" Class="mud-text-secondary">@address.Address</MudText>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
@if (address.IsDefault)
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||||
{
|
@if (address.IsDefault)
|
||||||
<MudChip T="string" Color="Color.Success" Variant="Variant.Outlined">پیشفرض</MudChip>
|
{
|
||||||
}
|
<MudChip T="string" Color="Color.Success" Variant="Variant.Outlined" Size="Size.Small">پیشفرض</MudChip>
|
||||||
|
}
|
||||||
|
<span @onclick:stopPropagation="true">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||||
|
Size="Size.Small"
|
||||||
|
Color="Color.Primary"
|
||||||
|
aria-label="ویرایش آدرس"
|
||||||
|
OnClick="@(() => OpenEditAddressDialog(address))" />
|
||||||
|
</span>
|
||||||
|
</MudStack>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
}
|
}
|
||||||
|
<MudButton Class="mt-2" Variant="Variant.Text" Color="Color.Primary"
|
||||||
|
StartIcon="@Icons.Material.Filled.Add"
|
||||||
|
OnClick="OpenAddAddressDialog">افزودن آدرس جدید</MudButton>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
}
|
}
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
||||||
using CMSMicroservice.Protobuf.Protos.UserOrder;
|
using CMSMicroservice.Protobuf.Protos.UserOrder;
|
||||||
|
using FrontOffice.Main.Pages.Profile.Components;
|
||||||
using FrontOffice.Main.Shared;
|
using FrontOffice.Main.Shared;
|
||||||
using FrontOffice.Main.Utilities;
|
using FrontOffice.Main.Utilities;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
@@ -35,7 +36,11 @@ public partial class CheckoutSummary : ComponentBase
|
|||||||
await AuthDialogService.ShowAuthDialogAsync();
|
await AuthDialogService.ShowAuthDialogAsync();
|
||||||
}
|
}
|
||||||
await Cart.EnsureInitializedAsync();
|
await Cart.EnsureInitializedAsync();
|
||||||
await LoadAddresses();
|
var userInfo = await AuthService.GetUserAuthInfo();
|
||||||
|
if (userInfo.HasAddress)
|
||||||
|
await LoadAddresses();
|
||||||
|
else
|
||||||
|
_addresses = new();
|
||||||
await LoadWalletBalance();
|
await LoadWalletBalance();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,12 +73,14 @@ public partial class CheckoutSummary : ComponentBase
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
_addresses = new();
|
_addresses = new();
|
||||||
|
_selectedAddress = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Snackbar.Add($"خطا در بارگذاری آدرسها: {ex.Message}", Severity.Error);
|
Snackbar.Add($"خطا در بارگذاری آدرسها: {ex.Message}", Severity.Error);
|
||||||
_addresses = new();
|
_addresses = new();
|
||||||
|
_selectedAddress = null;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -82,6 +89,30 @@ 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()
|
private async Task PlaceOrder()
|
||||||
{
|
{
|
||||||
if (!CanPlaceOrder || _selectedAddress is null)
|
if (!CanPlaceOrder || _selectedAddress is null)
|
||||||
@@ -125,11 +156,42 @@ public partial class CheckoutSummary : ComponentBase
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
var shortfall = totalRequired - walletBalance;
|
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>
|
var parameters = new DialogParameters<InsufficientCreditDialog>
|
||||||
{
|
{
|
||||||
{ x => x.CurrentBalance, walletBalance },
|
{ x => x.CurrentBalance, walletBalance },
|
||||||
{ x => x.RequiredAmount, totalRequired },
|
{ x => x.RequiredAmount, totalRequired },
|
||||||
{ x => x.ShortfallAmount, shortfall }
|
{ x => x.ShortfallAmount, shortfall },
|
||||||
|
{ x => x.AllowChargeCredit, allowCharge },
|
||||||
|
{ x => x.HasPurchasedPackage, hasPurchasedPackage },
|
||||||
|
{ x => x.IsMagicWallet, isMagicWallet },
|
||||||
|
{ x => x.MagicCeilingFull, magicCeilingFull }
|
||||||
};
|
};
|
||||||
|
|
||||||
var dialog = await DialogService.ShowAsync<InsufficientCreditDialog>(
|
var dialog = await DialogService.ShowAsync<InsufficientCreditDialog>(
|
||||||
@@ -143,7 +205,7 @@ public partial class CheckoutSummary : ComponentBase
|
|||||||
});
|
});
|
||||||
|
|
||||||
var result = await dialog.Result;
|
var result = await dialog.Result;
|
||||||
if (result is { Canceled: false } && result.Data is long chargeShortfall)
|
if (allowCharge && result is { Canceled: false } && result.Data is long chargeShortfall)
|
||||||
{
|
{
|
||||||
var chargeAmount = CreditChargeNavigation.NormalizeChargeAmount(chargeShortfall);
|
var chargeAmount = CreditChargeNavigation.NormalizeChargeAmount(chargeShortfall);
|
||||||
Navigation.NavigateTo(CreditChargeNavigation.BuildChargeUrl(
|
Navigation.NavigateTo(CreditChargeNavigation.BuildChargeUrl(
|
||||||
|
|||||||
@@ -112,30 +112,6 @@ else
|
|||||||
</MudStack>
|
</MudStack>
|
||||||
}
|
}
|
||||||
</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>
|
</MudStack>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
|
|||||||
@@ -12,17 +12,12 @@ public partial class OrderDetail : ComponentBase
|
|||||||
[Parameter] public long id { get; set; }
|
[Parameter] public long id { get; set; }
|
||||||
|
|
||||||
private GetUserOrderResponse? _order;
|
private GetUserOrderResponse? _order;
|
||||||
private CalculateOrderPVResponse? _pvData;
|
|
||||||
private bool _loading;
|
private bool _loading;
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
_loading = true;
|
_loading = true;
|
||||||
_order = await OrderService.GetOrderAsync(id);
|
_order = await OrderService.GetOrderAsync(id);
|
||||||
if (_order != null)
|
|
||||||
{
|
|
||||||
_pvData = await OrderService.CalculateOrderPVAsync(id);
|
|
||||||
}
|
|
||||||
_loading = false;
|
_loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,4 +47,3 @@ public partial class OrderDetail : ComponentBase
|
|||||||
private static string GetProductImageUrl(string? imageUrl)
|
private static string GetProductImageUrl(string? imageUrl)
|
||||||
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/');
|
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,12 +46,19 @@ public partial class OrderTracking : ComponentBase
|
|||||||
private void BuildTrackingSteps()
|
private void BuildTrackingSteps()
|
||||||
{
|
{
|
||||||
if (_order is null) return;
|
if (_order is null) return;
|
||||||
|
|
||||||
// Build tracking steps based on PaymentStatus
|
|
||||||
var isPaid = _order.PaymentStatus == Messages.PaymentStatus.Success;
|
var isPaid = _order.PaymentStatus == Messages.PaymentStatus.Success;
|
||||||
|
// Domain/public_messages: None=0, Pending=1, InTransit=2, Delivered=3, Returned=4, Cancelled=5, ReadyForOfficePickup=6
|
||||||
_trackingSteps = new List<TrackingStep>
|
var delivery = (int)_order.DeliveryStatus;
|
||||||
{
|
var isCancelled = delivery == 5;
|
||||||
|
var isReturned = delivery == 4;
|
||||||
|
var isOfficePickup = delivery == 6;
|
||||||
|
var isInTransit = delivery == 2;
|
||||||
|
var isDelivered = delivery == 3;
|
||||||
|
var isPendingDelivery = delivery is 0 or 1;
|
||||||
|
|
||||||
|
_trackingSteps =
|
||||||
|
[
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Title = "ثبت سفارش",
|
Title = "ثبت سفارش",
|
||||||
@@ -61,45 +68,77 @@ public partial class OrderTracking : ComponentBase
|
|||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Title = "در انتظار پرداخت",
|
Title = "پرداخت",
|
||||||
Description = "منتظر تایید پرداخت هستیم",
|
Description = isPaid ? "پرداخت شما تایید شد" : "منتظر تایید پرداخت هستیم",
|
||||||
IsCompleted = isPaid,
|
IsCompleted = isPaid,
|
||||||
IsCurrent = !isPaid,
|
IsCurrent = !isPaid && !isCancelled,
|
||||||
Date = isPaid ? FormatDate(_order.PaymentDate) : ""
|
Date = isPaid ? FormatDate(_order.PaymentDate) : ""
|
||||||
},
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Title = "تایید پرداخت",
|
|
||||||
Description = "پرداخت شما تایید شد",
|
|
||||||
IsCompleted = isPaid,
|
|
||||||
IsCurrent = false,
|
|
||||||
Date = isPaid ? FormatDate(_order.PaymentDate) : ""
|
|
||||||
},
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Title = "در حال پردازش",
|
|
||||||
Description = "سفارش شما در حال آمادهسازی است",
|
|
||||||
IsCompleted = false,
|
|
||||||
IsCurrent = isPaid,
|
|
||||||
Date = ""
|
|
||||||
},
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Title = "ارسال شده",
|
|
||||||
Description = "سفارش شما به پست تحویل داده شد",
|
|
||||||
IsCompleted = false,
|
|
||||||
IsCurrent = false,
|
|
||||||
Date = ""
|
|
||||||
},
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Title = "تحویل داده شده",
|
|
||||||
Description = "سفارش به دست شما رسید",
|
|
||||||
IsCompleted = false,
|
|
||||||
IsCurrent = false,
|
|
||||||
Date = ""
|
|
||||||
}
|
}
|
||||||
};
|
];
|
||||||
|
|
||||||
|
if (isCancelled)
|
||||||
|
{
|
||||||
|
_trackingSteps.Add(new()
|
||||||
|
{
|
||||||
|
Title = "لغو شده",
|
||||||
|
Description = "ارسال این سفارش لغو شده است",
|
||||||
|
IsCompleted = true,
|
||||||
|
IsCurrent = true,
|
||||||
|
Date = ""
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isReturned)
|
||||||
|
{
|
||||||
|
_trackingSteps.Add(new()
|
||||||
|
{
|
||||||
|
Title = "مرجوع شده",
|
||||||
|
Description = "سفارش مرجوع شده است",
|
||||||
|
IsCompleted = true,
|
||||||
|
IsCurrent = true,
|
||||||
|
Date = ""
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOfficePickup)
|
||||||
|
{
|
||||||
|
_trackingSteps.Add(new()
|
||||||
|
{
|
||||||
|
Title = "آماده تحویل در دفتر",
|
||||||
|
Description = "سفارش برای تحویل حضوری در دفتر آماده است",
|
||||||
|
IsCompleted = true,
|
||||||
|
IsCurrent = true,
|
||||||
|
Date = ""
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_trackingSteps.Add(new()
|
||||||
|
{
|
||||||
|
Title = "در حال آمادهسازی",
|
||||||
|
Description = "سفارش شما در حال آمادهسازی است",
|
||||||
|
IsCompleted = isInTransit || isDelivered,
|
||||||
|
IsCurrent = isPaid && isPendingDelivery,
|
||||||
|
Date = ""
|
||||||
|
});
|
||||||
|
_trackingSteps.Add(new()
|
||||||
|
{
|
||||||
|
Title = "ارسال شده",
|
||||||
|
Description = "سفارش شما به پست تحویل داده شد",
|
||||||
|
IsCompleted = isDelivered,
|
||||||
|
IsCurrent = isInTransit,
|
||||||
|
Date = ""
|
||||||
|
});
|
||||||
|
_trackingSteps.Add(new()
|
||||||
|
{
|
||||||
|
Title = "تحویل داده شده",
|
||||||
|
Description = "سفارش به دست شما رسید",
|
||||||
|
IsCompleted = isDelivered,
|
||||||
|
IsCurrent = false,
|
||||||
|
Date = ""
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string FormatDate(Google.Protobuf.WellKnownTypes.Timestamp? timestamp)
|
private static string FormatDate(Google.Protobuf.WellKnownTypes.Timestamp? timestamp)
|
||||||
|
|||||||
@@ -1,80 +1,102 @@
|
|||||||
@attribute [Route(RouteConstants.Store.Orders)]
|
@attribute [Route(RouteConstants.Store.Orders)]
|
||||||
@* Injection is handled in code-behind *@
|
|
||||||
|
|
||||||
<PageTitle>سفارشات من</PageTitle>
|
<PageTitle>سفارشات من</PageTitle>
|
||||||
|
|
||||||
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
|
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
|
||||||
<PageHeader Title="سفارشات من" BackHref="@RouteConstants.Store.Products" />
|
<MudStack Spacing="3">
|
||||||
@if (_loading)
|
<PageHeader Title="سفارشات من" BackHref="@RouteConstants.Store.Products" />
|
||||||
{
|
|
||||||
<LoadingState />
|
|
||||||
}
|
|
||||||
else if (_orders.Count == 0)
|
|
||||||
{
|
|
||||||
<MudAlert Severity="Severity.Info">هنوز سفارشی ثبت نکردهاید.</MudAlert>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
|
|
||||||
<MudTable Items="_orders" Dense="true">
|
|
||||||
<HeaderContent>
|
|
||||||
<MudTh>شناسه</MudTh>
|
|
||||||
<MudTh>تاریخ</MudTh>
|
|
||||||
<MudTh>وضعیت</MudTh>
|
|
||||||
<MudTh>مبلغ</MudTh>
|
|
||||||
<MudTh></MudTh>
|
|
||||||
</HeaderContent>
|
|
||||||
<RowTemplate>
|
|
||||||
<MudTd>@context.Id</MudTd>
|
|
||||||
<MudTd>
|
|
||||||
@if (context.PaymentDate != null)
|
|
||||||
{
|
|
||||||
@context.PaymentDate.ToDateTime().MiladiToJalaliWithTime()
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<text>در انتظار پرداخت</text>
|
|
||||||
}
|
|
||||||
</MudTd>
|
|
||||||
<MudTd>@GetStatusText(context.PaymentStatus)</MudTd>
|
|
||||||
<MudTd>@FormatPrice(context.FactorDetails.Sum(s=>(s.UnitPrice ?? 0) * (s.Count ?? 0)))</MudTd>
|
|
||||||
<MudTd>
|
|
||||||
<MudButton Variant="Variant.Text" Href="@($"{RouteConstants.Store.OrderDetail}{context.Id}")" StartIcon="@Icons.Material.Filled.Receipt">جزئیات</MudButton>
|
|
||||||
</MudTd>
|
|
||||||
</RowTemplate>
|
|
||||||
</MudTable>
|
|
||||||
</MudHidden>
|
|
||||||
|
|
||||||
<MudHidden Breakpoint="Breakpoint.MdAndUp">
|
@if (_loading)
|
||||||
<MudStack Spacing="2">
|
{
|
||||||
@foreach (var o in _orders)
|
<LoadingState />
|
||||||
{
|
}
|
||||||
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
|
else if (_orders.Count == 0)
|
||||||
<MudStack Spacing="1">
|
{
|
||||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
<MudAlert Severity="Severity.Info">هنوز سفارشی ثبت نشده است.</MudAlert>
|
||||||
<MudText>سفارش #@o.Id</MudText>
|
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Store"
|
||||||
<MudText Class="mud-text-secondary">
|
Href="@RouteConstants.Store.Products">
|
||||||
@if (o.PaymentDate != null)
|
مشاهده فروشگاه
|
||||||
{
|
</MudButton>
|
||||||
@o.PaymentDate.ToDateTime().MiladiToJalaliWithTime()
|
}
|
||||||
}
|
else
|
||||||
else
|
{
|
||||||
{
|
<!-- Desktop Table -->
|
||||||
<text>در انتظار پرداخت</text>
|
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
|
||||||
}
|
<MudPaper Elevation="1" Class="pa-4 rounded-lg">
|
||||||
</MudText>
|
<MudTable Items="_orders">
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>شماره سفارش</MudTh>
|
||||||
|
<MudTh>تعداد اقلام</MudTh>
|
||||||
|
<MudTh>مبلغ کل</MudTh>
|
||||||
|
<MudTh>وضعیت پرداخت</MudTh>
|
||||||
|
<MudTh>وضعیت ارسال</MudTh>
|
||||||
|
<MudTh>تاریخ</MudTh>
|
||||||
|
<MudTh></MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd>@context.Id</MudTd>
|
||||||
|
<MudTd>@GetItemsCount(context)</MudTd>
|
||||||
|
<MudTd>@FormatPrice(GetTotalAmount(context))</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudChip T="string" Size="Size.Small"
|
||||||
|
Color="@GetPaymentStatusColor(context.PaymentStatus)"
|
||||||
|
Variant="Variant.Filled">
|
||||||
|
@GetPaymentStatusText(context.PaymentStatus)
|
||||||
|
</MudChip>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudChip T="string" Size="Size.Small"
|
||||||
|
Color="@GetDeliveryStatusColor((int)context.DeliveryStatus)"
|
||||||
|
Variant="Variant.Outlined">
|
||||||
|
@GetDeliveryStatusText((int)context.DeliveryStatus)
|
||||||
|
</MudChip>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>@FormatDate(context)</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
|
||||||
|
OnClick="() => ViewOrder(context.Id)">
|
||||||
|
جزئیات
|
||||||
|
</MudButton>
|
||||||
|
</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
</MudTable>
|
||||||
|
</MudPaper>
|
||||||
|
</MudHidden>
|
||||||
|
|
||||||
|
<!-- Mobile Cards -->
|
||||||
|
<MudHidden Breakpoint="Breakpoint.MdAndUp">
|
||||||
|
<MudStack Spacing="2">
|
||||||
|
@foreach (var order in _orders)
|
||||||
|
{
|
||||||
|
<MudPaper Class="pa-3 rounded-lg cursor-pointer" Outlined="true"
|
||||||
|
@onclick="() => ViewOrder(order.Id)">
|
||||||
|
<MudStack Spacing="1">
|
||||||
|
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||||
|
<MudText Typo="Typo.subtitle2">سفارش #@order.Id</MudText>
|
||||||
|
<MudChip T="string" Size="Size.Small"
|
||||||
|
Color="@GetPaymentStatusColor(order.PaymentStatus)"
|
||||||
|
Variant="Variant.Filled">
|
||||||
|
@GetPaymentStatusText(order.PaymentStatus)
|
||||||
|
</MudChip>
|
||||||
|
</MudStack>
|
||||||
|
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||||
|
<MudText Typo="Typo.caption" Class="mud-text-secondary">@FormatDate(order)</MudText>
|
||||||
|
<MudChip T="string" Size="Size.Small"
|
||||||
|
Color="@GetDeliveryStatusColor((int)order.DeliveryStatus)"
|
||||||
|
Variant="Variant.Outlined">
|
||||||
|
@GetDeliveryStatusText((int)order.DeliveryStatus)
|
||||||
|
</MudChip>
|
||||||
|
</MudStack>
|
||||||
|
<MudDivider />
|
||||||
|
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||||
|
<MudText Typo="Typo.body2">@GetItemsCount(order) قلم</MudText>
|
||||||
|
<MudText Typo="Typo.body2" Class="fw-semibold">@FormatPrice(GetTotalAmount(order)) تومان</MudText>
|
||||||
|
</MudStack>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
</MudPaper>
|
||||||
<MudText>وضعیت: @GetStatusText(o.PaymentStatus)</MudText>
|
}
|
||||||
<MudText Color="Color.Primary">@FormatPrice(o.FactorDetails.Sum(s=>(s.UnitPrice ?? 0) * (s.Count ?? 0)))</MudText>
|
</MudStack>
|
||||||
</MudStack>
|
</MudHidden>
|
||||||
<MudStack Row="true" Justify="Justify.FlexEnd">
|
}
|
||||||
<MudButton Size="Size.Small" Variant="Variant.Outlined" Href="@($"{RouteConstants.Store.OrderDetail}{o.Id}")" StartIcon="@Icons.Material.Filled.Receipt">جزئیات</MudButton>
|
</MudStack>
|
||||||
</MudStack>
|
|
||||||
</MudStack>
|
|
||||||
</MudPaper>
|
|
||||||
}
|
|
||||||
</MudStack>
|
|
||||||
</MudHidden>
|
|
||||||
}
|
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System;
|
|
||||||
using CMSMicroservice.Protobuf.Protos.UserOrder;
|
using CMSMicroservice.Protobuf.Protos.UserOrder;
|
||||||
|
using DateTimeConverterCL;
|
||||||
using FrontOffice.Main.Utilities;
|
using FrontOffice.Main.Utilities;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Messages = CMSMicroservice.Protobuf.Protos;
|
using Messages = CMSMicroservice.Protobuf.Protos;
|
||||||
@@ -10,7 +10,6 @@ namespace FrontOffice.Main.Pages.Store;
|
|||||||
public partial class Orders : ComponentBase
|
public partial class Orders : ComponentBase
|
||||||
{
|
{
|
||||||
[Inject] private OrderService OrderService { get; set; } = default!;
|
[Inject] private OrderService OrderService { get; set; } = default!;
|
||||||
[Inject] private VATService VAT { get; set; } = default!;
|
|
||||||
|
|
||||||
private List<GetUserOrderResponse> _orders = new();
|
private List<GetUserOrderResponse> _orders = new();
|
||||||
private bool _loading;
|
private bool _loading;
|
||||||
@@ -33,28 +32,81 @@ public partial class Orders : ComponentBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
private void ViewOrder(long orderId)
|
||||||
{
|
{
|
||||||
if (firstRender)
|
Navigation.NavigateTo($"{RouteConstants.Store.OrderDetail}{orderId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatPrice(long price) => $"{price:N0}";
|
||||||
|
|
||||||
|
private static string FormatDate(GetUserOrderResponse order)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
// بارگذاری نرخ VAT
|
var timestamp = order.PaymentDate;
|
||||||
await VAT.LoadAsync();
|
if (timestamp is null)
|
||||||
|
return "در انتظار پرداخت";
|
||||||
|
|
||||||
|
return timestamp.ToDateTime().ToLocalTime().MiladiToJalaliWithTime();
|
||||||
}
|
}
|
||||||
await base.OnAfterRenderAsync(firstRender);
|
catch
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
|
|
||||||
|
|
||||||
|
|
||||||
private string GetStatusText(Messages.PaymentStatus contextPaymentStatus)
|
|
||||||
{
|
|
||||||
return contextPaymentStatus switch
|
|
||||||
{
|
{
|
||||||
Messages.PaymentStatus.Pending => "در انتظار پرداخت",
|
return "—";
|
||||||
Messages.PaymentStatus.Success => "پرداخت شده",
|
}
|
||||||
Messages.PaymentStatus.Reject => "پرداخت ناموفق",
|
|
||||||
_ => "نامشخص",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
private static int GetItemsCount(GetUserOrderResponse order) =>
|
||||||
|
order.FactorDetails.Sum(f => f.Count ?? 0);
|
||||||
|
|
||||||
|
private static long GetTotalAmount(GetUserOrderResponse order)
|
||||||
|
{
|
||||||
|
if (order.VatInfo is not null && order.VatInfo.TotalAmount > 0)
|
||||||
|
return order.VatInfo.TotalAmount;
|
||||||
|
|
||||||
|
if (order.Amount > 0)
|
||||||
|
return order.Amount;
|
||||||
|
|
||||||
|
return order.FactorDetails.Sum(f => (f.UnitPrice ?? 0) * (f.Count ?? 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetPaymentStatusText(Messages.PaymentStatus status) => status switch
|
||||||
|
{
|
||||||
|
Messages.PaymentStatus.Pending => "در انتظار پرداخت",
|
||||||
|
Messages.PaymentStatus.Success => "پرداخت شده",
|
||||||
|
Messages.PaymentStatus.Reject => "پرداخت ناموفق",
|
||||||
|
_ => "نامشخص"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Color GetPaymentStatusColor(Messages.PaymentStatus status) => status switch
|
||||||
|
{
|
||||||
|
Messages.PaymentStatus.Pending => Color.Warning,
|
||||||
|
Messages.PaymentStatus.Success => Color.Success,
|
||||||
|
Messages.PaymentStatus.Reject => Color.Error,
|
||||||
|
_ => Color.Default
|
||||||
|
};
|
||||||
|
|
||||||
|
// Domain/public_messages: None=0, Pending=1, InTransit=2, Delivered=3, Returned=4, Cancelled=5, ReadyForOfficePickup=6
|
||||||
|
private static string GetDeliveryStatusText(int status) => status switch
|
||||||
|
{
|
||||||
|
0 => "بدون ارسال",
|
||||||
|
1 => "در انتظار ارسال",
|
||||||
|
2 => "ارسال شده",
|
||||||
|
3 => "تحویل داده شده",
|
||||||
|
4 => "مرجوع شده",
|
||||||
|
5 => "لغو شده",
|
||||||
|
6 => "آماده تحویل در دفتر",
|
||||||
|
_ => "نامشخص"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Color GetDeliveryStatusColor(int status) => status switch
|
||||||
|
{
|
||||||
|
0 => Color.Default,
|
||||||
|
1 => Color.Warning,
|
||||||
|
2 => Color.Primary,
|
||||||
|
3 => Color.Success,
|
||||||
|
4 => Color.Error,
|
||||||
|
5 => Color.Dark,
|
||||||
|
6 => Color.Primary,
|
||||||
|
_ => Color.Default
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -118,7 +118,8 @@
|
|||||||
@foreach (var p in _products)
|
@foreach (var p in _products)
|
||||||
{
|
{
|
||||||
<MudItem xs="6" sm="6" md="3"
|
<MudItem xs="6" sm="6" md="3"
|
||||||
onclick="@(() => Navigation.NavigateTo($"{RouteConstants.Store.ProductDetail}{p.Id}"))">
|
onclick="@(() => OpenProduct(p.Id))">
|
||||||
|
<div id="@($"shop-product-{p.Id}")" class="h-100">
|
||||||
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden"
|
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden"
|
||||||
Style="cursor:pointer;">
|
Style="cursor:pointer;">
|
||||||
|
|
||||||
@@ -164,6 +165,7 @@
|
|||||||
</MudButton>
|
</MudButton>
|
||||||
</MudCardActions>
|
</MudCardActions>
|
||||||
</MudCard>
|
</MudCard>
|
||||||
|
</div>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
}
|
}
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
|
|||||||
@@ -2,19 +2,11 @@ using System.Linq;
|
|||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.AspNetCore.Components.Routing;
|
using Microsoft.AspNetCore.Components.Routing;
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
using Microsoft.AspNetCore.WebUtilities;
|
using Microsoft.JSInterop;
|
||||||
using FrontOffice.Main.Utilities;
|
using FrontOffice.Main.Utilities;
|
||||||
|
|
||||||
namespace FrontOffice.Main.Pages.Store;
|
namespace FrontOffice.Main.Pages.Store;
|
||||||
|
|
||||||
public enum ProductSortOption
|
|
||||||
{
|
|
||||||
PriceDesc, // گرانترین (پیشفرض)
|
|
||||||
PriceAsc, // ارزانترین
|
|
||||||
Newest, // جدیدترین
|
|
||||||
Title // الفبایی
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class Products : ComponentBase, IDisposable
|
public partial class Products : ComponentBase, IDisposable
|
||||||
{
|
{
|
||||||
[Inject] private ProductService ProductService { get; set; } = default!;
|
[Inject] private ProductService ProductService { get; set; } = default!;
|
||||||
@@ -23,6 +15,7 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
[Inject] private VATService VAT { get; set; } = default!;
|
[Inject] private VATService VAT { get; set; } = default!;
|
||||||
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
||||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||||
|
[Inject] private IJSRuntime Js { get; set; } = default!;
|
||||||
|
|
||||||
private bool _isAuthenticated;
|
private bool _isAuthenticated;
|
||||||
private string _query = string.Empty;
|
private string _query = string.Empty;
|
||||||
@@ -35,8 +28,9 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
private List<Product> _products = new();
|
private List<Product> _products = new();
|
||||||
private long? _activeCategoryId;
|
private long? _activeCategoryId;
|
||||||
private string? _activeCategoryTitle;
|
private string? _activeCategoryTitle;
|
||||||
|
private ProductSortOption _sortOption = ProductSortOption.PriceDesc;
|
||||||
private ProductSortOption _sortOption = ProductSortOption.PriceDesc; // پیشفرض: گرانترین
|
private bool _ignoreNextLocationChange;
|
||||||
|
private bool _pendingScrollRestore;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
@@ -44,80 +38,132 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
await Cart.EnsureInitializedAsync();
|
await Cart.EnsureInitializedAsync();
|
||||||
Cart.OnChange += StateHasChanged;
|
Cart.OnChange += StateHasChanged;
|
||||||
Navigation.LocationChanged += HandleLocationChanged;
|
Navigation.LocationChanged += HandleLocationChanged;
|
||||||
await LoadInitial();
|
ApplyStateFromUri();
|
||||||
|
await LoadPages(_currentPage);
|
||||||
|
_pendingScrollRestore = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (firstRender)
|
||||||
{
|
|
||||||
// بارگذاری نرخ VAT
|
|
||||||
await VAT.LoadAsync();
|
await VAT.LoadAsync();
|
||||||
|
|
||||||
|
if (_pendingScrollRestore && !_loading && _products.Count > 0)
|
||||||
|
{
|
||||||
|
_pendingScrollRestore = false;
|
||||||
|
var payload = await ShopListScrollRestore.TakeAsync(Js, ShopListScrollRestore.StoreKey);
|
||||||
|
if (payload is not null)
|
||||||
|
await ShopListScrollRestore.RestoreAsync(Js, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
await base.OnAfterRenderAsync(firstRender);
|
await base.OnAfterRenderAsync(firstRender);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadInitial()
|
private void ApplyStateFromUri()
|
||||||
|
{
|
||||||
|
var state = ShopListQueryState.Parse(Navigation.ToAbsoluteUri(Navigation.Uri));
|
||||||
|
_query = state.Query;
|
||||||
|
_sortOption = ShopListQueryState.ParseSortOption(state.Sort);
|
||||||
|
_activeCategoryId = state.CategoryId;
|
||||||
|
_currentPage = state.Pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ShopListQueryState CaptureState() => new()
|
||||||
|
{
|
||||||
|
Query = _query,
|
||||||
|
Sort = ShopListQueryState.ToSortKey(_sortOption),
|
||||||
|
CategoryId = _activeCategoryId,
|
||||||
|
Pages = Math.Max(1, _currentPage)
|
||||||
|
};
|
||||||
|
|
||||||
|
private void SyncUrl()
|
||||||
|
{
|
||||||
|
var target = CaptureState().ToRelativeUrl(RouteConstants.Store.Products);
|
||||||
|
var currentPathAndQuery = Navigation.ToAbsoluteUri(Navigation.Uri).PathAndQuery;
|
||||||
|
if (string.Equals(currentPathAndQuery, target, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(currentPathAndQuery, target + "/", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_ignoreNextLocationChange = true;
|
||||||
|
Navigation.NavigateTo(target, replace: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadPages(int pagesToLoad)
|
||||||
{
|
{
|
||||||
_loading = true;
|
_loading = true;
|
||||||
_currentPage = 1;
|
|
||||||
_products.Clear();
|
_products.Clear();
|
||||||
UpdateCategoryFilterFromUri();
|
_hasMore = true;
|
||||||
var sortBy = GetSortByValue();
|
pagesToLoad = Math.Max(1, pagesToLoad);
|
||||||
var result = await ProductService.GetProductsPagedAsync(_query, _activeCategoryId, sortBy, _currentPage, PageSize);
|
var sortBy = ShopListQueryState.ToApiSortBy(_sortOption);
|
||||||
_products = result.Products;
|
|
||||||
_hasMore = result.HasNext;
|
for (var page = 1; page <= pagesToLoad; page++)
|
||||||
_totalCount = result.TotalCount;
|
{
|
||||||
|
var result = await ProductService.GetProductsPagedAsync(
|
||||||
|
_query, _activeCategoryId, sortBy, page, PageSize);
|
||||||
|
if (page == 1)
|
||||||
|
{
|
||||||
|
_products = result.Products;
|
||||||
|
_totalCount = result.TotalCount;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_products.AddRange(result.Products);
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentPage = page;
|
||||||
|
_hasMore = result.HasNext;
|
||||||
|
if (!_hasMore)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
_activeCategoryTitle = _activeCategoryId is { } categoryId
|
_activeCategoryTitle = _activeCategoryId is { } categoryId
|
||||||
? (await CategoryService.GetByIdAsync(categoryId))?.Title
|
? (await CategoryService.GetByIdAsync(categoryId))?.Title
|
||||||
: null;
|
: null;
|
||||||
_loading = false;
|
_loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ReloadFromFilters()
|
||||||
|
{
|
||||||
|
_currentPage = 1;
|
||||||
|
await LoadPages(1);
|
||||||
|
SyncUrl();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task LoadMore()
|
private async Task LoadMore()
|
||||||
{
|
{
|
||||||
if (_loadingMore || !_hasMore) return;
|
if (_loadingMore || !_hasMore) return;
|
||||||
|
|
||||||
_loadingMore = true;
|
_loadingMore = true;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
|
|
||||||
_currentPage++;
|
_currentPage++;
|
||||||
var sortBy = GetSortByValue();
|
var sortBy = ShopListQueryState.ToApiSortBy(_sortOption);
|
||||||
var result = await ProductService.GetProductsPagedAsync(_query, _activeCategoryId, sortBy, _currentPage, PageSize);
|
var result = await ProductService.GetProductsPagedAsync(
|
||||||
|
_query, _activeCategoryId, sortBy, _currentPage, PageSize);
|
||||||
_products.AddRange(result.Products);
|
_products.AddRange(result.Products);
|
||||||
_hasMore = result.HasNext;
|
_hasMore = result.HasNext;
|
||||||
|
SyncUrl();
|
||||||
|
|
||||||
_loadingMore = false;
|
_loadingMore = false;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetSortByValue()
|
private async Task OnSortChanged() => await ReloadFromFilters();
|
||||||
{
|
|
||||||
return _sortOption switch
|
|
||||||
{
|
|
||||||
ProductSortOption.PriceDesc => "price desc",
|
|
||||||
ProductSortOption.PriceAsc => "price asc",
|
|
||||||
ProductSortOption.Newest => "id desc",
|
|
||||||
ProductSortOption.Title => "title asc",
|
|
||||||
_ => "price desc"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnSortChanged()
|
private async Task OnQueryChanged(KeyboardEventArgs _) => await ReloadFromFilters();
|
||||||
{
|
|
||||||
await LoadInitial();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnQueryChanged(KeyboardEventArgs _)
|
|
||||||
{
|
|
||||||
await LoadInitial();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddToCart(Product p)
|
private async Task AddToCart(Product p)
|
||||||
{
|
{
|
||||||
await GuestGate.RunAsync(() => Cart.Add(p, 1));
|
await GuestGate.RunAsync(() => Cart.Add(p, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task OpenProduct(long productId)
|
||||||
|
{
|
||||||
|
await ShopListScrollRestore.SaveAsync(Js, ShopListScrollRestore.StoreKey, productId);
|
||||||
|
Navigation.NavigateTo($"{RouteConstants.Store.ProductDetail}{productId}");
|
||||||
|
}
|
||||||
|
|
||||||
private string FormatPrice(long price) => $"{VAT.AddVAT(price):N0} تومان";
|
private string FormatPrice(long price) => $"{VAT.AddVAT(price):N0} تومان";
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@@ -128,24 +174,33 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
|
|
||||||
private void HandleLocationChanged(object? sender, LocationChangedEventArgs args)
|
private void HandleLocationChanged(object? sender, LocationChangedEventArgs args)
|
||||||
{
|
{
|
||||||
_ = InvokeAsync(LoadInitial);
|
if (_ignoreNextLocationChange)
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateCategoryFilterFromUri()
|
|
||||||
{
|
|
||||||
var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
|
|
||||||
if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("category", out var values) &&
|
|
||||||
long.TryParse(values.FirstOrDefault(), out var categoryId))
|
|
||||||
{
|
{
|
||||||
_activeCategoryId = categoryId;
|
_ignoreNextLocationChange = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_activeCategoryId = null;
|
var uri = Navigation.ToAbsoluteUri(args.Location);
|
||||||
|
if (!uri.AbsolutePath.Equals(RouteConstants.Store.Products, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var incoming = ShopListQueryState.Parse(uri);
|
||||||
|
if (incoming.Matches(CaptureState()))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_ = InvokeAsync(async () =>
|
||||||
|
{
|
||||||
|
ApplyStateFromUri();
|
||||||
|
await LoadPages(_currentPage);
|
||||||
|
_pendingScrollRestore = true;
|
||||||
|
StateHasChanged();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ClearCategoryFilter()
|
private async Task ClearCategoryFilter()
|
||||||
{
|
{
|
||||||
Navigation.NavigateTo(RouteConstants.Store.Products);
|
_activeCategoryId = null;
|
||||||
|
_activeCategoryTitle = null;
|
||||||
|
await ReloadFromFilters();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,10 +60,14 @@ webApp.MapBlazorHub();
|
|||||||
|
|
||||||
webApp.MapGet("/sitemap.xml", async (SitemapGenerator sitemapGenerator, CancellationToken cancellationToken) =>
|
webApp.MapGet("/sitemap.xml", async (SitemapGenerator sitemapGenerator, CancellationToken cancellationToken) =>
|
||||||
{
|
{
|
||||||
var xml = await sitemapGenerator.GenerateAsync(cancellationToken);
|
var bytes = await sitemapGenerator.GenerateBytesAsync(cancellationToken);
|
||||||
return Results.Content(xml, "application/xml; charset=utf-8");
|
return Results.File(bytes, "application/xml; charset=utf-8");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Legacy URLs from old site / common e-commerce paths — redirect to homepage
|
||||||
|
webApp.MapGet("/wishlist", () => Results.Redirect("/", permanent: true));
|
||||||
|
webApp.MapGet("/wishlist/", () => Results.Redirect("/", permanent: true));
|
||||||
|
|
||||||
webApp.MapFallbackToPage("/_Host");
|
webApp.MapFallbackToPage("/_Host");
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,9 @@
|
|||||||
OnChangePhone="ChangePhoneAsync"
|
OnChangePhone="ChangePhoneAsync"
|
||||||
OnResendOtp="ResendOtpAsync"
|
OnResendOtp="ResendOtpAsync"
|
||||||
ReferralCode="@_referralCode"
|
ReferralCode="@_referralCode"
|
||||||
ReferralCodeChanged="OnReferralCodeChanged" />
|
ReferralCodeChanged="OnReferralCodeChanged"
|
||||||
|
InviterMobile="@_inviterMobile"
|
||||||
|
InviterMobileChanged="OnInviterMobileChanged" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -55,7 +57,9 @@ else
|
|||||||
OnChangePhone="ChangePhoneAsync"
|
OnChangePhone="ChangePhoneAsync"
|
||||||
OnResendOtp="ResendOtpAsync"
|
OnResendOtp="ResendOtpAsync"
|
||||||
ReferralCode="@_referralCode"
|
ReferralCode="@_referralCode"
|
||||||
ReferralCodeChanged="OnReferralCodeChanged" />
|
ReferralCodeChanged="OnReferralCodeChanged"
|
||||||
|
InviterMobile="@_inviterMobile"
|
||||||
|
InviterMobileChanged="OnInviterMobileChanged" />
|
||||||
|
|
||||||
<MudStack Class="mt-4" Spacing="2">
|
<MudStack Class="mt-4" Spacing="2">
|
||||||
@if (_currentStep == AuthStep.Phone)
|
@if (_currentStep == AuthStep.Phone)
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ public partial class AuthDialog : IDisposable
|
|||||||
|
|
||||||
// Referral code field
|
// Referral code field
|
||||||
private string? _referralCode;
|
private string? _referralCode;
|
||||||
|
// Inviter mobile (دعوتکننده)
|
||||||
|
private string? _inviterMobile;
|
||||||
|
|
||||||
[Inject] private ILocalStorageService LocalStorage { get; set; } = default!;
|
[Inject] private ILocalStorageService LocalStorage { get; set; } = default!;
|
||||||
[Inject] private UserContract.UserContractClient UserClient { get; set; } = default!;
|
[Inject] private UserContract.UserContractClient UserClient { get; set; } = default!;
|
||||||
@@ -71,6 +73,7 @@ public partial class AuthDialog : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
_referralCode = await LocalStorage.GetItemAsync<string>("referral:code");
|
_referralCode = await LocalStorage.GetItemAsync<string>("referral:code");
|
||||||
|
_inviterMobile = await LocalStorage.GetItemAsync<string>("inviter:mobile");
|
||||||
// await LocalStorage.RemoveItemAsync(TokenStorageKey);
|
// await LocalStorage.RemoveItemAsync(TokenStorageKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +86,15 @@ public partial class AuthDialog : IDisposable
|
|||||||
await LocalStorage.RemoveItemAsync("referral:code");
|
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()
|
private void GenerateCaptcha()
|
||||||
{
|
{
|
||||||
_captchaCode = Guid.NewGuid().ToString("N")[..6].ToUpperInvariant();
|
_captchaCode = Guid.NewGuid().ToString("N")[..6].ToUpperInvariant();
|
||||||
@@ -204,6 +216,15 @@ public partial class AuthDialog : IDisposable
|
|||||||
_verifyRequest.ParentReferralCode = effectiveReferralCode;
|
_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);
|
var validationResult = true; // _verifyRequestValidator.Validate(_verifyRequest);
|
||||||
if (!validationResult)
|
if (!validationResult)
|
||||||
{
|
{
|
||||||
@@ -236,6 +257,7 @@ public partial class AuthDialog : IDisposable
|
|||||||
await LocalStorage.RemoveItemAsync(PhoneStorageKey);
|
await LocalStorage.RemoveItemAsync(PhoneStorageKey);
|
||||||
await LocalStorage.RemoveItemAsync(RedirectStorageKey);
|
await LocalStorage.RemoveItemAsync(RedirectStorageKey);
|
||||||
await LocalStorage.RemoveItemAsync("referral:code");
|
await LocalStorage.RemoveItemAsync("referral:code");
|
||||||
|
await LocalStorage.RemoveItemAsync("inviter:mobile");
|
||||||
|
|
||||||
_attemptsLeft = MaxVerificationAttempts;
|
_attemptsLeft = MaxVerificationAttempts;
|
||||||
_verifyRequest.Code = string.Empty;
|
_verifyRequest.Code = string.Empty;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@using FrontOffice.Main.Utilities
|
||||||
|
|
||||||
<MudDialog>
|
<MudDialog>
|
||||||
<TitleContent>
|
<TitleContent>
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||||
@@ -7,9 +9,34 @@
|
|||||||
</TitleContent>
|
</TitleContent>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<MudStack Spacing="2">
|
<MudStack Spacing="2">
|
||||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">
|
@if (AllowChargeCredit)
|
||||||
برای تکمیل این سفارش، موجودی اصلی شما کافی نیست. میتوانید کیف پول را شارژ کنید و سپس سفارش را ثبت کنید.
|
{
|
||||||
</MudText>
|
@if (IsMagicWallet && !MagicCeilingFull)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body2" Class="mud-text-secondary">
|
||||||
|
برای تکمیل این سفارش موجودی شما کافی نیست. کیف پول شما در حالت جادویی است؛ برای دریافت ضریب، از مسیر شارژ جادویی اقدام کنید.
|
||||||
|
</MudText>
|
||||||
|
}
|
||||||
|
else if (IsMagicWallet && MagicCeilingFull)
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Warning" Dense="true" Variant="Variant.Outlined">
|
||||||
|
سقف شارژ جادویی شما در این دور پر است. میتوانید بهصورت استثنایی کیف اصلی را بدون ضریب (۱:۱) شارژ کنید.
|
||||||
|
</MudAlert>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body2" Class="mud-text-secondary">
|
||||||
|
برای تکمیل این سفارش، موجودی اصلی شما کافی نیست. میتوانید کیف پول را شارژ کنید و سپس سفارش را ثبت کنید.
|
||||||
|
</MudText>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Warning" Dense="true" Variant="Variant.Outlined">
|
||||||
|
برای شارژ کیف پول اصلی ابتدا باید پکیج را خریداری کرده و قرارداد باشگاه مشتریان را امضا کنید.
|
||||||
|
</MudAlert>
|
||||||
|
}
|
||||||
|
|
||||||
<MudPaper Outlined="true" Class="pa-3 rounded-lg">
|
<MudPaper Outlined="true" Class="pa-3 rounded-lg">
|
||||||
<MudStack Spacing="1">
|
<MudStack Spacing="1">
|
||||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||||
@@ -31,12 +58,33 @@
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<MudButton OnClick="Cancel">بستن</MudButton>
|
<MudButton OnClick="Cancel">بستن</MudButton>
|
||||||
<MudButton Variant="Variant.Filled"
|
@if (AllowChargeCredit)
|
||||||
Color="Color.Primary"
|
{
|
||||||
StartIcon="@Icons.Material.Filled.Payment"
|
<MudButton Variant="Variant.Filled"
|
||||||
OnClick="GoToCharge">
|
Color="@(IsMagicWallet && !MagicCeilingFull ? Color.Secondary : Color.Primary)"
|
||||||
شارژ حساب اصلی
|
StartIcon="@(IsMagicWallet && !MagicCeilingFull ? Icons.Material.Filled.AutoAwesome : Icons.Material.Filled.Payment)"
|
||||||
</MudButton>
|
OnClick="GoToCharge">
|
||||||
|
@ChargeButtonLabel
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
else if (!HasPurchasedPackage)
|
||||||
|
{
|
||||||
|
<MudButton Variant="Variant.Filled"
|
||||||
|
Color="Color.Primary"
|
||||||
|
StartIcon="@Icons.Material.Filled.CardGiftcard"
|
||||||
|
OnClick="GoToPackages">
|
||||||
|
مشاهده پکیجها
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudButton Variant="Variant.Filled"
|
||||||
|
Color="Color.Primary"
|
||||||
|
StartIcon="@Icons.Material.Filled.Handshake"
|
||||||
|
OnClick="GoToClubMembership">
|
||||||
|
امضای قرارداد باشگاه
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</MudDialog>
|
</MudDialog>
|
||||||
|
|
||||||
@@ -53,9 +101,52 @@
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public long ShortfallAmount { get; set; }
|
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 Cancel() => MudDialog.Cancel();
|
||||||
|
|
||||||
private void GoToCharge() => MudDialog.Close(DialogResult.Ok(ShortfallAmount));
|
private void GoToCharge()
|
||||||
|
{
|
||||||
|
if (IsMagicWallet && !MagicCeilingFull)
|
||||||
|
{
|
||||||
|
MudDialog.Cancel();
|
||||||
|
Navigation.NavigateTo(RouteConstants.Profile.MagicWallet);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MudDialog.Close(DialogResult.Ok(ShortfallAmount));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GoToPackages()
|
||||||
|
{
|
||||||
|
MudDialog.Cancel();
|
||||||
|
Navigation.NavigateTo(RouteConstants.Package.List);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GoToClubMembership()
|
||||||
|
{
|
||||||
|
MudDialog.Cancel();
|
||||||
|
Navigation.NavigateTo(RouteConstants.Club.Membership);
|
||||||
|
}
|
||||||
|
|
||||||
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
|
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
روش پرداخت خود را انتخاب کنید:
|
روش پرداخت خود را انتخاب کنید:
|
||||||
</MudText>
|
</MudText>
|
||||||
|
|
||||||
@* Direct Payment — always available *@
|
@* Direct Payment (ZarinPal) — always available *@
|
||||||
<MudPaper Elevation="0" Class="pa-4 rounded-lg pkg-payment-option"
|
<MudPaper Elevation="0" Class="pa-4 rounded-lg pkg-payment-option"
|
||||||
Style="border:2px solid var(--mud-palette-primary); cursor:pointer;"
|
Style="border:2px solid var(--mud-palette-primary); cursor:pointer;"
|
||||||
@onclick="() => SelectPaymentMethod(PaymentMethodType.DirectPayment)">
|
@onclick="() => SelectPaymentMethod(PaymentMethodType.DirectPayment)">
|
||||||
@@ -61,13 +61,29 @@
|
|||||||
<MudIcon Icon="@Icons.Material.Filled.CreditCard" Color="Color.Primary" Size="Size.Medium" />
|
<MudIcon Icon="@Icons.Material.Filled.CreditCard" Color="Color.Primary" Size="Size.Medium" />
|
||||||
<MudStack Spacing="0" Class="flex-grow-1">
|
<MudStack Spacing="0" Class="flex-grow-1">
|
||||||
<MudText Typo="Typo.subtitle1" Color="Color.Primary"><b>پرداخت مستقیم</b></MudText>
|
<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>
|
</MudStack>
|
||||||
<MudIcon Icon="@Icons.Material.Filled.ArrowBack" Color="Color.Primary" Size="Size.Small" />
|
<MudIcon Icon="@Icons.Material.Filled.ArrowBack" Color="Color.Primary" Size="Size.Small" />
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudPaper>
|
</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 *@
|
@* Daya Loan — only for base package AND first purchase cycle *@
|
||||||
@if (_selectedPackage.SupportsDayaPurchase && PurchaseCycleCount == 0)
|
@if (_selectedPackage.SupportsDayaPurchase && PurchaseCycleCount == 0)
|
||||||
{
|
{
|
||||||
@@ -229,6 +245,6 @@
|
|||||||
: "background:rgba(16,185,129,.12); color:#10b981;";
|
: "background:rgba(16,185,129,.12); color:#10b981;";
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum PaymentMethodType { DirectPayment, DayaLoan }
|
public enum PaymentMethodType { DirectPayment, TwentyCard, DayaLoan }
|
||||||
public record PackagePurchaseResult(PackageDto Package, PaymentMethodType Method);
|
public record PackagePurchaseResult(PackageDto Package, PaymentMethodType Method);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,11 +38,21 @@
|
|||||||
|
|
||||||
<MudTextField Value="ReferralCode"
|
<MudTextField Value="ReferralCode"
|
||||||
ValueChanged="@((string? v) => ReferralCodeChanged.InvokeAsync(v))"
|
ValueChanged="@((string? v) => ReferralCodeChanged.InvokeAsync(v))"
|
||||||
Label="کد معرف (اختیاری)"
|
Label="کد معرف سازمان (اختیاری)"
|
||||||
Variant="Variant.Outlined"
|
Variant="Variant.Outlined"
|
||||||
Immediate="true"
|
Immediate="true"
|
||||||
Placeholder="کد معرف را وارد کنید"
|
Placeholder="کد معرف ۱۰ رقمی"
|
||||||
Class="mb-3" />
|
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="کسی که شما را به کارابازار معرفی کرده — متفاوت از معرف سازمان" />
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
|
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ public partial class PhoneVerifyForm
|
|||||||
[Parameter] public string? ReferralCode { get; set; }
|
[Parameter] public string? ReferralCode { get; set; }
|
||||||
[Parameter] public EventCallback<string?> ReferralCodeChanged { 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 ──
|
// ── Verify actions ──
|
||||||
[Parameter] public EventCallback OnChangePhone { get; set; }
|
[Parameter] public EventCallback OnChangePhone { get; set; }
|
||||||
[Parameter] public EventCallback OnResendOtp { get; set; }
|
[Parameter] public EventCallback OnResendOtp { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
@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,6 +79,7 @@ public class AuthService
|
|||||||
var isSignMainContractStr = claims.FirstOrDefault(c => c.key == "IsSignMainContract").value;
|
var isSignMainContractStr = claims.FirstOrDefault(c => c.key == "IsSignMainContract").value;
|
||||||
var hasPurchasedPackageStr = claims.FirstOrDefault(c => c.key == "HasPurchasedPackage").value;
|
var hasPurchasedPackageStr = claims.FirstOrDefault(c => c.key == "HasPurchasedPackage").value;
|
||||||
var isClubMemberActiveStr = claims.FirstOrDefault(c => c.key == "IsClubMemberActive").value;
|
var isClubMemberActiveStr = claims.FirstOrDefault(c => c.key == "IsClubMemberActive").value;
|
||||||
|
var hasAddressStr = claims.FirstOrDefault(c => c.key == "HasAddress").value;
|
||||||
|
|
||||||
_userAuthInfo.UserId = long.TryParse(userIdStr, out var userId) ? userId : 0;
|
_userAuthInfo.UserId = long.TryParse(userIdStr, out var userId) ? userId : 0;
|
||||||
_userAuthInfo.FirstName = firstName ?? string.Empty;
|
_userAuthInfo.FirstName = firstName ?? string.Empty;
|
||||||
@@ -88,6 +89,7 @@ public class AuthService
|
|||||||
_userAuthInfo.IsSignMainContract = bool.TryParse(isSignMainContractStr, out var isSignMainContract) && isSignMainContract;
|
_userAuthInfo.IsSignMainContract = bool.TryParse(isSignMainContractStr, out var isSignMainContract) && isSignMainContract;
|
||||||
_userAuthInfo.HasPurchasedPackage = bool.TryParse(hasPurchasedPackageStr, out var hasPurchased) && hasPurchased;
|
_userAuthInfo.HasPurchasedPackage = bool.TryParse(hasPurchasedPackageStr, out var hasPurchased) && hasPurchased;
|
||||||
_userAuthInfo.IsClubMemberActive = bool.TryParse(isClubMemberActiveStr, out var isClubActive) && isClubActive;
|
_userAuthInfo.IsClubMemberActive = bool.TryParse(isClubMemberActiveStr, out var isClubActive) && isClubActive;
|
||||||
|
_userAuthInfo.HasAddress = bool.TryParse(hasAddressStr, out var hasAddress) && hasAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<(string key, string value)>?> GetTokenClaimsAsync()
|
private async Task<List<(string key, string value)>?> GetTokenClaimsAsync()
|
||||||
|
|||||||
@@ -219,6 +219,8 @@ public class DiscountOrderService
|
|||||||
2 => "ارسال شده",
|
2 => "ارسال شده",
|
||||||
3 => "تحویل داده شده",
|
3 => "تحویل داده شده",
|
||||||
4 => "لغو شده",
|
4 => "لغو شده",
|
||||||
|
5 => "آماده تحویل در دفتر",
|
||||||
|
6 => "مرجوع شده",
|
||||||
_ => "نامشخص"
|
_ => "نامشخص"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -228,7 +230,9 @@ public class DiscountOrderService
|
|||||||
1 => Color.Info,
|
1 => Color.Info,
|
||||||
2 => Color.Primary,
|
2 => Color.Primary,
|
||||||
3 => Color.Success,
|
3 => Color.Success,
|
||||||
4 => Color.Error,
|
4 => Color.Dark,
|
||||||
|
5 => Color.Primary,
|
||||||
|
6 => Color.Error,
|
||||||
_ => Color.Default
|
_ => Color.Default
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public class NetworkNodeDto
|
|||||||
public NetworkNodeDto? RightChild { get; set; }
|
public NetworkNodeDto? RightChild { get; set; }
|
||||||
public int Level { get; set; }
|
public int Level { get; set; }
|
||||||
public bool IsActive { get; set; } = true;
|
public bool IsActive { get; set; } = true;
|
||||||
public DateTime? JoinedAt { get; set; }
|
public DateTime? JoinedAt { get; set; } // تاریخ ثبتنام (UserCreated)
|
||||||
public bool IsClubActive { get; set; }
|
public bool IsClubActive { get; set; }
|
||||||
public string? ActivationWeekNumber { get; set; }
|
public string? ActivationWeekNumber { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -26,6 +26,18 @@ public class NetworkNodeDto
|
|||||||
/// نام پکیج موثر (مثلاً پکیج طلایی / نقرهای)
|
/// نام پکیج موثر (مثلاً پکیج طلایی / نقرهای)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? PackageName { get; set; }
|
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>
|
/// <summary>
|
||||||
@@ -53,6 +65,11 @@ public class FlatNetworkNodeDto
|
|||||||
/// نام پکیج موثر (مثلاً پکیج طلایی / نقرهای)
|
/// نام پکیج موثر (مثلاً پکیج طلایی / نقرهای)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? PackageName { get; set; }
|
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>
|
/// <summary>
|
||||||
@@ -92,7 +109,11 @@ public class NetworkTreeDto
|
|||||||
ActivationWeekNumber = node.ActivationWeekNumber,
|
ActivationWeekNumber = node.ActivationWeekNumber,
|
||||||
JoinedAt = node.JoinedAt,
|
JoinedAt = node.JoinedAt,
|
||||||
ReferralCode = node.ReferralCode,
|
ReferralCode = node.ReferralCode,
|
||||||
PackageName = node.PackageName
|
PackageName = node.PackageName,
|
||||||
|
GoldLeftLegTotal = node.GoldLeftLegTotal,
|
||||||
|
GoldRightLegTotal = node.GoldRightLegTotal,
|
||||||
|
SilverLeftLegTotal = node.SilverLeftLegTotal,
|
||||||
|
SilverRightLegTotal = node.SilverRightLegTotal
|
||||||
};
|
};
|
||||||
|
|
||||||
result.Add(flatNode);
|
result.Add(flatNode);
|
||||||
@@ -125,10 +146,19 @@ public class LastMemberDto
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class NetworkStatisticsDto
|
public class NetworkStatisticsDto
|
||||||
{
|
{
|
||||||
|
/// <summary>اعضای فعال باشگاه در پای چپ</summary>
|
||||||
public int LeftLegCount { get; set; }
|
public int LeftLegCount { get; set; }
|
||||||
|
/// <summary>اعضای فعال باشگاه در پای راست</summary>
|
||||||
public int RightLegCount { get; set; }
|
public int RightLegCount { get; set; }
|
||||||
|
/// <summary>LeftLegCount + RightLegCount</summary>
|
||||||
public int TotalMembers { get; set; }
|
public int TotalMembers { get; set; }
|
||||||
public int TreeDepth { get; set; }
|
public int TreeDepth { get; set; }
|
||||||
public string WeakerLeg { get; set; } = string.Empty; // "Left" or "Right"
|
public string WeakerLeg { get; set; } = string.Empty; // "Left" or "Right"
|
||||||
public LastMemberDto? LastMember { get; set; }
|
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,6 +96,10 @@ public class NetworkMembershipService
|
|||||||
TotalMembers = response.TotalMembers,
|
TotalMembers = response.TotalMembers,
|
||||||
TreeDepth = response.MaxDepth,
|
TreeDepth = response.MaxDepth,
|
||||||
WeakerLeg = response.WeakerLeg,
|
WeakerLeg = response.WeakerLeg,
|
||||||
|
GoldLeftLegTotal = response.GoldLeftLegTotal,
|
||||||
|
GoldRightLegTotal = response.GoldRightLegTotal,
|
||||||
|
SilverLeftLegTotal = response.SilverLeftLegTotal,
|
||||||
|
SilverRightLegTotal = response.SilverRightLegTotal,
|
||||||
LastMember = response.LastMember != null ? new LastMemberDto
|
LastMember = response.LastMember != null ? new LastMemberDto
|
||||||
{
|
{
|
||||||
UserId = response.LastMember.UserId,
|
UserId = response.LastMember.UserId,
|
||||||
@@ -138,8 +142,13 @@ public class NetworkMembershipService
|
|||||||
IsClubActive = node.IsClubActive,
|
IsClubActive = node.IsClubActive,
|
||||||
ReferralCode = node.ReferralCode,
|
ReferralCode = node.ReferralCode,
|
||||||
PackageName = string.IsNullOrWhiteSpace(node.PackageName) ? null : node.PackageName,
|
PackageName = string.IsNullOrWhiteSpace(node.PackageName) ? null : node.PackageName,
|
||||||
|
GoldLeftLegTotal = node.GoldLeftLegTotal,
|
||||||
|
GoldRightLegTotal = node.GoldRightLegTotal,
|
||||||
|
SilverLeftLegTotal = node.SilverLeftLegTotal,
|
||||||
|
SilverRightLegTotal = node.SilverRightLegTotal,
|
||||||
ActivationWeekNumber = node.ActivationWeekDefinitionId?.ToString(),
|
ActivationWeekNumber = node.ActivationWeekDefinitionId?.ToString(),
|
||||||
JoinedAt = node.JoinedAt?.ToDateTime(),
|
JoinedAt = node.UserCreated?.ToDateTime()
|
||||||
|
?? node.JoinedAt?.ToDateTime(),
|
||||||
LeftChild = MapNodeFromProto(node.LeftChild),
|
LeftChild = MapNodeFromProto(node.LeftChild),
|
||||||
RightChild = MapNodeFromProto(node.RightChild)
|
RightChild = MapNodeFromProto(node.RightChild)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -97,22 +97,4 @@ public class OrderService
|
|||||||
return 0.10;
|
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,6 +21,7 @@ public static class RouteConstants
|
|||||||
public const string Settings = "/profile/settings";
|
public const string Settings = "/profile/settings";
|
||||||
public const string ChangePassword = "/profile/change-password";
|
public const string ChangePassword = "/profile/change-password";
|
||||||
public const string Tree = "/profile/tree";
|
public const string Tree = "/profile/tree";
|
||||||
|
public const string Invitees = "/profile/invitees";
|
||||||
public const string Wallet = "/profile/wallet";
|
public const string Wallet = "/profile/wallet";
|
||||||
public const string MagicWallet = "/profile/magic-wallet";
|
public const string MagicWallet = "/profile/magic-wallet";
|
||||||
public const string ChargeDiscountWallet = "/profile/charge-discount-wallet";
|
public const string ChargeDiscountWallet = "/profile/charge-discount-wallet";
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace FrontOffice.Main.Utilities.Seo;
|
namespace FrontOffice.Main.Utilities.Seo;
|
||||||
@@ -30,6 +31,9 @@ public class SeoMetadataProvider
|
|||||||
[RouteConstants.Store.Products] = (
|
[RouteConstants.Store.Products] = (
|
||||||
"محصولات | کارابازار سلامت",
|
"محصولات | کارابازار سلامت",
|
||||||
"فروشگاه محصولات سلامت کارابازار — خرید آنلاین با پاداش باشگاه مشتریان."),
|
"فروشگاه محصولات سلامت کارابازار — خرید آنلاین با پاداش باشگاه مشتریان."),
|
||||||
|
[RouteConstants.Store.Categories] = (
|
||||||
|
"دستهبندیها | کارابازار سلامت",
|
||||||
|
"دستهبندی محصولات فروشگاه کارابازار سلامت."),
|
||||||
[RouteConstants.Package.List] = (
|
[RouteConstants.Package.List] = (
|
||||||
"پکیجها | کارابازار سلامت",
|
"پکیجها | کارابازار سلامت",
|
||||||
"پکیجهای عضویت باشگاه مشتریان کارابازار سلامت."),
|
"پکیجهای عضویت باشگاه مشتریان کارابازار سلامت."),
|
||||||
@@ -45,6 +49,18 @@ public class SeoMetadataProvider
|
|||||||
[RouteConstants.Registration.Wizard] = (
|
[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)
|
private static readonly HashSet<string> NoIndexPrefixes = new(StringComparer.OrdinalIgnoreCase)
|
||||||
@@ -62,8 +78,20 @@ public class SeoMetadataProvider
|
|||||||
"/discount-store/order/",
|
"/discount-store/order/",
|
||||||
RouteConstants.Gateway.OrdersChooser,
|
RouteConstants.Gateway.OrdersChooser,
|
||||||
RouteConstants.Gateway.CartChooser,
|
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)
|
public SeoMetadataProvider(BlogPostService blogPostService, IOptions<SeoSettings> settings)
|
||||||
{
|
{
|
||||||
_blogPostService = blogPostService;
|
_blogPostService = blogPostService;
|
||||||
@@ -121,14 +149,31 @@ public class SeoMetadataProvider
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (IsIndexableDynamicRoute(normalizedPath))
|
||||||
|
{
|
||||||
|
return new SeoPageMetadata
|
||||||
|
{
|
||||||
|
Title = _settings.SiteName,
|
||||||
|
Description = _settings.DefaultDescription,
|
||||||
|
CanonicalUrl = $"{baseUrl}{normalizedPath}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return new SeoPageMetadata
|
return new SeoPageMetadata
|
||||||
{
|
{
|
||||||
Title = _settings.SiteName,
|
Title = _settings.SiteName,
|
||||||
Description = _settings.DefaultDescription,
|
Description = _settings.DefaultDescription,
|
||||||
CanonicalUrl = $"{baseUrl}{normalizedPath}"
|
CanonicalUrl = $"{baseUrl}{normalizedPath}",
|
||||||
|
NoIndex = true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsIndexableDynamicRoute(string path) =>
|
||||||
|
ProductDetailPattern.IsMatch(path)
|
||||||
|
|| DiscountProductPattern.IsMatch(path)
|
||||||
|
|| PackageDetailPattern.IsMatch(path)
|
||||||
|
|| CheckoutPackagePattern.IsMatch(path);
|
||||||
|
|
||||||
private static string NormalizePath(string path)
|
private static string NormalizePath(string path)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
@@ -146,6 +191,9 @@ public class SeoMetadataProvider
|
|||||||
|
|
||||||
private static bool IsNoIndexPath(string path)
|
private static bool IsNoIndexPath(string path)
|
||||||
{
|
{
|
||||||
|
if (NoIndexPaths.Contains(path))
|
||||||
|
return true;
|
||||||
|
|
||||||
foreach (var prefix in NoIndexPrefixes)
|
foreach (var prefix in NoIndexPrefixes)
|
||||||
{
|
{
|
||||||
if (path.Equals(prefix, StringComparison.OrdinalIgnoreCase)
|
if (path.Equals(prefix, StringComparison.OrdinalIgnoreCase)
|
||||||
@@ -160,6 +208,6 @@ public class SeoMetadataProvider
|
|||||||
|
|
||||||
private static string GetHomeCrawlableContent() =>
|
private static string GetHomeCrawlableContent() =>
|
||||||
"کارابازار سلامت (KBS) — باشگاه مشتریان و فروشگاه محصولات سلامت. " +
|
"کارابازار سلامت (KBS) — باشگاه مشتریان و فروشگاه محصولات سلامت. " +
|
||||||
"با عضویت در باشگاه مشتریان KBS از پاداش شفاف، کیف پول جادویی، فروشگاه اعتباری و شبکه فروش بهرهمند شوید. " +
|
"با عضویت در باشگاه مشتریان KBS از پاداش شفاف، کیف پول جادویی، فروشگاه اعتباری و سازمان فروش بهرهمند شوید. " +
|
||||||
"ثبتنام رایگان در kbs2.ir.";
|
"ثبتنام رایگان در kbs2.ir.";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,5 @@ public class SeoSettings
|
|||||||
public string SiteUrl { get; set; } = "https://kbs2.ir";
|
public string SiteUrl { get; set; } = "https://kbs2.ir";
|
||||||
public string SiteName { get; set; } = "کارابازار سلامت";
|
public string SiteName { get; set; } = "کارابازار سلامت";
|
||||||
public string DefaultDescription { get; set; } =
|
public string DefaultDescription { get; set; } =
|
||||||
"باشگاه مشتریان کارابازار سلامت (KBS) — فروشگاه محصولات سلامت، پاداش شفاف، کیف پول و شبکه فروش.";
|
"باشگاه مشتریان کارابازار سلامت (KBS) — فروشگاه محصولات سلامت، پاداش شفاف، کیف پول و سازمان فروش.";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Xml;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
@@ -71,16 +72,29 @@ public class SitemapGenerator
|
|||||||
}
|
}
|
||||||
|
|
||||||
var document = new XDocument(
|
var document = new XDocument(
|
||||||
new XDeclaration("1.0", "UTF-8", null),
|
new XDeclaration("1.0", "utf-8", null),
|
||||||
new XElement(ns + "urlset", urls));
|
new XElement(ns + "urlset", urls));
|
||||||
|
|
||||||
var builder = new StringBuilder();
|
var settings = new XmlWriterSettings
|
||||||
using (var writer = new StringWriter(builder))
|
{
|
||||||
|
Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false),
|
||||||
|
Indent = true,
|
||||||
|
OmitXmlDeclaration = false
|
||||||
|
};
|
||||||
|
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
using (var writer = XmlWriter.Create(stream, settings))
|
||||||
{
|
{
|
||||||
document.Save(writer);
|
document.Save(writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.ToString();
|
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(
|
private static XElement CreateUrlElement(
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
namespace FrontOffice.Main.Utilities;
|
||||||
|
|
||||||
|
public enum ProductSortOption
|
||||||
|
{
|
||||||
|
PriceDesc,
|
||||||
|
PriceAsc,
|
||||||
|
Newest,
|
||||||
|
Title
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// وضعیت لیست فروشگاه در query string — فقط برای /products و /discount-store.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ShopListQueryState
|
||||||
|
{
|
||||||
|
public string Query { get; init; } = string.Empty;
|
||||||
|
public string? Sort { get; init; }
|
||||||
|
public long? CategoryId { get; init; }
|
||||||
|
public int Pages { get; init; } = 1;
|
||||||
|
|
||||||
|
public static ShopListQueryState Parse(Uri uri)
|
||||||
|
{
|
||||||
|
var q = QueryHelpers.ParseQuery(uri.Query);
|
||||||
|
return new ShopListQueryState
|
||||||
|
{
|
||||||
|
Query = GetString(q, "q") ?? string.Empty,
|
||||||
|
Sort = GetString(q, "sort"),
|
||||||
|
CategoryId = GetLong(q, "category"),
|
||||||
|
Pages = Math.Max(1, GetInt(q, "pages") ?? 1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToRelativeUrl(string path)
|
||||||
|
{
|
||||||
|
var dict = new Dictionary<string, string?>();
|
||||||
|
if (!string.IsNullOrWhiteSpace(Query))
|
||||||
|
dict["q"] = Query.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(Sort) && !string.Equals(Sort, "price-desc", StringComparison.OrdinalIgnoreCase))
|
||||||
|
dict["sort"] = Sort;
|
||||||
|
if (CategoryId is > 0)
|
||||||
|
dict["category"] = CategoryId.Value.ToString();
|
||||||
|
if (Pages > 1)
|
||||||
|
dict["pages"] = Pages.ToString();
|
||||||
|
|
||||||
|
return dict.Count == 0
|
||||||
|
? path
|
||||||
|
: QueryHelpers.AddQueryString(path, dict!);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Matches(ShopListQueryState other) =>
|
||||||
|
string.Equals(Query?.Trim() ?? "", other.Query?.Trim() ?? "", StringComparison.Ordinal)
|
||||||
|
&& string.Equals(NormalizeSort(Sort), NormalizeSort(other.Sort), StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& CategoryId == other.CategoryId
|
||||||
|
&& Pages == other.Pages;
|
||||||
|
|
||||||
|
public static string NormalizeSort(string? sort) =>
|
||||||
|
string.IsNullOrWhiteSpace(sort) ? "price-desc" : sort.Trim().ToLowerInvariant();
|
||||||
|
|
||||||
|
public static string ToSortKey(ProductSortOption option) => option switch
|
||||||
|
{
|
||||||
|
ProductSortOption.PriceAsc => "price-asc",
|
||||||
|
ProductSortOption.Newest => "newest",
|
||||||
|
ProductSortOption.Title => "title",
|
||||||
|
_ => "price-desc"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static ProductSortOption ParseSortOption(string? sort) => NormalizeSort(sort) switch
|
||||||
|
{
|
||||||
|
"price-asc" => ProductSortOption.PriceAsc,
|
||||||
|
"newest" => ProductSortOption.Newest,
|
||||||
|
"title" => ProductSortOption.Title,
|
||||||
|
_ => ProductSortOption.PriceDesc
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string ToApiSortBy(ProductSortOption option) => option switch
|
||||||
|
{
|
||||||
|
ProductSortOption.PriceAsc => "price asc",
|
||||||
|
ProductSortOption.Newest => "id desc",
|
||||||
|
ProductSortOption.Title => "title asc",
|
||||||
|
_ => "price desc"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string? GetString(Dictionary<string, StringValues> q, string key) =>
|
||||||
|
q.TryGetValue(key, out var v) ? v.FirstOrDefault() : null;
|
||||||
|
|
||||||
|
private static long? GetLong(Dictionary<string, StringValues> q, string key) =>
|
||||||
|
long.TryParse(GetString(q, key), out var n) ? n : null;
|
||||||
|
|
||||||
|
private static int? GetInt(Dictionary<string, StringValues> q, string key) =>
|
||||||
|
int.TryParse(GetString(q, key), out var n) ? n : null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
|
namespace FrontOffice.Main.Utilities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// اسکرول/فوکوس محصول پس از بازگشت از جزئیات — کلیدهای محدود به لیست فروشگاه (نه لندینگ).
|
||||||
|
/// </summary>
|
||||||
|
public static class ShopListScrollRestore
|
||||||
|
{
|
||||||
|
public const string StoreKey = "fo:store:scroll";
|
||||||
|
public const string DiscountKey = "fo:discount:scroll";
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
};
|
||||||
|
|
||||||
|
public static async Task SaveAsync(IJSRuntime js, string storageKey, long productId)
|
||||||
|
{
|
||||||
|
double scrollY = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
scrollY = await js.InvokeAsync<double>("eval", "window.scrollY || window.pageYOffset || 0");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = JsonSerializer.Serialize(new ScrollPayload(productId, scrollY), JsonOptions);
|
||||||
|
await js.InvokeVoidAsync("sessionStorage.setItem", storageKey, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<ScrollPayload?> TakeAsync(IJSRuntime js, string storageKey)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var raw = await js.InvokeAsync<string?>("sessionStorage.getItem", storageKey);
|
||||||
|
await js.InvokeVoidAsync("sessionStorage.removeItem", storageKey);
|
||||||
|
if (string.IsNullOrWhiteSpace(raw))
|
||||||
|
return null;
|
||||||
|
return JsonSerializer.Deserialize<ScrollPayload>(raw, JsonOptions);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task RestoreAsync(IJSRuntime js, ScrollPayload payload)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await js.InvokeVoidAsync("eval",
|
||||||
|
$@"(function(){{
|
||||||
|
var el = document.getElementById('shop-product-{payload.ProductId}');
|
||||||
|
if (el) {{ el.scrollIntoView({{ block: 'center', behavior: 'instant' }}); return; }}
|
||||||
|
window.scrollTo(0, {payload.ScrollY.ToString(System.Globalization.CultureInfo.InvariantCulture)});
|
||||||
|
}})()");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record ScrollPayload(long ProductId, double ScrollY);
|
||||||
|
}
|
||||||
@@ -11,4 +11,5 @@ public class UserAuthInfo
|
|||||||
public bool IsSignMainContract { get; set; }
|
public bool IsSignMainContract { get; set; }
|
||||||
public bool HasPurchasedPackage { get; set; }
|
public bool HasPurchasedPackage { get; set; }
|
||||||
public bool IsClubMemberActive { get; set; }
|
public bool IsClubMemberActive { get; set; }
|
||||||
|
public bool HasAddress { get; set; }
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,17 @@
|
|||||||
"Seo": {
|
"Seo": {
|
||||||
"SiteUrl": "https://kbs1.ir",
|
"SiteUrl": "https://kbs1.ir",
|
||||||
"SiteName": "کارابازار سلامت",
|
"SiteName": "کارابازار سلامت",
|
||||||
"DefaultDescription": "باشگاه مشتریان کارابازار سلامت (KBS) — فروشگاه محصولات سلامت، پاداش شفاف، کیف پول و شبکه فروش."
|
"DefaultDescription": "باشگاه مشتریان کارابازار سلامت (KBS) — فروشگاه محصولات سلامت، پاداش شفاف، کیف پول و سازمان فروش."
|
||||||
},
|
},
|
||||||
"GwUrl": "https://cms.se.kbs1.ir",
|
"GwUrl": "https://cms.se.kbs1.ir",
|
||||||
"DownloadUrl": "",
|
"DownloadUrl": "",
|
||||||
|
"TwentyCard": {
|
||||||
|
"CreditPortalUrl": "https://userpanel.20card.ir/"
|
||||||
|
},
|
||||||
|
"Landing": {
|
||||||
|
"ShowStepsSection": false,
|
||||||
|
"ShowFeaturesSection": false
|
||||||
|
},
|
||||||
"EncryptionSettings": {
|
"EncryptionSettings": {
|
||||||
"Key": "kmcQ3XTmH4mrdh8VHziuscyf8LLYjG//Kyni81nH/0E=",
|
"Key": "kmcQ3XTmH4mrdh8VHziuscyf8LLYjG//Kyni81nH/0E=",
|
||||||
"IV": "1wyF3Tt142MOkCpIyCxh/g=="
|
"IV": "1wyF3Tt142MOkCpIyCxh/g=="
|
||||||
|
|||||||
@@ -2,10 +2,17 @@
|
|||||||
"Seo": {
|
"Seo": {
|
||||||
"SiteUrl": "http://localhost:5268",
|
"SiteUrl": "http://localhost:5268",
|
||||||
"SiteName": "کارابازار سلامت",
|
"SiteName": "کارابازار سلامت",
|
||||||
"DefaultDescription": "باشگاه مشتریان کارابازار سلامت (KBS) — فروشگاه محصولات سلامت، پاداش شفاف، کیف پول و شبکه فروش."
|
"DefaultDescription": "باشگاه مشتریان کارابازار سلامت (KBS) — فروشگاه محصولات سلامت، پاداش شفاف، کیف پول و سازمان فروش."
|
||||||
},
|
},
|
||||||
"GwUrl": "https://cms.se.kbs1.ir",
|
"GwUrl": "https://cms.se.kbs1.ir",
|
||||||
"DownloadUrl": "",
|
"DownloadUrl": "",
|
||||||
|
"TwentyCard": {
|
||||||
|
"CreditPortalUrl": "https://userpanel.20card.ir/"
|
||||||
|
},
|
||||||
|
"Landing": {
|
||||||
|
"ShowStepsSection": false,
|
||||||
|
"ShowFeaturesSection": false
|
||||||
|
},
|
||||||
"EncryptionSettings": {
|
"EncryptionSettings": {
|
||||||
"Key": "kmcQ3XTmH4mrdh8VHziuscyf8LLYjG//Kyni81nH/0E=",
|
"Key": "kmcQ3XTmH4mrdh8VHziuscyf8LLYjG//Kyni81nH/0E=",
|
||||||
"IV": "1wyF3Tt142MOkCpIyCxh/g=="
|
"IV": "1wyF3Tt142MOkCpIyCxh/g=="
|
||||||
|
|||||||
@@ -118,6 +118,16 @@
|
|||||||
line-height: 1.2;
|
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 {
|
.node-level-sm {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
@@ -141,6 +151,7 @@
|
|||||||
max-width: 120px;
|
max-width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* D3: تعداد پکیج طلایی/نقرهای فقط در نوار پایین شجره (.org-chart-stats) */
|
||||||
/* Referral Code - only for club active members */
|
/* Referral Code - only for club active members */
|
||||||
.node-referral {
|
.node-referral {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -370,14 +381,27 @@
|
|||||||
/* Stats Bar */
|
/* Stats Bar */
|
||||||
.org-chart-stats {
|
.org-chart-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 24px;
|
gap: 8px;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
border-top: 1px solid #e9ecef;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
.stat-item {
|
.stat-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -385,6 +409,21 @@
|
|||||||
font-size: 13px;
|
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 {
|
.stat-label {
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
}
|
}
|
||||||
@@ -448,10 +487,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.org-chart-stats {
|
.org-chart-stats {
|
||||||
flex-wrap: wrap;
|
gap: 8px;
|
||||||
gap: 12px;
|
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.org-chart-stats-row {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.stat-item {
|
.stat-item {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@@ -1617,6 +1617,12 @@ html, body {
|
|||||||
overflow: hidden;
|
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 ── */
|
/* ── Register Wizard Unified ── */
|
||||||
.wizard-section {
|
.wizard-section {
|
||||||
min-height: 80vh;
|
min-height: 80vh;
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ window.OrgChart = {
|
|||||||
.container('#' + containerId)
|
.container('#' + containerId)
|
||||||
.data(data)
|
.data(data)
|
||||||
.nodeWidth((d) => 140)
|
.nodeWidth((d) => 140)
|
||||||
.nodeHeight((d) => 85)
|
.nodeHeight((d) => 98)
|
||||||
.childrenMargin((d) => 50)
|
.childrenMargin((d) => 50)
|
||||||
.compactMarginBetween((d) => 15)
|
.compactMarginBetween((d) => 15)
|
||||||
.compactMarginPair((d) => 15)
|
.compactMarginPair((d) => 15)
|
||||||
@@ -86,6 +86,7 @@ window.OrgChart = {
|
|||||||
<div class="node-avatar-sm">${firstChar}</div>
|
<div class="node-avatar-sm">${firstChar}</div>
|
||||||
<div class="node-info">
|
<div class="node-info">
|
||||||
<div class="node-name-sm">${data.fullName || 'بدون نام'}</div>
|
<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>
|
<div class="node-level-sm">L${data.level || 0}${!isRoot ? ' • ' + (data.position === 'Left' ? 'چپ' : 'راست') : ''}</div>
|
||||||
${packageBadge}
|
${packageBadge}
|
||||||
${referralCodeHtml}
|
${referralCodeHtml}
|
||||||
@@ -296,7 +297,7 @@ window.OrgChart = {
|
|||||||
isActive: node.isActive !== false,
|
isActive: node.isActive !== false,
|
||||||
isClubActive: node.isClubActive || false,
|
isClubActive: node.isClubActive || false,
|
||||||
activationWeekNumber: node.activationWeekNumber,
|
activationWeekNumber: node.activationWeekNumber,
|
||||||
joinedAt: node.joinedAt
|
registeredAt: node.registeredAt || node.joinedAt || ''
|
||||||
};
|
};
|
||||||
|
|
||||||
result.push(flatNode);
|
result.push(flatNode);
|
||||||
|
|||||||
@@ -13,5 +13,9 @@ Disallow: /discount-store/orders
|
|||||||
Disallow: /discount-store/order/
|
Disallow: /discount-store/order/
|
||||||
Disallow: /my-orders
|
Disallow: /my-orders
|
||||||
Disallow: /my-cart
|
Disallow: /my-cart
|
||||||
|
Disallow: /wishlist
|
||||||
|
Disallow: /commission/
|
||||||
|
Disallow: /network/
|
||||||
|
Disallow: /my-packages
|
||||||
|
|
||||||
Sitemap: https://kbs2.ir/sitemap.xml
|
Sitemap: https://kbs2.ir/sitemap.xml
|
||||||
|
|||||||
Reference in New Issue
Block a user