Compare commits
171 Commits
4286399b97
...
production
| Author | SHA1 | Date | |
|---|---|---|---|
| 3eff5b6dfa | |||
| aa97d25302 | |||
| 24ae4ec15c | |||
| c3ee46ff62 | |||
| e9f8328404 | |||
| 6a9d142dc6 | |||
| 1d78633e17 | |||
| cbc61ad0f5 | |||
| 68d0aed468 | |||
| a9aa03d6a8 | |||
| e63b80fe47 | |||
| 4115f64fff | |||
| 7d892c2f6a | |||
| d757c8220c | |||
| b53d9facc9 | |||
| 5a004e5c88 | |||
| 5d83eb4e05 | |||
| 6126ecf023 | |||
| 31f0fc4860 | |||
| 0a2c5f913b | |||
| 2f040cab02 | |||
| 933cdb1094 | |||
| 4596296d97 | |||
| f2316eafe4 | |||
| 9c6149bbb9 | |||
| 8afc684098 | |||
| c17ad68c30 | |||
| 9378b7474c | |||
| 68d7647cb1 | |||
| c4712a00ac | |||
| 3b4562665d | |||
| 52c8965704 | |||
| 7fb2062721 | |||
| 797bfef964 | |||
| 6d8d681046 | |||
| b7d1fe13c7 | |||
| 67f57ce99f | |||
| c4cbe94ed7 | |||
| ed24f505d3 | |||
| 3c2a0edca8 | |||
| aed6258fb3 | |||
| e6d1d833ec | |||
| a8c52db544 | |||
| c0c7bb3ac5 | |||
| d0fe99492d | |||
| d38f50584c | |||
| 1c67d7798a | |||
| 2a7bb0592d | |||
| 9699833ac9 | |||
| b3e6066c90 | |||
| 148b8e4011 | |||
| 1ef5edf939 | |||
| 76dbee7d41 | |||
| 148a9e0493 | |||
| 851a1bbc4f | |||
| aa1d21a616 | |||
| 7147e9930f | |||
| 85208da1b5 | |||
| ea7414dc7f | |||
| a450d9a001 | |||
| 92c992249e | |||
| 6939780cd1 | |||
| e6cf90e3fa | |||
| e0203541bf | |||
| 8be98ae33e | |||
| c96377a674 | |||
| 89f5241680 | |||
| f1b0085181 | |||
| 60c91438ec | |||
| 172101d0ab | |||
| 1d8f34ae6c | |||
| 76b7211a5c | |||
| fe72774d08 | |||
| bdea2e8aef | |||
| 0386a2b1aa | |||
| 8f29fbaf71 | |||
| 7a62e3caac | |||
| 4eaca9bc4c | |||
| e541aec115 | |||
| c9affe62bd | |||
| 6147499f9c | |||
| 3409133757 | |||
| af51c02d3d | |||
| b9171d548b | |||
| 743403e4d4 | |||
| 44fd5f29d9 | |||
| e7bed65149 | |||
| 6fc00e134f | |||
| fac3d2c85e | |||
| 51c20116a7 | |||
| 4454b8dd3a | |||
| aea614e095 | |||
| 6ec221cb41 | |||
| 05af68bae1 | |||
| 4fd039b250 | |||
| 49017027d0 | |||
| d84dfe3bc5 | |||
| 8d9942ddc2 | |||
| 2bbb1ba4c7 | |||
| 7a27347142 | |||
| 5d563aa405 | |||
| b05f92195a | |||
| fc5d81425a | |||
| 66884d8216 | |||
| ad47f1f495 | |||
| 1a5a71a27f | |||
| 8a285e485b | |||
| 21ee004869 | |||
| 31226399ae | |||
| 53f56636b3 | |||
| a2c496c16f | |||
| d28d826a4b | |||
| b7543fc10c | |||
| 3e9f40bd41 | |||
| 229aa6aec7 | |||
| 910a1ebfcc | |||
| c062bd358c | |||
| 2d833605b4 | |||
| 978475360c | |||
| 81ee23e313 | |||
| 230453e491 | |||
| 1a65babb9d | |||
| bbe783dc83 | |||
| 6f5bfba0a5 | |||
| 98a4ba9e99 | |||
| ece29956b2 | |||
| d69b838a72 | |||
| 6b140c3bd8 | |||
| 332fe0112c | |||
| f846a927a7 | |||
| 6dec03cf4d | |||
| e808cf827e | |||
| 8e98b1f3c7 | |||
| 0ff1a17419 | |||
| 0c5c30ca42 | |||
| 0f99d4c80c | |||
| 33d4f4c543 | |||
| 787fe6def6 | |||
| 51c5edc0ae | |||
| ec923dc22e | |||
| be0cb98222 | |||
| 869cf7e516 | |||
| d1940033db | |||
| cc721da65f | |||
| 2fc7733c84 | |||
| 82068bf8f8 | |||
| f00ade566c | |||
| 5a4774b1f3 | |||
| 2bfac7afaa | |||
| 00290e91fd | |||
| 5c8c0418ae | |||
| 6c2d011361 | |||
| 200825064a | |||
| 59d9a0644a | |||
| dea8de348e | |||
| 2c899f6029 | |||
| 0262ae68c4 | |||
| a5904cd077 | |||
| c7df3db502 | |||
| 2b460e4e82 | |||
| 419bcbdf7a | |||
| 17f8346a87 | |||
| f689a95ddd | |||
| ef4e2b5964 | |||
| 10607eb5fa | |||
| 22b5ce3927 | |||
| 9146391928 | |||
| f5b8052245 | |||
| 523754af2c | |||
| 88c691c3fb | |||
| 5cec4e9313 |
@@ -0,0 +1,2 @@
|
|||||||
|
# Keep production config from current branch during merges — never overwrite
|
||||||
|
src/BackOffice/wwwroot/appsettings.Production.json merge=ours
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
|
|
||||||
name: Push nuget and docker image Actions Workflow
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- stage
|
|
||||||
jobs:
|
|
||||||
Deploy:
|
|
||||||
runs-on: windows
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: https://git.afrino.co/actions/checkout@v3
|
|
||||||
- name: Setup dotnet
|
|
||||||
uses: https://git.afrino.co/actions/setup-dotnet@v3
|
|
||||||
with:
|
|
||||||
dotnet-version: 7.0.x
|
|
||||||
|
|
||||||
- name: Remove Package Source
|
|
||||||
run: dotnet nuget remove source FourSat
|
|
||||||
continue-on-error: true
|
|
||||||
- name: Add Package Source
|
|
||||||
run: dotnet nuget add source --name FourSat --username systemuser --password sZSA7PTiv3pUSQZ https://git.afrino.co/api/packages/FourSat/nuget/index.json --store-password-in-clear-text
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: dotnet restore ".\src\BackOffice\BackOffice.csproj"
|
|
||||||
- name: Build
|
|
||||||
run: dotnet build ".\src\BackOffice\BackOffice.csproj" --configuration Release --no-restore
|
|
||||||
- name: Test
|
|
||||||
run: dotnet test ".\src\BackOffice\BackOffice.csproj" --no-restore --verbosity normal
|
|
||||||
- name: Recycle Apppool
|
|
||||||
run: |
|
|
||||||
& "C:\Windows\System32\inetsrv\appcmd.exe" recycle apppool /apppool.name:admin.kbs1.ir
|
|
||||||
shell: powershell
|
|
||||||
- name: Stop Website
|
|
||||||
run: |
|
|
||||||
& "C:\Windows\System32\inetsrv\appcmd.exe" stop site /site.name:admin.kbs1.ir
|
|
||||||
shell: powershell
|
|
||||||
- name: Publish
|
|
||||||
run: dotnet publish ".\src\BackOffice\BackOffice.csproj" -c Release -o publish
|
|
||||||
- name: Copy Publish To IIS Directory
|
|
||||||
run: Get-ChildItem -Path "publish\*" | Copy-Item -Destination "E:\kbs1.ir\admin.kbs1.ir\" -Recurse -Force
|
|
||||||
- name: Start Website
|
|
||||||
run: |
|
|
||||||
& "C:\Windows\System32\inetsrv\appcmd.exe" start site /site.name:admin.kbs1.ir
|
|
||||||
shell: powershell
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Build and Deploy
|
name: Build and Deploy to Kubernetes
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -6,53 +6,76 @@ on:
|
|||||||
- kub-stage
|
- kub-stage
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: gitea-svc:3000
|
REGISTRY: 194.5.195.53:30080
|
||||||
IMAGE_NAME: admin/backoffice
|
IMAGE_NAME: admin/backoffice
|
||||||
|
K8S_SERVER: 194.5.195.53
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build-and-deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: docker:latest
|
image: 194.5.195.53:32082/docker-sshpass:latest
|
||||||
options: --privileged
|
options: --privileged
|
||||||
steps:
|
steps:
|
||||||
- name: Install git
|
- name: Start Docker daemon
|
||||||
run: |
|
run: |
|
||||||
export http_proxy=http://proxyuser:87zH26nbqT2@46.249.98.211:3128
|
mkdir -p /etc/docker
|
||||||
export https_proxy=http://proxyuser:87zH26nbqT2@46.249.98.211:3128
|
cat > /etc/docker/daemon.json << 'DAEMON'
|
||||||
apk add --no-cache git
|
{
|
||||||
|
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
|
||||||
|
}
|
||||||
|
DAEMON
|
||||||
|
echo "🚀 Starting Docker daemon..."
|
||||||
|
dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs &
|
||||||
|
|
||||||
|
# Wait up to 3 minutes for Docker to be ready
|
||||||
|
for i in $(seq 1 90); do
|
||||||
|
if docker info >/dev/null 2>&1; then
|
||||||
|
echo "✅ Docker daemon is ready (attempt $i)"
|
||||||
|
docker version
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "⏳ Waiting for Docker daemon... (attempt $i/90)"
|
||||||
|
sleep 2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Final check
|
||||||
|
if ! docker info >/dev/null 2>&1; then
|
||||||
|
echo "❌ Docker daemon failed to start after 3 minutes"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
run: |
|
run: |
|
||||||
git clone --depth 1 --branch kub-stage http://gitea-svc:3000/admin/BackOffice.git .
|
git clone --depth 1 --branch kub-stage http://gitea-svc:3000/admin/BackOffice.git .
|
||||||
git log -1 --format="%H %s"
|
|
||||||
|
|
||||||
- name: Start Docker daemon with insecure registry
|
- name: Login to Docker registries
|
||||||
run: |
|
run: |
|
||||||
mkdir -p /etc/docker
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 194.5.195.53:32082 -u admin --password-stdin
|
||||||
cat > /etc/docker/daemon.json << 'DOCKER_EOF'
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
|
||||||
{
|
|
||||||
"insecure-registries": ["gitea-svc:3000"]
|
|
||||||
}
|
|
||||||
DOCKER_EOF
|
|
||||||
|
|
||||||
dockerd &
|
|
||||||
for i in $(seq 1 30); do
|
|
||||||
docker info >/dev/null 2>&1 && break || sleep 2
|
|
||||||
done
|
|
||||||
docker info
|
|
||||||
|
|
||||||
- name: Build Docker Image
|
- name: Build Docker Image
|
||||||
run: |
|
run: |
|
||||||
cd src/BackOffice
|
cd src
|
||||||
docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
|
DOCKER_BUILDKIT=0 docker build --network host -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest -f BackOffice/Dockerfile .
|
||||||
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
|
|
||||||
--build-arg HTTP_PROXY=http://proxyuser:87zH26nbqT2@46.249.98.211:3128 \
|
|
||||||
--build-arg HTTPS_PROXY=http://proxyuser:87zH26nbqT2@46.249.98.211:3128 \
|
|
||||||
.
|
|
||||||
|
|
||||||
- name: Push to Registry
|
- name: Push to Registry
|
||||||
run: |
|
run: |
|
||||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
|
|
||||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
|
|
||||||
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||||
|
|
||||||
|
- name: Deploy to Kubernetes
|
||||||
|
run: |
|
||||||
|
export SSHPASS="${{ secrets.SERVER_PASSWORD }}"
|
||||||
|
|
||||||
|
# Copy K8s manifests to server
|
||||||
|
sshpass -e scp -o StrictHostKeyChecking=no k8s/staging/backoffice-deployment.yaml root@${{ env.K8S_SERVER }}:/tmp/backoffice-deployment.yaml
|
||||||
|
|
||||||
|
# Apply manifests and restart
|
||||||
|
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
|
||||||
|
kubectl apply -f /tmp/backoffice-deployment.yaml &&
|
||||||
|
kubectl rollout restart deployment/backoffice &&
|
||||||
|
kubectl rollout status deployment/backoffice --timeout=180s &&
|
||||||
|
rm -f /tmp/backoffice-deployment.yaml
|
||||||
|
"
|
||||||
|
echo "✅ Deployed!"
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
name: Build and Deploy to Production
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- production
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: 194.5.195.53:30080
|
||||||
|
IMAGE_NAME: admin/backoffice
|
||||||
|
K8S_SERVER: 45.149.79.127
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: 194.5.195.53:32082/docker-sshpass:latest
|
||||||
|
options: --privileged
|
||||||
|
steps:
|
||||||
|
- name: Start Docker daemon
|
||||||
|
run: |
|
||||||
|
mkdir -p /etc/docker
|
||||||
|
cat > /etc/docker/daemon.json << 'DAEMON'
|
||||||
|
{
|
||||||
|
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
|
||||||
|
}
|
||||||
|
DAEMON
|
||||||
|
echo "🚀 Starting Docker daemon..."
|
||||||
|
dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs &
|
||||||
|
|
||||||
|
for i in $(seq 1 90); do
|
||||||
|
if docker info >/dev/null 2>&1; then
|
||||||
|
echo "✅ Docker daemon is ready (attempt $i)"
|
||||||
|
docker version
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "⏳ Waiting for Docker daemon... (attempt $i/90)"
|
||||||
|
sleep 2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! docker info >/dev/null 2>&1; then
|
||||||
|
echo "❌ Docker daemon failed to start"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
run: |
|
||||||
|
git clone --depth 1 --branch production http://gitea-svc:3000/admin/BackOffice.git .
|
||||||
|
|
||||||
|
- name: Login to Docker registries
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 194.5.195.53:32082 -u admin --password-stdin
|
||||||
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
|
||||||
|
|
||||||
|
- name: Build Docker Image
|
||||||
|
run: |
|
||||||
|
cd src
|
||||||
|
DOCKER_BUILDKIT=0 docker build --network host -f BackOffice/Dockerfile \
|
||||||
|
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
|
||||||
|
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod \
|
||||||
|
.
|
||||||
|
|
||||||
|
- name: Push to Registry
|
||||||
|
run: |
|
||||||
|
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
|
||||||
|
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod
|
||||||
|
|
||||||
|
- name: Deploy to Production
|
||||||
|
run: |
|
||||||
|
export SSHPASS="${{ secrets.SERVER_PASSWORD }}"
|
||||||
|
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
|
||||||
|
kubectl set image deployment/backoffice backoffice=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
|
||||||
|
kubectl rollout status deployment/backoffice --timeout=300s
|
||||||
|
"
|
||||||
|
echo "✅ Deployed to Production!"
|
||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
*.user
|
*.user
|
||||||
*.userosscache
|
*.userosscache
|
||||||
*.sln.docstates
|
*.sln.docstates
|
||||||
|
|
||||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
*.userprefs
|
*.userprefs
|
||||||
|
|
||||||
|
|||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
|
||||||
WORKDIR /src
|
|
||||||
COPY src/*.sln ./ 2>/dev/null || true
|
|
||||||
COPY src/*/*.csproj ./
|
|
||||||
RUN for file in *.csproj; do mkdir -p "${file%.*}" && mv "$file" "${file%.*}/"; done 2>/dev/null || true
|
|
||||||
RUN dotnet restore "BackOffice/BackOffice.csproj" || dotnet restore
|
|
||||||
COPY src/ ./
|
|
||||||
RUN dotnet publish "BackOffice/BackOffice.csproj" -c Release -o /app/publish
|
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=build /app/publish .
|
|
||||||
ENV ASPNETCORE_URLS=http://+:8080
|
|
||||||
EXPOSE 8080
|
|
||||||
ENTRYPOINT ["dotnet", "BackOffice.dll"]
|
|
||||||
Executable
+79
@@ -0,0 +1,79 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build all BFF proto dependencies and copy DLLs to libs folder
|
||||||
|
# This script is automatically run before build via Directory.Build.targets
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
LIBS_DIR="$SCRIPT_DIR/libs"
|
||||||
|
BFF_PROTOS_DIR="$SCRIPT_DIR/../BackOffice.BFF/src/Protobufs"
|
||||||
|
|
||||||
|
echo "🔧 Building all proto dependencies..."
|
||||||
|
|
||||||
|
# Create libs directory
|
||||||
|
mkdir -p "$LIBS_DIR"
|
||||||
|
|
||||||
|
# All proto projects
|
||||||
|
PROTO_PROJECTS=(
|
||||||
|
"BackOffice.BFF.Common.Protobuf"
|
||||||
|
"BackOffice.BFF.Category.Protobuf"
|
||||||
|
"BackOffice.BFF.ClubMembership.Protobuf"
|
||||||
|
"BackOffice.BFF.Commission.Protobuf"
|
||||||
|
"BackOffice.BFF.Configuration.Protobuf"
|
||||||
|
"BackOffice.BFF.DiscountCategory.Protobuf"
|
||||||
|
"BackOffice.BFF.DiscountOrder.Protobuf"
|
||||||
|
"BackOffice.BFF.DiscountProduct.Protobuf"
|
||||||
|
"BackOffice.BFF.DiscountShoppingCart.Protobuf"
|
||||||
|
"BackOffice.BFF.Health.Protobuf"
|
||||||
|
"BackOffice.BFF.Inventory.Protobuf"
|
||||||
|
"BackOffice.BFF.ManualPayment.Protobuf"
|
||||||
|
"BackOffice.BFF.NetworkMembership.Protobuf"
|
||||||
|
"BackOffice.BFF.Otp.Protobuf"
|
||||||
|
"BackOffice.BFF.Package.Protobuf"
|
||||||
|
"BackOffice.BFF.Products.Protobuf"
|
||||||
|
"BackOffice.BFF.ProductTag.Protobuf"
|
||||||
|
"BackOffice.BFF.PublicMessage.Protobuf"
|
||||||
|
"BackOffice.BFF.Role.Protobuf"
|
||||||
|
"BackOffice.BFF.Tag.Protobuf"
|
||||||
|
"BackOffice.BFF.User.Protobuf"
|
||||||
|
"BackOffice.BFF.UserAddress.Protobuf"
|
||||||
|
"BackOffice.BFF.UserOrder.Protobuf"
|
||||||
|
"BackOffice.BFF.UserRole.Protobuf"
|
||||||
|
)
|
||||||
|
|
||||||
|
BUILT_COUNT=0
|
||||||
|
FAILED_COUNT=0
|
||||||
|
|
||||||
|
for PROJECT in "${PROTO_PROJECTS[@]}"; do
|
||||||
|
PROJECT_DIR="$BFF_PROTOS_DIR/$PROJECT"
|
||||||
|
if [ -d "$PROJECT_DIR" ]; then
|
||||||
|
echo "📦 Building $PROJECT..."
|
||||||
|
if dotnet build "$PROJECT_DIR" -c Release --verbosity quiet 2>/dev/null; then
|
||||||
|
# Copy DLL and PDB - check both net9.0 and net8.0
|
||||||
|
if [ -f "$PROJECT_DIR/bin/Release/net9.0/$PROJECT.dll" ]; then
|
||||||
|
cp "$PROJECT_DIR/bin/Release/net9.0/$PROJECT.dll" "$LIBS_DIR/" && ((BUILT_COUNT++))
|
||||||
|
cp "$PROJECT_DIR/bin/Release/net9.0/$PROJECT.pdb" "$LIBS_DIR/" 2>/dev/null || true
|
||||||
|
elif [ -f "$PROJECT_DIR/bin/Release/net8.0/$PROJECT.dll" ]; then
|
||||||
|
cp "$PROJECT_DIR/bin/Release/net8.0/$PROJECT.dll" "$LIBS_DIR/" && ((BUILT_COUNT++))
|
||||||
|
cp "$PROJECT_DIR/bin/Release/net8.0/$PROJECT.pdb" "$LIBS_DIR/" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
echo "⚠️ DLL not found for: $PROJECT"
|
||||||
|
((FAILED_COUNT++))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "⚠️ Failed to build: $PROJECT"
|
||||||
|
((FAILED_COUNT++))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "⚠️ Project not found: $PROJECT"
|
||||||
|
((FAILED_COUNT++))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Build complete!"
|
||||||
|
echo " Built: $BUILT_COUNT projects"
|
||||||
|
echo " Failed: $FAILED_COUNT projects"
|
||||||
|
echo " Output: $LIBS_DIR"
|
||||||
|
echo ""
|
||||||
|
ls -la "$LIBS_DIR"/*.dll 2>/dev/null | wc -l | xargs -I {} echo " Total DLLs: {}"
|
||||||
@@ -0,0 +1,421 @@
|
|||||||
|
# سرویسهای BackOffice (پنل ادمین)
|
||||||
|
|
||||||
|
این سند تمام سرویسهایی را که **BackOffice** (فرانت ادمین) به **CMS** از طریق gRPC Gateway (`GwUrl`) فراخوانی میکند، به ترتیب **Use Case** فهرست میکند.
|
||||||
|
|
||||||
|
- **مسیر wrapper سرویسها:** `BackOffice/src/BackOffice/Services/`
|
||||||
|
- **ثبت gRPC:** `BackOffice/src/BackOffice/Common/Configure/ConfigureService.cs`
|
||||||
|
- **تاریخ:** ۱۴۰۵/۰۳/۱۷ (بروز: خرداد ۱۴۰۵ — گزارشهای read-only + proto 0.0.201؛ **بروز ۱۴۰۵/۰۳/۱۸ — دفتر تراکنشهای مالی proto 0.0.203**)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۱. ورود و احراز هویت ادمین
|
||||||
|
|
||||||
|
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||||
|
|---|-------------|---------------|-----------------|--------|
|
||||||
|
| 1.1 | `OtpTokenContract.CreateNewOtpToken` | `OtpTokenContract` | `/Login/` (`LoginPage`) | ارسال OTP ورود ادمین |
|
||||||
|
| 1.2 | `UserContract.VerifyOtpToken` | `UserContract` | `/VerifyCodePage/{Mobile}` | تأیید OTP و دریافت JWT |
|
||||||
|
| 1.3 | `OtpTokenContract.CreateNewOtpToken` (resend) | `OtpTokenContract` | `/VerifyCodePage/{Mobile}` | ارسال مجدد OTP |
|
||||||
|
| 1.4 | `AuthorizationService.HasPermissionAsync` | — (JWT Role/Claim) | `NavMenu`, صفحات محافظتشده | کنترل نمایش منو بر اساس نقش (فعلاً role-based، نه API) |
|
||||||
|
| 1.5 | `AppTokenProvider` / `TokenHandler` | — (localStorage) | سراسری | attach کردن Bearer token به همه gRPC callها |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۲. داشبورد
|
||||||
|
|
||||||
|
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||||
|
|---|-------------|---------------|-----------------|--------|
|
||||||
|
| 2.1 | `UserContract.GetAllUserByFilter` | `UserContract` | `/` (Index) | شمارش کاربران |
|
||||||
|
| 2.2 | `ProductsContract.GetAllProductsByFilter` | `ProductsContract` | `/` | شمارش محصولات |
|
||||||
|
| 2.3 | `PackageContract.GetAllPackageByFilter` | `PackageContract` | `/` | شمارش پکیجها |
|
||||||
|
| 2.4 | `CategoryContract.GetAllCategoryByFilter` | `CategoryContract` | `/` | شمارش دستهبندیها |
|
||||||
|
| 2.5 | `CommissionContract.GetAvailableWeeks` | `CommissionContract` | `/` | هفته جاری کمیسیون |
|
||||||
|
| 2.6 | `CommissionContract.GetWeeklyCommissionPool` | `CommissionContract` | `/` | وضعیت pool هفته جاری |
|
||||||
|
| 2.7 | `ClubMembershipContract.GetAllClubMemberships` | `ClubMembershipContract` | `/` | آمار اعضای باشگاه |
|
||||||
|
| 2.8 | `UserOrderContract.GetAllUserOrderByFilter` | `UserOrderContract` | `/` | سفارشهای اخیر |
|
||||||
|
| 2.9 | `IDiscountOrderService.GetOrdersAsync` | `DiscountOrderContract` | `Dashboard/DiscountShopWidget` | ویجت سفارشهای فروشگاه تخفیفی |
|
||||||
|
| 2.10 | `CommissionContract.GetWithdrawalRequests` | `CommissionContract` | `NavMenu` | badge درخواستهای برداشت معلق |
|
||||||
|
| 2.11 | `GlobalSearch` — `UserContract.GetAllUserByFilter` | `UserContract` | `MainLayout/GlobalSearch` | جستجوی سریع کاربر + ناوبری صفحات |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۳. مدیریت کاربران
|
||||||
|
|
||||||
|
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||||
|
|---|-------------|---------------|-----------------|--------|
|
||||||
|
| 3.1 | `UserContract.GetAllUserByFilter` | `UserContract` | `/UserPage/`, `UserAutoComplete`, GlobalSearch | لیست/جستجوی کاربران |
|
||||||
|
| 3.2 | `UserContract.DeleteUser` | `UserContract` | `/UserPage/` | حذف کاربر |
|
||||||
|
| 3.3 | Export users | `UserContract.GetAllUserByFilter` | `/UserPage/` | خروجی CSV کاربران |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۴. نقشها و دسترسیها
|
||||||
|
|
||||||
|
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||||
|
|---|-------------|---------------|-----------------|--------|
|
||||||
|
| 4.1 | `RoleContract.GetAllRoleByFilter` | `RoleContract` | `/RolePage/`, `RoleAutoComplete` | لیست نقشها |
|
||||||
|
| 4.2 | `RoleContract.CreateNewRole` | `RoleContract` | `Role/Components/CreateDialog` | ایجاد نقش |
|
||||||
|
| 4.3 | `RoleContract.UpdateRole` | `RoleContract` | `Role/Components/UpdateDialog` | ویرایش نقش |
|
||||||
|
| 4.4 | `RoleContract.DeleteRole` | `RoleContract` | `/RolePage/` | حذف نقش |
|
||||||
|
| 4.5 | `RolePermissionsDialog` | — (UI فقط) | `/RolePage/` | ویرایش permissionها — **هنوز به API وصل نشده** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۵. نقش کاربر و آدرس
|
||||||
|
|
||||||
|
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||||
|
|---|-------------|---------------|-----------------|--------|
|
||||||
|
| 5.1 | `UserRoleContract.GetAllUserRoleByFilter` | `UserRoleContract` | `/UserRolePage/{UserId}` | نقشهای یک کاربر |
|
||||||
|
| 5.2 | `UserRoleContract.CreateNewUserRole` | `UserRoleContract` | CreateDialog, `UserRoleDialog` | تخصیص نقش |
|
||||||
|
| 5.3 | `UserRoleContract.UpdateUserRole` | `UserRoleContract` | UpdateDialog | ویرایش نقش کاربر |
|
||||||
|
| 5.4 | `UserRoleContract.DeleteUserRole` | `UserRoleContract` | `/UserRolePage/{UserId}` | حذف نقش کاربر |
|
||||||
|
| 5.5 | `RoleContract.GetAllRoleByFilter` | `RoleContract` | `UserRoleDialog`, `RoleTitleColumn` | نمایش عنوان نقش |
|
||||||
|
| 5.6 | `UserAddressContract.GetAllUserAddressByFilter` | `UserAddressContract` | `/UserAddressPage/{UserId}` | آدرسهای کاربر |
|
||||||
|
| 5.7 | `UserAddressContract.CreateNewUserAddress` | `UserAddressContract` | CreateDialog | ثبت آدرس |
|
||||||
|
| 5.8 | `UserAddressContract.UpdateUserAddress` | `UserAddressContract` | UpdateDialog | ویرایش آدرس |
|
||||||
|
| 5.9 | `UserAddressContract.DeleteUserAddress` | `UserAddressContract` | `/UserAddressPage/{UserId}` | حذف آدرس |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۶. پکیجها
|
||||||
|
|
||||||
|
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||||
|
|---|-------------|---------------|-----------------|--------|
|
||||||
|
| 6.1 | `PackageContract.GetAllPackageByFilter` | `PackageContract` | `/PackagePage/`, `PackageSelect`, ManualPaymentDialog | لیست پکیجها |
|
||||||
|
| 6.2 | `PackageContract.CreateNewPackage` | `PackageContract` | `Package/Components/CreateDialog` | ایجاد پکیج |
|
||||||
|
| 6.3 | `PackageContract.UpdatePackage` | `PackageContract` | `Package/Components/UpdateDialog` | ویرایش پکیج |
|
||||||
|
| 6.4 | `PackageContract.DeletePackage` | `PackageContract` | `/PackagePage/` | حذف پکیج |
|
||||||
|
| 6.5 | `ConfigurationContract.GetClubFeatures` | `ConfigurationContract` | Create/Update Dialog | انتخاب featureهای باشگاه مرتبط با پکیج |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۷. فروشگاه اصلی — محصولات و دستهبندی
|
||||||
|
|
||||||
|
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||||
|
|---|-------------|---------------|-----------------|--------|
|
||||||
|
| 7.1 | `ProductsContract.GetAllProductsByFilter` | `ProductsContract` | `/ProductsPage/`, BulkEdit, AutoComplete | لیست محصولات |
|
||||||
|
| 7.2 | `ProductsContract.CreateNewProducts` | `ProductsContract` | `Products/Components/CreateDialog` | ایجاد محصول |
|
||||||
|
| 7.3 | `ProductsContract.UpdateProducts` | `ProductsContract` | UpdateDialog | ویرایش محصول |
|
||||||
|
| 7.4 | `ProductsContract.DeleteProducts` | `ProductsContract` | `/ProductsPage/` | حذف محصول |
|
||||||
|
| 7.5 | `ProductsContract.ToggleProductStatus` | `ProductsContract` | `/ProductsPage/`, BulkEdit | فعال/غیرفعال |
|
||||||
|
| 7.6 | `ProductsContract.BulkUpdateProductPrices` | `ProductsContract` | `/ProductsBulkEditPage/` | ویرایش گروهی قیمت |
|
||||||
|
| 7.7 | `ProductsContract.GetProductGallery` / Add/Remove Image | `ProductsContract` | `GalleryDialog` | گالری تصاویر محصول |
|
||||||
|
| 7.8 | `ProductsContract.GetProducts` / `UpdateProductCategories` | `ProductsContract` | `/ProductCategoriesPage/{ProductId}` | drag-drop دستهبندی محصول |
|
||||||
|
| 7.9 | `CategoryContract.GetAllCategoryByFilter` | `CategoryContract` | `/CategoryPage/`, AutoComplete | لیست دستهها |
|
||||||
|
| 7.10 | `CategoryContract.GetCategory` / Create / Update / Delete | `CategoryContract` | CategoryMainPage, Dialogs | CRUD دستهبندی |
|
||||||
|
| 7.11 | `ProductsContract.GetProductsForCategory` / `UpdateCategoryProducts` | `ProductsContract` | `/CategoryProductsPage/{CategoryId}` | drag-drop محصولات یک دسته |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۸. انبارداری (Inventory)
|
||||||
|
|
||||||
|
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||||
|
|---|-------------|---------------|-----------------|--------|
|
||||||
|
| 8.1 | `InventoryContract.GetAllWarehouses` | `InventoryContract` | `/InventoryPage/`, Warehouses, LowStock | لیست انبارها |
|
||||||
|
| 8.2 | `InventoryContract.GetAllInventoryItems` | `InventoryContract` | `/InventoryPage/` | موجودی کالا |
|
||||||
|
| 8.3 | `InventoryContract.GetLowStockItems` | `InventoryContract` | `/InventoryPage/`, `/InventoryLowStockPage/` | اقلام کمموجود |
|
||||||
|
| 8.4 | `InventoryContract.AddStock` | `InventoryContract` | `AddStockDialog` | ورود کالا به انبار |
|
||||||
|
| 8.5 | `InventoryContract.AdjustStock` | `InventoryContract` | `AdjustStockDialog` | تعدیل موجودی |
|
||||||
|
| 8.6 | `InventoryContract.RecordLoss` | `InventoryContract` | `RecordLossDialog` | ثبت ضایعات |
|
||||||
|
| 8.7 | `InventoryContract.CreateWarehouse` | `InventoryContract` | `CreateWarehouseDialog` | ایجاد انبار |
|
||||||
|
| 8.8 | `InventoryContract.UpdateInventorySettings` | `InventoryContract` | `InventorySettingsDialog` | تنظیمات انبار |
|
||||||
|
| 8.9 | `InventoryContract.GetStockMovements` | `InventoryContract` | `/InventoryMovementsPage/` | تاریخچه جابجایی موجودی |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۹. سفارشهای فروشگاه اصلی
|
||||||
|
|
||||||
|
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||||
|
|---|-------------|---------------|-----------------|--------|
|
||||||
|
| 9.1 | `UserOrderContract.GetAllUserOrderByFilter` | `UserOrderContract` | `/OrdersPage/`, `/UserOrderPage/{UserId}`, SalesReports | لیست سفارشها |
|
||||||
|
| 9.2 | `UserOrderContract.GetUserOrder` | `UserOrderContract` | OrderDetailsDialog, Index | جزئیات سفارش |
|
||||||
|
| 9.3 | `UserOrderContract.UpdateUserOrder` | `UserOrderContract` | UserOrderDetailsDialog, ChangeOrderStatus | ویرایش سفارش / tracking |
|
||||||
|
| 9.4 | `UserOrderContract.UpdateOrderStatus` | `UserOrderContract` | ChangeOrderStatusDialog | تغییر وضعیت تحویل |
|
||||||
|
| 9.5 | `UserOrderContract.CancelOrder` | `UserOrderContract` | CancelOrderDialog | لغو سفارش |
|
||||||
|
| 9.6 | `UserOrderContract.DeleteUserOrder` | `UserOrderContract` | `/UserOrderPage/{UserId}` | حذف سفارش |
|
||||||
|
| 9.7 | `UserOrderContract.ApplyDiscountToOrder` | `UserOrderContract` | ApplyDiscountDialog | اعمال تخفیف روی سفارش |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۱۰. پرداخت دستی و تراکنشها
|
||||||
|
|
||||||
|
> **دو گزارش تراکنش — اشتباه نگیرید:**
|
||||||
|
>
|
||||||
|
> | مسیر | جدول DB | کاربرد |
|
||||||
|
> |------|---------|--------|
|
||||||
|
> | `/payment/transactions` | `PaymentTransactions` | ممیزی درگاه (Authority، ZarinPal، کارت) |
|
||||||
|
> | `/payment/ledger-transactions` | `Transactions` | دفتر مالی داخلی (نوع تراکنش، وضعیت، RefId) |
|
||||||
|
|
||||||
|
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||||
|
|---|-------------|---------------|-----------------|--------|
|
||||||
|
| 10.1 | `ManualPaymentContract.GetAllManualPayments` | `ManualPaymentContract` | `/payment/manual-payments` | لیست پرداختهای دستی |
|
||||||
|
| 10.2 | `ManualPaymentContract.CreateManualPayment` | `ManualPaymentContract` | `ManualPaymentDialog` | ثبت پرداخت دستی (مثلاً Daya) |
|
||||||
|
| 10.3 | `ManualPaymentContract.ApproveManualPayment` | `ManualPaymentContract` | ManualPayments, Dialog | تأیید پرداخت |
|
||||||
|
| 10.4 | `ManualPaymentContract.RejectManualPayment` | `ManualPaymentContract` | ManualPayments, Dialog | رد پرداخت |
|
||||||
|
| 10.5 | `PaymentTransactionContract.GetAllPaymentTransactionByFilter` | `PaymentTransactionContract` | `/payment/transactions` | گزارش read-only تراکنش **درگاه** (`PaymentTransactions`) |
|
||||||
|
| 10.5b | `PaymentTransactionContract.GetPaymentTransactionSummary` | `PaymentTransactionContract` | `/payment/transactions` | کارت خلاصه گزارش درگاه |
|
||||||
|
| 10.6 | `TransactionsContract.GetAllTransactionsByFilter` | `TransactionsContract` | `/payment/ledger-transactions` | گزارش read-only **دفتر مالی** (`Transactions`) |
|
||||||
|
| 10.7 | `TransactionsContract.GetTransactionSummary` | `TransactionsContract` | `/payment/ledger-transactions` | کارت خلاصه دفتر تراکنش |
|
||||||
|
| 10.8 | `UserPackagePurchaseContract.GetAllUserPackagePurchaseByFilter` | `UserPackagePurchaseContract` | `/club/purchases` | گزارش خرید پکیج |
|
||||||
|
| 10.9 | `ClubMembershipCycleContract.GetAllClubMembershipCycleByFilter` | `ClubMembershipCycleContract` | `/club/cycles` | گزارش دوره عضویت |
|
||||||
|
| 10.10 | `ClubMembershipCycleHistoryContract.GetAllClubMembershipCycleHistoryByFilter` | `ClubMembershipCycleHistoryContract` | `/club/history` (زیرتب دوره) | تاریخچه تغییرات دوره |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۱۱. کیف پول و تاریخچه
|
||||||
|
|
||||||
|
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||||
|
|---|-------------|---------------|-----------------|--------|
|
||||||
|
| 11.1 | `UserWalletContract.GetAllUserWalletByFilter` | `UserWalletContract` | `/wallets` | لیست کیف پول کاربران |
|
||||||
|
| 11.2 | `UserWalletHistoryContract.GetAllUserWalletHistoryByFilter` | `UserWalletHistoryContract` | `/wallets` (تب تاریخچه) | لاگ تغییرات کیف پول |
|
||||||
|
| 11.3 | `UserWalletContract.AdminManualCreditCharge` | `UserWalletContract` | `/payment/manual-credit-charges` | شارژ دستی کیف پول اصلی (Balance) |
|
||||||
|
| 11.4 | `UserWalletContract.GetManualCreditCharges` | `UserWalletContract` | `/payment/manual-credit-charges` | لیست شارژهای دستی (Type=17) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۱۲. قراردادهای کاربران
|
||||||
|
|
||||||
|
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||||
|
|---|-------------|---------------|-----------------|--------|
|
||||||
|
| 12.1 | `UserContractContract.GetAllUserContractByFilter` | `UserContractContract` | `/contracts` | لیست قراردادهای امضاشده (اصلی / باشگاه) |
|
||||||
|
| 12.2 | Download PDF | — (local JS) | `/contracts` | دانلود PDF قرارداد ذخیرهشده |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۱۳. کمیسیون و پاداش
|
||||||
|
|
||||||
|
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||||
|
|---|-------------|---------------|-----------------|--------|
|
||||||
|
| 13.1 | `CommissionContract.GetAvailableWeeks` | `CommissionContract` | `/commission/dashboard`, WeekNumberPicker | انتخاب هفته |
|
||||||
|
| 13.2 | `CommissionContract.GetWeeklyCommissionPool` | `CommissionContract` | `/commission/dashboard` | جزئیات pool هفتگی |
|
||||||
|
| 13.3 | `CommissionContract.TriggerWeeklyCalculation` | `CommissionContract` | Dashboard, WorkerControl | اجرای دستی محاسبه هفتگی |
|
||||||
|
| 13.4 | `CommissionContract.GetAllWeeklyPools` | `CommissionContract` | `/commission/reports` | گزارش poolهای هفتگی |
|
||||||
|
| 13.5 | `CommissionContract.GetUserCommissionPayouts` | `CommissionContract` | `/commission/payouts` | payoutهای کاربران |
|
||||||
|
| 13.6 | `CommissionContract.ProcessWithdrawal` | `CommissionContract` | UserPayouts, WithdrawalRequests | پردازش/واریز برداشت |
|
||||||
|
| 13.7 | `CommissionContract.BulkCreditPayouts` | `CommissionContract` | `/commission/payouts` | واریز گروهی payoutها |
|
||||||
|
| 13.8 | `CommissionContract.GetWithdrawalRequests` | `CommissionContract` | `/commission/withdrawals`, NavMenu | درخواستهای برداشت کاربران |
|
||||||
|
| 13.9 | `CommissionContract.ApproveWithdrawal` | `CommissionContract` | WithdrawalRequests | تأیید درخواست برداشت |
|
||||||
|
| 13.10 | `CommissionContract.RejectWithdrawal` | `CommissionContract` | WithdrawalRequests | رد درخواست برداشت |
|
||||||
|
| 13.11 | `CommissionContract.GetWithdrawalReports` | `CommissionContract` | `/commission/withdrawal-reports` | گزارش برداشتها |
|
||||||
|
| 13.12 | `CommissionContract.GetUserWeeklyBalances` | `CommissionContract` | `/network/balances` | گزارش بالانس هفتگی کاربران |
|
||||||
|
| 13.13 | `CommissionContract.GetWorkerStatus` | `CommissionContract` | `/system/worker-control` | وضعیت worker محاسبه |
|
||||||
|
| 13.14 | `CommissionContract.GetWorkerExecutionLogs` | `CommissionContract` | `/system/worker-control` | لاگ اجرای worker |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۱۴. شبکه فروش (MLM)
|
||||||
|
|
||||||
|
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||||
|
|---|-------------|---------------|-----------------|--------|
|
||||||
|
| 14.1 | `NetworkMembershipContract.GetNetworkTree` | `NetworkMembershipContract` | `/network/tree` | درخت شبکه (admin view) |
|
||||||
|
| 14.2 | `NetworkMembershipContract.GetUserNetwork` | `NetworkMembershipContract` | `/network/user-info/{UserId}` | اطلاعات شبکه یک کاربر |
|
||||||
|
| 14.3 | `NetworkMembershipContract.GetNetworkMembershipHistory` | `NetworkMembershipContract` | UserNetworkInfo | تاریخچه عضویت شبکه |
|
||||||
|
| 14.4 | `NetworkMembershipContract.ChangeNetworkParent` | `NetworkMembershipContract` | `ChangeParentDialog` | تغییر والد در درخت |
|
||||||
|
| 14.5 | `NetworkMembershipContract.GetNetworkStatistics` | `NetworkMembershipContract` | `/network/statistics` | آمار کلی شبکه (admin) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۱۵. باشگاه مشتریان
|
||||||
|
|
||||||
|
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||||
|
|---|-------------|---------------|-----------------|--------|
|
||||||
|
| 15.1 | `ClubMembershipContract.GetAllClubMemberships` | `ClubMembershipContract` | `/club/members`, Index | لیست اعضا |
|
||||||
|
| 15.2 | `ClubMembershipContract.ActivateClubMembership` | `ClubMembershipContract` | `ActivateClubDialog` | فعالسازی دستی عضویت |
|
||||||
|
| 15.3 | `ClubMembershipContract.DeactivateClubMembership` | `ClubMembershipContract` | `DeactivateClubDialog` | غیرفعالسازی |
|
||||||
|
| 15.4 | `ClubMembershipContract.GetClubMembershipHistory` | `ClubMembershipContract` | `/club/history` (زیرتب عضویت) | گزارش تاریخچه عضویت |
|
||||||
|
| 15.5 | `ClubMembershipContract.GetClubStatistics` | `ClubMembershipContract` | `/club/statistics` | آمار باشگاه |
|
||||||
|
| 15.6 | `ConfigurationContract.GetClubFeatures` | `ConfigurationContract` | `/club/features` | لیست featureهای باشگاه |
|
||||||
|
| 15.7 | `ClubMembershipContract.GetUserClubFeatures` | `ClubMembershipContract` | `/club/features` | featureهای یک کاربر |
|
||||||
|
| 15.8 | `ClubMembershipContract.ToggleUserClubFeature` | `ClubMembershipContract` | `/club/features` | فعال/غیرفعال feature برای کاربر |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۱۶. فروشگاه تخفیفی (Discount Shop)
|
||||||
|
|
||||||
|
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||||
|
|---|-------------|---------------|-----------------|--------|
|
||||||
|
| 16.1 | `IDiscountProductService` — CRUD | `DiscountProductContract` | `/discount-products` | مدیریت محصولات تخفیفی |
|
||||||
|
| 16.2 | `IDiscountCategoryService` — CRUD | `DiscountCategoryContract` | `/discount-categories` | مدیریت دستهبندی تخفیفی |
|
||||||
|
| 16.3 | `IDiscountOrderService.GetOrdersAsync` | `DiscountOrderContract.GetAllDiscountOrders` | `/discount-orders`, Dashboard widget | لیست سفارشها (admin) |
|
||||||
|
| 16.4 | `IDiscountOrderService.GetByIdAsync` | `DiscountOrderContract` | OrderDetailsDialog | جزئیات سفارش |
|
||||||
|
| 16.5 | `IDiscountOrderService.UpdateStatusAsync` | `DiscountOrderContract` | Orders page, OrderDetailsDialog | تغییر وضعیت پرداخت/ارسال |
|
||||||
|
| 16.6 | `IDiscountOrderService.GetSalesReportAsync` | `DiscountOrderContract` | `/discount-sales-reports` | گزارش فروش |
|
||||||
|
| 16.7 | `DiscountProductContract.GetDiscountProducts` | `DiscountProductContract` | `DiscountProductsAutoComplete` | autocomplete محصول |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۱۷. تگ محصولات
|
||||||
|
|
||||||
|
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||||
|
|---|-------------|---------------|-----------------|--------|
|
||||||
|
| 17.1 | `ITagService.GetTagsAsync` | `TagContract` | `/tags` | لیست تگها |
|
||||||
|
| 17.2 | `ITagService.CreateAsync` / `UpdateAsync` / `DeleteAsync` | `TagContract` | TagEditDialog, TagManagement | CRUD تگ |
|
||||||
|
| 17.3 | `ITagService.GetProductTagsAsync` | `ProductTagContract` | AssignTagsDialog | تگهای یک محصول |
|
||||||
|
| 17.4 | `ITagService.AssignToProductAsync` / `RemoveProductTagAsync` | `ProductTagContract` | AssignTagsDialog, Discount products | تخصیص/حذف تگ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۱۸. پیامهای عمومی
|
||||||
|
|
||||||
|
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||||
|
|---|-------------|---------------|-----------------|--------|
|
||||||
|
| 18.1 | `IPublicMessageService.GetMessagesAsync` | `PublicMessageContract` | `/public-messages` | لیست پیامها |
|
||||||
|
| 18.2 | `IPublicMessageService.GetByIdAsync` | `PublicMessageContract` | PublicMessages | جزئیات |
|
||||||
|
| 18.3 | `IPublicMessageService.CreateAsync` / `UpdateAsync` | `PublicMessageContract` | MessageFormDialog | ایجاد/ویرایش |
|
||||||
|
| 18.4 | `IPublicMessageService.PublishAsync` / `ArchiveAsync` | `PublicMessageContract` | PublicMessages | انتشار/بایگانی |
|
||||||
|
| 18.5 | `IPublicMessageService.DeleteAsync` | `PublicMessageContract` | PublicMessages | حذف |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۱۹. بلاگ
|
||||||
|
|
||||||
|
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||||
|
|---|-------------|---------------|-----------------|--------|
|
||||||
|
| 19.1 | `IBlogPostService.GetAllAsync` | `BlogPostContract` | `/blog/posts` | لیست پستها |
|
||||||
|
| 19.2 | `IBlogPostService.GetByIdAsync` | `BlogPostContract` | BlogPostEditDialog | جزئیات پست |
|
||||||
|
| 19.3 | `IBlogPostService.CreateAsync` / `UpdateAsync` | `BlogPostContract` | BlogPostEditDialog | ایجاد/ویرایش |
|
||||||
|
| 19.4 | `IBlogPostService.DeleteAsync` | `BlogPostContract` | BlogPostManagement | حذف |
|
||||||
|
| 19.5 | `IBlogPostService.PublishAsync` / `ArchiveAsync` | `BlogPostContract` | BlogPostManagement | انتشار/بایگانی |
|
||||||
|
| 19.6 | `IBlogCategoryService` — CRUD | `BlogCategoryContract` | `/blog/categories` | مدیریت دستهبندی بلاگ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۲۰. مدیریت محتوای سایت
|
||||||
|
|
||||||
|
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||||
|
|---|-------------|---------------|-----------------|--------|
|
||||||
|
| 20.1 | `ISitePageSettingsService.GetAllAsync` | `SitePageSettingsContract` | `/content/page-settings` | لیست صفحات (landing, about, contact, licenses) |
|
||||||
|
| 20.2 | `ISitePageSettingsService.GetByKeyAsync` | `SitePageSettingsContract` | PageSettings dialogs | جزئیات یک صفحه |
|
||||||
|
| 20.3 | `ISitePageSettingsService.SaveSettingsAsync` | `SitePageSettingsContract` | PageSettingsEditDialog | ذخیره hero + JSON تنظیمات |
|
||||||
|
| 20.4 | `ISitePageSettingsService.SaveImageAsync` / `DeleteImageAsync` | `SitePageSettingsContract` | PageImagesDialog | مدیریت تصاویر گروهبندیشده |
|
||||||
|
| 20.5 | `ISitePageService` — CRUD sections | `SitePageContract` | `/content/pages` | صفحات داینامیک قدیمی (section-based) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۲۱. تنظیمات سیستم و سلامت
|
||||||
|
|
||||||
|
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||||
|
|---|-------------|---------------|-----------------|--------|
|
||||||
|
| 21.1 | `ConfigurationContract.GetAllConfigurations` | `ConfigurationContract` | `/system/configuration` | خواندن key-value تنظیمات (کمیسیون، شبکه، باشگاه، VAT…) |
|
||||||
|
| 21.2 | `ConfigurationContract.CreateOrUpdateConfiguration` | `ConfigurationContract` | `/system/configuration` | ذخیره تنظیمات |
|
||||||
|
| 21.3 | `HealthContract.GetSystemHealth` | `HealthContract` | `/system/health` | وضعیت سلامت سرویسها |
|
||||||
|
| 21.4 | `AlertsMonitoring` | — (mock) | `/system/alerts` | **TODO — API هشدار هنوز وصل نشده** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۲۲. نسخه اپلیکیشن
|
||||||
|
|
||||||
|
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|
||||||
|
|---|-------------|---------------|-----------------|--------|
|
||||||
|
| 22.1 | `IAppVersionService.GetAllAsync` | `AppVersionContract` | `/settings/app-versions` | لیست نسخه FrontOffice/App |
|
||||||
|
| 22.2 | `IAppVersionService.GetByNameAsync` | `AppVersionContract` | AppVersions | جزئیات یک app |
|
||||||
|
| 22.3 | `IAppVersionService.UpdateAsync` | `AppVersionContract.UpdateAppVersion` | AppVersions dialog | بهروزرسانی نسخه + force update |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۲۳. کامپوننتهای مشترک (AutoComplete / Hub)
|
||||||
|
|
||||||
|
| کامپوننت | Contract | کاربرد |
|
||||||
|
|----------|----------|--------|
|
||||||
|
| `UserAutoComplete` | `UserContract` | جستجوی کاربر در فرمها |
|
||||||
|
| `ProductsAutoComplete` | `ProductsContract` | جستجوی محصول |
|
||||||
|
| `CategoryAutoComplete` / MultiSelect | `CategoryContract` | انتخاب دسته |
|
||||||
|
| `PackageSelect` | `PackageContract` | انتخاب پکیج |
|
||||||
|
| `WeekNumberPicker` | `CommissionContract` | انتخاب هفته |
|
||||||
|
| `DiscountProductsAutoComplete` | `DiscountProductContract` | جستجوی محصول تخفیفی |
|
||||||
|
| Hub pages (`ClubHub`, `DiscountShopHub`, `SystemHub`, `OrdersHub`, `BlogHub`) | — | مسیریابی tab-based؛ RPC در child component |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۲۴. سرویسهای ثبتشده ولی بدون استفاده / ناقص
|
||||||
|
|
||||||
|
| سرویس / صفحه | وضعیت |
|
||||||
|
|--------------|--------|
|
||||||
|
| `IBlogPostImageService` (`BlogPostImageContract`) | ثبت در DI؛ **هیچ صفحهای inject نمیکند** |
|
||||||
|
| `RolePermissionsDialog` | UI permission؛ **ذخیره در backend TODO** |
|
||||||
|
| `AuthorizationService` | فقط JWT role؛ **Permission API از CMS/BFF TODO** |
|
||||||
|
| `/system/alerts` | داده mock؛ **AlertContract وجود ندارد** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## نمودار جریان ورود ادمین
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
A[LoginPage] -->|CreateNewOtpToken| B[SMS OTP]
|
||||||
|
B --> C[VerifyCodePage]
|
||||||
|
C -->|VerifyOtpToken| D[JWT in localStorage]
|
||||||
|
D --> E[All gRPC via TokenHandler]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## فهرست gRPC Contractهای فعال BackOffice
|
||||||
|
|
||||||
|
| Contract | Wrapper / استفاده مستقیم |
|
||||||
|
|----------|---------------------------|
|
||||||
|
| `OtpTokenContract` | Login |
|
||||||
|
| `UserContract` | Login, Users, Dashboard, Search |
|
||||||
|
| `RoleContract` | Roles, UserRoles |
|
||||||
|
| `UserRoleContract` | UserRole pages |
|
||||||
|
| `UserAddressContract` | UserAddress pages |
|
||||||
|
| `PackageContract` | Packages, ManualPayment |
|
||||||
|
| `ProductsContract` | Products, Categories drag-drop |
|
||||||
|
| `CategoryContract` | Categories |
|
||||||
|
| `UserOrderContract` | Orders |
|
||||||
|
| `ManualPaymentContract` | Manual payments |
|
||||||
|
| `PaymentTransactionContract` | Gateway transaction report (`/payment/transactions`) |
|
||||||
|
| `TransactionsContract` | Ledger transaction report (`/payment/ledger-transactions`) |
|
||||||
|
| `UserPackagePurchaseContract` | Club package purchase report |
|
||||||
|
| `ClubMembershipCycleContract` | Club cycle report |
|
||||||
|
| `ClubMembershipCycleHistoryContract` | Club cycle history report |
|
||||||
|
| `UserWalletContract` | Wallet management |
|
||||||
|
| `UserWalletHistoryContract` | Wallet change log |
|
||||||
|
| `UserContractContract` | Signed contracts |
|
||||||
|
| `CommissionContract` | Commission, Network balances, Worker |
|
||||||
|
| `NetworkMembershipContract` | Network tree/info |
|
||||||
|
| `ClubMembershipContract` | Club members/features |
|
||||||
|
| `ConfigurationContract` | System config, Club features, Package |
|
||||||
|
| `DiscountProductContract` | DiscountProductService |
|
||||||
|
| `DiscountCategoryContract` | DiscountCategoryService |
|
||||||
|
| `DiscountOrderContract` | DiscountOrderService |
|
||||||
|
| `TagContract` / `ProductTagContract` | TagService |
|
||||||
|
| `PublicMessageContract` | PublicMessageService |
|
||||||
|
| `InventoryContract` | Inventory pages |
|
||||||
|
| `BlogPostContract` | BlogPostService |
|
||||||
|
| `BlogCategoryContract` | BlogCategoryService |
|
||||||
|
| `BlogPostImageContract` | BlogPostImageService *(unused)* |
|
||||||
|
| `SitePageSettingsContract` | SitePageSettingsService |
|
||||||
|
| `SitePageContract` | SitePageService |
|
||||||
|
| `AppVersionContract` | AppVersionService |
|
||||||
|
| `HealthContract` | Health dashboard |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## مقایسه با FrontOffice
|
||||||
|
|
||||||
|
| موضوع | FrontOffice (مشتری) | BackOffice (ادمین) |
|
||||||
|
|-------|----------------------|-------------------|
|
||||||
|
| Auth | OTP + JWT (UserContract) | OTP + JWT (همان flow) |
|
||||||
|
| Orders | Customer RPC | Admin filter + status management |
|
||||||
|
| Discount Shop | User orders + checkout | Full CRUD + admin order management |
|
||||||
|
| Commission | Read-only (my payouts) | Full admin (calculation, approval, reports) |
|
||||||
|
| Network | My tree only | Full tree + change parent |
|
||||||
|
| Content | Read page settings | Edit page settings + blog + public messages |
|
||||||
|
| Wallet | My balances | All users wallet + history log |
|
||||||
|
|
||||||
|
مرجع مشابه برای فرانت مشتری: [`FrontOffice/docs/CUSTOMER-SERVICES.md`](../FrontOffice/docs/CUSTOMER-SERVICES.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## یادداشت deploy (خرداد ۱۴۰۵)
|
||||||
|
|
||||||
|
| گام | جزئیات |
|
||||||
|
|-----|--------|
|
||||||
|
| Proto | `Foursat.CMSMicroservice.Protobuf` **0.0.203** (گزارش دفتر تراکنش) |
|
||||||
|
| CMS | deploy WebApi (شامل `GetTransactionSummary` + فیلتر تاریخ) |
|
||||||
|
| BackOffice | NuGet 0.0.203 یا ProjectReference در monorepo |
|
||||||
|
| FrontOffice | **redeploy لازم نیست** برای گزارش ledger؛ برای شارژ کیف اصلی proto **0.0.202** لازم است |
|
||||||
|
|
||||||
|
گزارشهای read-only (بخش ۱۰ و ۱۵): فقط فیلتر + grid + summary + CSV — بدون CRUD یا detail dialog.
|
||||||
|
|
||||||
|
مستندات فنی کامل: [`totalDoc/technical/TECH-08-SESSION-LOG-202606.md`](../../totalDoc/technical/TECH-08-SESSION-LOG-202606.md) — §10 Ledger transactions
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Docs moved to totalDoc
|
||||||
|
|
||||||
|
See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation.
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Docs moved to totalDoc
|
||||||
|
|
||||||
|
See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Docs moved to /totalDoc — see totalDoc/INDEX.md
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
# BackOffice UI Deployment (Blazor WASM + nginx)
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: backoffice
|
||||||
|
namespace: default
|
||||||
|
labels:
|
||||||
|
app: backoffice
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: backoffice
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: backoffice
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: backoffice
|
||||||
|
image: 194.5.195.53:30080/admin/backoffice:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
name: http
|
||||||
|
env:
|
||||||
|
- name: BLAZOR_ENVIRONMENT
|
||||||
|
value: "Staging"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "128Mi"
|
||||||
|
cpu: "100m"
|
||||||
|
limits:
|
||||||
|
memory: "512Mi"
|
||||||
|
cpu: "500m"
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: gitea-registry-secret
|
||||||
|
|
||||||
|
---
|
||||||
|
# BackOffice UI Service
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: backoffice-svc
|
||||||
|
namespace: default
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: backoffice
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 80
|
||||||
|
type: ClusterIP
|
||||||
|
|
||||||
|
---
|
||||||
|
# BackOffice UI Ingress
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: backoffice-ingress
|
||||||
|
namespace: default
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||||
|
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||||
|
nginx.ingress.kubernetes.io/service-upstream: "true"
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- backoffice.se.kbs1.ir
|
||||||
|
secretName: backoffice-tls
|
||||||
|
rules:
|
||||||
|
- host: backoffice.se.kbs1.ir
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: backoffice-svc
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,25 @@
|
|||||||
|
**/.dockerignore
|
||||||
|
**/.env
|
||||||
|
**/.git
|
||||||
|
**/.gitignore
|
||||||
|
**/.project
|
||||||
|
**/.settings
|
||||||
|
**/.toolstarget
|
||||||
|
**/.vs
|
||||||
|
**/.vscode
|
||||||
|
**/.idea
|
||||||
|
**/*.*proj.user
|
||||||
|
**/*.dbmdl
|
||||||
|
**/*.jfm
|
||||||
|
**/azds.yaml
|
||||||
|
**/bin
|
||||||
|
**/charts
|
||||||
|
**/docker-compose*
|
||||||
|
**/Dockerfile*
|
||||||
|
**/node_modules
|
||||||
|
**/npm-debug.log
|
||||||
|
**/obj
|
||||||
|
**/secrets.dev.yaml
|
||||||
|
**/values.dev.yaml
|
||||||
|
LICENSE
|
||||||
|
README.md
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<NotFound>
|
<NotFound>
|
||||||
<PageTitle>یافت نشد</PageTitle>
|
<PageTitle>یافت نشد</PageTitle>
|
||||||
<LayoutView Layout="@typeof(MainLayout)">
|
<LayoutView Layout="@typeof(MainLayout)">
|
||||||
<p role="alert">آدرس مورد نظر یافت نشد</p>
|
<p role="alert">آدرس مورد نظر یافت نشد</p>
|
||||||
</LayoutView>
|
</LayoutView>
|
||||||
</NotFound>
|
</NotFound>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@@ -1,70 +1,262 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
|
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
|
||||||
<BlazorCacheBootResources>false</BlazorCacheBootResources>
|
<BlazorCacheBootResources>false</BlazorCacheBootResources>
|
||||||
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
|
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
</PropertyGroup>
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
|
<!-- Suppress pre-existing warnings that do not affect runtime correctness -->
|
||||||
|
<NoWarn>MUD0002;CA2022;BL0005</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Remove="Common\NewFolder\**" />
|
<Compile Remove="Common\NewFolder\**"/>
|
||||||
<Content Remove="Common\NewFolder\**" />
|
<Content Remove="Common\NewFolder\**"/>
|
||||||
<EmbeddedResource Remove="Common\NewFolder\**" />
|
<Content Include="..\.dockerignore">
|
||||||
<None Remove="Common\NewFolder\**" />
|
<Link>.dockerignore</Link>
|
||||||
</ItemGroup>
|
</Content>
|
||||||
|
<EmbeddedResource Remove="Common\NewFolder\**"/>
|
||||||
|
<None Remove="Common\NewFolder\**"/>
|
||||||
|
|
||||||
<ItemGroup>
|
<!-- =========================================================== -->
|
||||||
|
<!-- TEMPORARILY EXCLUDED - Missing Proto Projects -->
|
||||||
|
<!-- TODO: Create these proto projects in BackOffice.BFF -->
|
||||||
|
<!-- =========================================================== -->
|
||||||
|
|
||||||
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
|
<!-- DiscountShop Pages & Services - FULLY ENABLED -->
|
||||||
|
<!-- <Compile Remove="Pages\DiscountShop\**" /> -->
|
||||||
|
<!-- <Content Remove="Pages\DiscountShop\**" /> -->
|
||||||
|
<!-- <Compile Remove="Services\DiscountProduct\**" /> -->
|
||||||
|
<!-- <Compile Remove="Services\DiscountCategory\**" /> -->
|
||||||
|
<!-- <Compile Remove="Services\DiscountOrder\**" /> -->
|
||||||
|
|
||||||
<PackageReference Include="Foursat.BackOffice.BFF.Category.Protobuf" Version="0.0.3" />
|
<!-- Tag Pages & Services - ENABLED -->
|
||||||
|
<!-- <Compile Remove="Pages\\Tag\\**" /> -->
|
||||||
|
<!-- <Content Remove="Pages\\Tag\\**" /> -->
|
||||||
|
<!-- <Compile Remove="Services\\Tag\\**" /> -->
|
||||||
|
|
||||||
<PackageReference Include="Foursat.BackOffice.BFF.Otp.Protobuf" Version="0.0.111" />
|
<!-- PublicMessages Pages & Services - ENABLED -->
|
||||||
|
<!-- <Compile Remove="Pages\PublicMessages\**" /> -->
|
||||||
|
<!-- <Content Remove="Pages\PublicMessages\**" /> -->
|
||||||
|
<!-- <Compile Remove="Services\PublicMessage\**" /> -->
|
||||||
|
|
||||||
<PackageReference Include="FourSat.BackOffice.BFF.Package.Protobuf" Version="0.0.111" />
|
<!-- ManualPayment - ENABLED -->
|
||||||
|
<!-- <Compile Remove="Pages\Payment\ManualPayments.razor" /> -->
|
||||||
|
<!-- <Content Remove="Pages\Payment\ManualPayments.razor" /> -->
|
||||||
|
<!-- <Compile Remove="Pages\Payment\ManualPayments.razor.cs" /> -->
|
||||||
|
<!-- <Compile Remove="Pages\Payment\Components\ManualPaymentDialog.razor" /> -->
|
||||||
|
<!-- <Content Remove="Pages\Payment\Components\ManualPaymentDialog.razor" /> -->
|
||||||
|
<!-- <Compile Remove="Pages\Payment\Components\ManualPaymentDialog.razor.cs" /> -->
|
||||||
|
|
||||||
<PackageReference Include="Foursat.BackOffice.BFF.Products.Protobuf" Version="0.0.8" />
|
<!-- Dashboard widgets - ENABLED -->
|
||||||
<PackageReference Include="Foursat.BackOffice.BFF.Role.Protobuf" Version="0.0.111" />
|
<!-- <Compile Remove="Pages\Dashboard\DiscountShopWidget.razor" /> -->
|
||||||
|
<!-- <Content Remove="Pages\Dashboard\DiscountShopWidget.razor" /> -->
|
||||||
|
<!-- <Compile Remove="Pages\Dashboard\DiscountShopWidget.razor.cs" /> -->
|
||||||
|
|
||||||
<PackageReference Include="Foursat.BackOffice.BFF.User.Protobuf" Version="0.0.111" />
|
<!-- BulkEdit - ENABLED -->
|
||||||
|
<!-- <Compile Remove="Pages\Products\BulkEdit.razor" /> -->
|
||||||
|
<!-- <Content Remove="Pages\Products\BulkEdit.razor" /> -->
|
||||||
|
<!-- <Compile Remove="Pages\Products\BulkEdit.razor.cs" /> -->
|
||||||
|
|
||||||
<PackageReference Include="Foursat.BackOffice.BFF.UserAddress.Protobuf" Version="0.0.111" />
|
<!-- GalleryDialog - ENABLED (proto added) -->
|
||||||
|
<!-- <Compile Remove="Pages\Products\Components\GalleryDialog.razor" /> -->
|
||||||
|
<!-- <Content Remove="Pages\Products\Components\GalleryDialog.razor" /> -->
|
||||||
|
<!-- <Compile Remove="Pages\Products\Components\GalleryDialog.razor.cs" /> -->
|
||||||
|
|
||||||
<PackageReference Include="Foursat.BackOffice.BFF.UserOrder.Protobuf" Version="0.0.114" />
|
<!-- CreateDialog/UpdateDialog - ENABLED (proto added) -->
|
||||||
|
<!-- <Compile Remove="Pages\Products\Components\CreateDialog.razor" /> -->
|
||||||
|
<!-- <Content Remove="Pages\Products\Components\CreateDialog.razor" /> -->
|
||||||
|
<!-- <Compile Remove="Pages\Products\Components\CreateDialog.razor.cs" /> -->
|
||||||
|
<!-- <Compile Remove="Pages\Products\Components\UpdateDialog.razor" /> -->
|
||||||
|
<!-- <Content Remove="Pages\Products\Components\UpdateDialog.razor" /> -->
|
||||||
|
<!-- <Compile Remove="Pages\Products\Components\UpdateDialog.razor.cs" /> -->
|
||||||
|
|
||||||
<PackageReference Include="Foursat.BackOffice.BFF.UserRole.Protobuf" Version="0.0.111" />
|
<!-- Category/Product DragDrop pages - ENABLED -->
|
||||||
<!--<PackageReference Include="Foursat.BackOffice.BFF.Commission.Protobuf" Version="0.0.2" />-->
|
<!-- <Compile Remove="Pages\Category\CategoryProductsDragDropPage.razor" /> -->
|
||||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Commission.Protobuf/BackOffice.BFF.Commission.Protobuf.csproj" />
|
<!-- <Content Remove="Pages\Category\CategoryProductsDragDropPage.razor" /> -->
|
||||||
<!--<PackageReference Include="Foursat.BackOffice.BFF.ClubMembership.Protobuf" Version="0.0.1" />-->
|
<!-- <Compile Remove="Pages\Category\CategoryProductsDragDropPage.razor.cs" /> -->
|
||||||
<!--<PackageReference Include="Foursat.BackOffice.BFF.NetworkMembership.Protobuf" Version="0.0.1" />-->
|
<!-- <Compile Remove="Pages\Products\ProductCategoriesDragDropPage.razor" /> -->
|
||||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.ClubMembership.Protobuf/BackOffice.BFF.ClubMembership.Protobuf.csproj" />
|
<!-- <Content Remove="Pages\Products\ProductCategoriesDragDropPage.razor" /> -->
|
||||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.NetworkMembership.Protobuf/BackOffice.BFF.NetworkMembership.Protobuf.csproj" />
|
<!-- <Compile Remove="Pages\Products\ProductCategoriesDragDropPage.razor.cs" /> -->
|
||||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Configuration.Protobuf/BackOffice.BFF.Configuration.Protobuf.csproj" />
|
|
||||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Health.Protobuf/BackOffice.BFF.Health.Protobuf.csproj" />
|
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.1" />
|
|
||||||
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
|
|
||||||
<PackageReference Include="Grpc.Core" Version="2.46.6" />
|
|
||||||
<PackageReference Include="Grpc.Net.Client" Version="2.71.0" />
|
|
||||||
<PackageReference Include="Grpc.Net.Client.Web" Version="2.71.0" />
|
|
||||||
<PackageReference Include="Mapster" Version="7.4.0" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.11" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.11" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.11" PrivateAssets="all" />
|
|
||||||
|
|
||||||
<PackageReference Include="MudBlazor" Version="8.14.0" />
|
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.8.0" />
|
|
||||||
<PackageReference Include="Tizzani.MudBlazor.HtmlEditor" Version="2.3.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<!-- UserOrder Components - RESTORED - proto methods exist -->
|
||||||
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
|
<!-- <Compile Remove="Pages\UserOrder\Components\CancelOrderDialog.razor" /> -->
|
||||||
</ItemGroup>
|
<!-- <Content Remove="Pages\UserOrder\Components\CancelOrderDialog.razor" /> -->
|
||||||
|
<!-- <Compile Remove="Pages\UserOrder\Components\CancelOrderDialog.razor.cs" /> -->
|
||||||
|
<!-- <Compile Remove="Pages\UserOrder\Components\ApplyDiscountDialog.razor" /> -->
|
||||||
|
<!-- <Content Remove="Pages\UserOrder\Components\ApplyDiscountDialog.razor" /> -->
|
||||||
|
<!-- <Compile Remove="Pages\UserOrder\Components\ApplyDiscountDialog.razor.cs" /> -->
|
||||||
|
<!-- <Compile Remove="Pages\UserOrder\Components\ChangeOrderStatusDialog.razor" /> -->
|
||||||
|
<!-- <Content Remove="Pages\UserOrder\Components\ChangeOrderStatusDialog.razor" /> -->
|
||||||
|
<!-- <Compile Remove="Pages\UserOrder\Components\ChangeOrderStatusDialog.razor.cs" /> -->
|
||||||
|
|
||||||
|
<!-- UserOrderDetailsDialog - RESTORED - VAT fields exist in proto -->
|
||||||
|
<!-- <Compile Remove="Pages\UserOrder\Components\UserOrderDetailsDialog.razor" /> -->
|
||||||
|
<!-- <Content Remove="Pages\UserOrder\Components\UserOrderDetailsDialog.razor" /> -->
|
||||||
|
<!-- <Compile Remove="Pages\UserOrder\Components\UserOrderDetailsDialog.razor.cs" /> -->
|
||||||
|
|
||||||
|
<!-- UserOrderMainPage - RESTORED - dialogs are back -->
|
||||||
|
<!-- <Compile Remove="Pages\UserOrder\UserOrderMainPage.razor" /> -->
|
||||||
|
<!-- <Content Remove="Pages\UserOrder\UserOrderMainPage.razor" /> -->
|
||||||
|
<!-- <Compile Remove="Pages\UserOrder\UserOrderMainPage.razor.cs" /> -->
|
||||||
|
|
||||||
|
<!-- Transactions - ENABLED -->
|
||||||
|
<!-- <Compile Remove="Pages\Payment\Transactions.razor" /> -->
|
||||||
|
<!-- <Content Remove="Pages\Payment\Transactions.razor" /> -->
|
||||||
|
<!-- <Compile Remove="Pages\Payment\Transactions.razor.cs" /> -->
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0"/>
|
||||||
|
<PackageReference Include="DateTimeConverterCL" Version="1.0.0"/>
|
||||||
|
<PackageReference Include="Grpc.Core" Version="2.46.6"/>
|
||||||
|
<PackageReference Include="Grpc.Net.Client" Version="2.71.0"/>
|
||||||
|
<PackageReference Include="Grpc.Net.Client.Web" Version="2.71.0"/>
|
||||||
|
<PackageReference Include="HtmlAgilityPack" Version="1.12.1"/>
|
||||||
|
<PackageReference Include="Mapster" Version="7.4.0"/>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.11"/>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.11"/>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.11" PrivateAssets="all"/>
|
||||||
|
<PackageReference Include="MudBlazor" Version="8.14.0"/>
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.4"/>
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.8.0"/>
|
||||||
|
<PackageReference Include="Tizzani.MudBlazor.HtmlEditor" Version="2.3.0"/>
|
||||||
|
|
||||||
|
<!-- Proto dependencies (needed when using DLL references) -->
|
||||||
|
<PackageReference Include="Google.Protobuf" Version="3.28.3"/>
|
||||||
|
<PackageReference Include="Google.Api.CommonProtos" Version="2.10.0"/>
|
||||||
|
<PackageReference Include="FluentValidation" Version="11.2.2"/>
|
||||||
|
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.2.2"/>
|
||||||
|
</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.208" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- ============================================================ -->
|
||||||
|
<!-- OLD BFF Proto References (DEPRECATED - kept for reference) -->
|
||||||
|
<!-- ============================================================ -->
|
||||||
|
|
||||||
|
<!-- CI/CD Mode: Use pre-built DLLs from libs/ folder -->
|
||||||
|
<ItemGroup Condition="false AND Exists('../../libs/BackOffice.BFF.Common.Protobuf.dll')">
|
||||||
|
<Reference Include="BackOffice.BFF.Common.Protobuf">
|
||||||
|
<HintPath>../../libs/BackOffice.BFF.Common.Protobuf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="BackOffice.BFF.Category.Protobuf">
|
||||||
|
<HintPath>../../libs/BackOffice.BFF.Category.Protobuf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="BackOffice.BFF.ClubMembership.Protobuf">
|
||||||
|
<HintPath>../../libs/BackOffice.BFF.ClubMembership.Protobuf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="BackOffice.BFF.Commission.Protobuf">
|
||||||
|
<HintPath>../../libs/BackOffice.BFF.Commission.Protobuf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="BackOffice.BFF.Configuration.Protobuf">
|
||||||
|
<HintPath>../../libs/BackOffice.BFF.Configuration.Protobuf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="BackOffice.BFF.DiscountCategory.Protobuf">
|
||||||
|
<HintPath>../../libs/BackOffice.BFF.DiscountCategory.Protobuf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="BackOffice.BFF.DiscountOrder.Protobuf">
|
||||||
|
<HintPath>../../libs/BackOffice.BFF.DiscountOrder.Protobuf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="BackOffice.BFF.DiscountProduct.Protobuf">
|
||||||
|
<HintPath>../../libs/BackOffice.BFF.DiscountProduct.Protobuf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="BackOffice.BFF.DiscountShoppingCart.Protobuf">
|
||||||
|
<HintPath>../../libs/BackOffice.BFF.DiscountShoppingCart.Protobuf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="BackOffice.BFF.Health.Protobuf">
|
||||||
|
<HintPath>../../libs/BackOffice.BFF.Health.Protobuf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="BackOffice.BFF.Inventory.Protobuf">
|
||||||
|
<HintPath>../../libs/BackOffice.BFF.Inventory.Protobuf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="BackOffice.BFF.ManualPayment.Protobuf">
|
||||||
|
<HintPath>../../libs/BackOffice.BFF.ManualPayment.Protobuf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="BackOffice.BFF.NetworkMembership.Protobuf">
|
||||||
|
<HintPath>../../libs/BackOffice.BFF.NetworkMembership.Protobuf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="BackOffice.BFF.Otp.Protobuf">
|
||||||
|
<HintPath>../../libs/BackOffice.BFF.Otp.Protobuf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="BackOffice.BFF.Package.Protobuf">
|
||||||
|
<HintPath>../../libs/BackOffice.BFF.Package.Protobuf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="BackOffice.BFF.Products.Protobuf">
|
||||||
|
<HintPath>../../libs/BackOffice.BFF.Products.Protobuf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="BackOffice.BFF.ProductTag.Protobuf">
|
||||||
|
<HintPath>../../libs/BackOffice.BFF.ProductTag.Protobuf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="BackOffice.BFF.PublicMessage.Protobuf">
|
||||||
|
<HintPath>../../libs/BackOffice.BFF.PublicMessage.Protobuf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="BackOffice.BFF.Role.Protobuf">
|
||||||
|
<HintPath>../../libs/BackOffice.BFF.Role.Protobuf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="BackOffice.BFF.Tag.Protobuf">
|
||||||
|
<HintPath>../../libs/BackOffice.BFF.Tag.Protobuf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="BackOffice.BFF.User.Protobuf">
|
||||||
|
<HintPath>../../libs/BackOffice.BFF.User.Protobuf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="BackOffice.BFF.UserAddress.Protobuf">
|
||||||
|
<HintPath>../../libs/BackOffice.BFF.UserAddress.Protobuf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="BackOffice.BFF.UserOrder.Protobuf">
|
||||||
|
<HintPath>../../libs/BackOffice.BFF.UserOrder.Protobuf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="BackOffice.BFF.UserRole.Protobuf">
|
||||||
|
<HintPath>../../libs/BackOffice.BFF.UserRole.Protobuf.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Dev Mode: OLD BFF ProjectReference (DEPRECATED) -->
|
||||||
|
<ItemGroup Condition="false AND !Exists('../../libs/BackOffice.BFF.Common.Protobuf.dll')">
|
||||||
|
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Common.Protobuf/BackOffice.BFF.Common.Protobuf.csproj" />
|
||||||
|
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Category.Protobuf/BackOffice.BFF.Category.Protobuf.csproj" />
|
||||||
|
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.ClubMembership.Protobuf/BackOffice.BFF.ClubMembership.Protobuf.csproj" />
|
||||||
|
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Commission.Protobuf/BackOffice.BFF.Commission.Protobuf.csproj" />
|
||||||
|
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Configuration.Protobuf/BackOffice.BFF.Configuration.Protobuf.csproj" />
|
||||||
|
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.DiscountCategory.Protobuf/BackOffice.BFF.DiscountCategory.Protobuf.csproj" />
|
||||||
|
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.DiscountOrder.Protobuf/BackOffice.BFF.DiscountOrder.Protobuf.csproj" />
|
||||||
|
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.DiscountProduct.Protobuf/BackOffice.BFF.DiscountProduct.Protobuf.csproj" />
|
||||||
|
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.DiscountShoppingCart.Protobuf/BackOffice.BFF.DiscountShoppingCart.Protobuf.csproj" />
|
||||||
|
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Health.Protobuf/BackOffice.BFF.Health.Protobuf.csproj" />
|
||||||
|
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Inventory.Protobuf/BackOffice.BFF.Inventory.Protobuf.csproj" />
|
||||||
|
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.ManualPayment.Protobuf/BackOffice.BFF.ManualPayment.Protobuf.csproj" />
|
||||||
|
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.NetworkMembership.Protobuf/BackOffice.BFF.NetworkMembership.Protobuf.csproj" />
|
||||||
|
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Otp.Protobuf/BackOffice.BFF.Otp.Protobuf.csproj" />
|
||||||
|
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Package.Protobuf/BackOffice.BFF.Package.Protobuf.csproj" />
|
||||||
|
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Products.Protobuf/BackOffice.BFF.Products.Protobuf.csproj" />
|
||||||
|
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.ProductTag.Protobuf/BackOffice.BFF.ProductTag.Protobuf.csproj" />
|
||||||
|
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.PublicMessage.Protobuf/BackOffice.BFF.PublicMessage.Protobuf.csproj" />
|
||||||
|
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Role.Protobuf/BackOffice.BFF.Role.Protobuf.csproj" />
|
||||||
|
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Tag.Protobuf/BackOffice.BFF.Tag.Protobuf.csproj" />
|
||||||
|
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.User.Protobuf/BackOffice.BFF.User.Protobuf.csproj" />
|
||||||
|
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.UserAddress.Protobuf/BackOffice.BFF.UserAddress.Protobuf.csproj" />
|
||||||
|
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.UserOrder.Protobuf/BackOffice.BFF.UserOrder.Protobuf.csproj" />
|
||||||
|
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.UserRole.Protobuf/BackOffice.BFF.UserRole.Protobuf.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,34 +1,54 @@
|
|||||||
|
@* ── Responsive base page: filter above grid, collapsible ── *@
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Class="pa-2 pr-4 rounded" Style="height:85vh;">
|
<MudStack Spacing="0" Class="base-page">
|
||||||
<MudItem xs="2">
|
|
||||||
<MudPaper Height="85vh" Style="overflow:hidden; position:relative;" Outlined="true" Class="rounded-lg">
|
@* ── Header row: page title + filter toggle ── *@
|
||||||
<MudStack Class="pa-5" Style="height:100%;">
|
@if (Filters != null)
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
{
|
||||||
<MudText Typo="Typo.subtitle2">فیلتر ها</MudText>
|
<MudPaper Outlined="true" Class="rounded-lg mb-3 base-page__filter-panel">
|
||||||
<MudSpacer />
|
<MudStack Row="true" AlignItems="AlignItems.Center" Class="pa-3 cursor-pointer"
|
||||||
@if (IsFiltered)
|
@onclick="ToggleFilterPanel">
|
||||||
{
|
<MudIcon Icon="@Icons.Material.Filled.FilterList" Size="Size.Small"
|
||||||
<MudButton Color="Color.Error" Variant="Variant.Text" Size="Size.Small" OnClick="@(async () => await OnClearFilterClick.InvokeAsync())">حذف فیلتر</MudButton>
|
Color="@(IsFiltered ? Color.Primary : Color.Default)" />
|
||||||
}
|
<MudText Typo="Typo.subtitle2" Class="mx-1">فیلترها</MudText>
|
||||||
</MudStack>
|
@if (IsFiltered)
|
||||||
<MudItem Style="height:100%;">
|
{
|
||||||
<MudStack StretchItems="StretchItems.None" Class="filter-item" Style="height:100%;">
|
<MudChip T="string" Color="Color.Primary" Size="Size.Small" Variant="Variant.Text">فعال</MudChip>
|
||||||
<MudStack>
|
}
|
||||||
@Filters
|
<MudSpacer />
|
||||||
</MudStack>
|
<MudIcon Icon="@(_filterOpen ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)"
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Size="Size.Small" FullWidth="true" Class="mt-auto" OnClick="@(async () => await OnSubmitClick.InvokeAsync())">جستجو</MudButton>
|
Size="Size.Small" />
|
||||||
</MudStack>
|
|
||||||
</MudItem>
|
|
||||||
</MudStack>
|
</MudStack>
|
||||||
|
|
||||||
|
@if (_filterOpen)
|
||||||
|
{
|
||||||
|
<MudDivider />
|
||||||
|
<div class="pa-3">
|
||||||
|
<MudGrid Spacing="2">
|
||||||
|
@Filters
|
||||||
|
</MudGrid>
|
||||||
|
<MudStack Row="true" Justify="Justify.FlexEnd" Class="mt-3" Spacing="2">
|
||||||
|
@if (IsFiltered)
|
||||||
|
{
|
||||||
|
<MudButton Color="Color.Error" Variant="Variant.Text" Size="Size.Small"
|
||||||
|
OnClick="@(async () => await OnClearFilterClick.InvokeAsync())">
|
||||||
|
پاک کردن فیلتر
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" Size="Size.Small"
|
||||||
|
StartIcon="@Icons.Material.Filled.Search"
|
||||||
|
OnClick="@(async () => await OnSubmitClick.InvokeAsync())">
|
||||||
|
جستجو
|
||||||
|
</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
}
|
||||||
|
|
||||||
</MudItem>
|
@* ── Content area (DataGrid or custom layout) ── *@
|
||||||
<MudItem xs="10">
|
<MudPaper Outlined="true" Class="rounded-lg base-page__content" Style="overflow:hidden; position:relative;">
|
||||||
<MudPaper Height="85vh" Style="overflow:hidden; position:relative;" Outlined="true" Class="rounded-lg">
|
@Content
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
@Content
|
|
||||||
</MudPaper>
|
|
||||||
</MudItem>
|
|
||||||
</MudStack>
|
</MudStack>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
namespace BackOffice.Common.BaseComponents;
|
namespace BackOffice.Common.BaseComponents;
|
||||||
|
|
||||||
public partial class BasePageComponent
|
public partial class BasePageComponent
|
||||||
{
|
{
|
||||||
[Parameter] public RenderFragment Filters { get; set; } = default!;
|
[Parameter] public RenderFragment? Filters { get; set; }
|
||||||
[Parameter] public RenderFragment Content { get; set; } = default!;
|
[Parameter] public RenderFragment Content { get; set; } = default!;
|
||||||
[Parameter] public bool IsFiltered { get; set; }
|
[Parameter] public bool IsFiltered { get; set; }
|
||||||
[Parameter] public EventCallback OnSubmitClick { get; set; }
|
[Parameter] public EventCallback OnSubmitClick { get; set; }
|
||||||
[Parameter] public EventCallback OnClearFilterClick { get; set; }
|
[Parameter] public EventCallback OnClearFilterClick { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Whether the filter panel is expanded.</summary>
|
||||||
|
private bool _filterOpen;
|
||||||
|
|
||||||
|
private void ToggleFilterPanel() => _filterOpen = !_filterOpen;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/* ── BasePageComponent scoped styles ── */
|
||||||
|
|
||||||
|
.base-page {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.base-page__filter-panel {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.base-page__content {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
@@ -8,21 +8,46 @@ namespace BackOffice.Common.BaseComponents;
|
|||||||
public partial class DateRangePicker
|
public partial class DateRangePicker
|
||||||
{
|
{
|
||||||
private DateRange _dateRange = new();
|
private DateRange _dateRange = new();
|
||||||
|
private MudDateRangePicker _picker = default!;
|
||||||
|
private DateTime? _lastSyncedFrom;
|
||||||
|
private DateTime? _lastSyncedTo;
|
||||||
|
|
||||||
private MudDateRangePicker _picker;
|
|
||||||
[Parameter] public string Label { get; set; } = "انتخاب بازه زمانی";
|
[Parameter] public string Label { get; set; } = "انتخاب بازه زمانی";
|
||||||
[Parameter] public DateTime? DefaultStart { get; set; }
|
[Parameter] public DateTime? DefaultStart { get; set; }
|
||||||
[Parameter] public DateTime? DefaultEnd { get; set; }
|
[Parameter] public DateTime? DefaultEnd { get; set; }
|
||||||
[Parameter] public EventCallback<DateRange> OnChanged { get; set; }
|
[Parameter] public EventCallback<DateRange> OnChanged { get; set; }
|
||||||
|
|
||||||
|
[Parameter] public DateTime? From { get; set; }
|
||||||
|
[Parameter] public EventCallback<DateTime?> FromChanged { get; set; }
|
||||||
|
[Parameter] public DateTime? To { get; set; }
|
||||||
|
[Parameter] public EventCallback<DateTime?> ToChanged { get; set; }
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
base.OnInitialized();
|
base.OnInitialized();
|
||||||
if (DefaultStart.HasValue)
|
var start = From ?? DefaultStart;
|
||||||
_dateRange.Start = DefaultStart.Value.Date;
|
var end = To ?? DefaultEnd;
|
||||||
|
ApplyExternalRange(start, end);
|
||||||
if (DefaultEnd.HasValue)
|
_lastSyncedFrom = From;
|
||||||
_dateRange.End = DefaultEnd.Value.Date;
|
_lastSyncedTo = To;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnParametersSet()
|
||||||
|
{
|
||||||
|
// فقط وقتی والد From/To را عوض کرده (مثلاً پاک کردن فیلتر)، نه هنگام انتخاب داخل پیکر
|
||||||
|
if (From == _lastSyncedFrom && To == _lastSyncedTo)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_lastSyncedFrom = From;
|
||||||
|
_lastSyncedTo = To;
|
||||||
|
ApplyExternalRange(From, To);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyExternalRange(DateTime? start, DateTime? end)
|
||||||
|
{
|
||||||
|
_dateRange = new DateRange(start?.Date, end?.Date);
|
||||||
|
}
|
||||||
|
|
||||||
public CultureInfo GetPersianCulture()
|
public CultureInfo GetPersianCulture()
|
||||||
{
|
{
|
||||||
var culture = new CultureInfo("fa-IR");
|
var culture = new CultureInfo("fa-IR");
|
||||||
@@ -53,19 +78,35 @@ public partial class DateRangePicker
|
|||||||
culture.NumberFormat.NumberNegativePattern = 0;
|
culture.NumberFormat.NumberNegativePattern = 0;
|
||||||
return culture;
|
return culture;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnClickOK()
|
private async Task OnClickOK()
|
||||||
{
|
{
|
||||||
if (_picker.DateRange is not null)
|
var range = _picker.DateRange ?? _dateRange;
|
||||||
{
|
_dateRange = range ?? new DateRange();
|
||||||
await OnChanged.InvokeAsync(_picker.DateRange);
|
await NotifyRangeChangedAsync(_dateRange);
|
||||||
}
|
|
||||||
|
|
||||||
await _picker.CloseAsync();
|
await _picker.CloseAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnClickClear()
|
private async Task OnClickClear()
|
||||||
{
|
{
|
||||||
_dateRange = new DateRange();
|
_dateRange = new DateRange();
|
||||||
await OnChanged.InvokeAsync(_dateRange);
|
await NotifyRangeChangedAsync(_dateRange);
|
||||||
await _picker.CloseAsync();
|
await _picker.CloseAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task NotifyRangeChangedAsync(DateRange range)
|
||||||
|
{
|
||||||
|
var start = range.Start?.Date;
|
||||||
|
var end = range.End?.Date;
|
||||||
|
|
||||||
|
_lastSyncedFrom = start;
|
||||||
|
_lastSyncedTo = end;
|
||||||
|
|
||||||
|
if (From != start)
|
||||||
|
await FromChanged.InvokeAsync(start);
|
||||||
|
if (To != end)
|
||||||
|
await ToChanged.InvokeAsync(end);
|
||||||
|
|
||||||
|
await OnChanged.InvokeAsync(range);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,6 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<MudImage Src="@src" Alt="@Alt" Elevation="@Elevation" Class="@Class" Fluid="@Fluid" Height="@Height" ObjectFit="@ObjectFit" ObjectPosition="@ObjectPosition" Style="@(Style + ";cursor:pointer;")" Tag="@Tag" Width="@Width" @onclick="async()=>await OnClick.InvokeAsync()" />
|
<MudImage Src="@src" Alt="@Alt" Elevation="@Elevation" Class="@Class" Fluid="@Fluid" Height="@Height" ObjectFit="@ObjectFit" ObjectPosition="@ObjectPosition" Style="@Style" Tag="@Tag" Width="@Width" @onclick="async()=>await OnClick.InvokeAsync()" />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
|
|
||||||
namespace BackOffice.Common.BaseComponents
|
namespace BackOffice.Common.BaseComponents
|
||||||
@@ -13,13 +14,24 @@ namespace BackOffice.Common.BaseComponents
|
|||||||
{
|
{
|
||||||
private string src = string.Empty;
|
private string src = string.Empty;
|
||||||
[Parameter] public EventCallback OnClick { get; set; }
|
[Parameter] public EventCallback OnClick { get; set; }
|
||||||
|
[Inject] private IConfiguration Configuration { get; set; } = default!;
|
||||||
|
|
||||||
|
private string FileBaseUrl => Configuration["GwUrl"]?.TrimEnd('/') ?? "";
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
await base.OnParametersSetAsync();
|
await base.OnParametersSetAsync();
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(Src))
|
if (!string.IsNullOrWhiteSpace(Src))
|
||||||
{
|
{
|
||||||
src = Src.Contains("data:") ? Src : "https://dl.afrino.co" + Src;
|
if (Src.Contains("data:") || Src.StartsWith("http"))
|
||||||
|
src = Src;
|
||||||
|
else
|
||||||
|
src = FileBaseUrl + "/uploads/" + Src.TrimStart('/');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
src = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,49 @@
|
|||||||
using BackOffice.BFF.Otp.Protobuf.Protos.Otp;
|
using CMSMicroservice.Protobuf.Protos.OtpToken;
|
||||||
using BackOffice.BFF.Package.Protobuf.Protos.Package;
|
using CMSMicroservice.Protobuf.Protos.Package;
|
||||||
using BackOffice.BFF.Role.Protobuf.Protos.Role;
|
using CMSMicroservice.Protobuf.Protos.Role;
|
||||||
using BackOffice.BFF.Products.Protobuf.Protos.Products;
|
using CMSMicroservice.Protobuf.Protos.Products;
|
||||||
using BackOffice.BFF.User.Protobuf.Protos.User;
|
using CMSMicroservice.Protobuf.Protos.User;
|
||||||
using BackOffice.BFF.UserAddress.Protobuf.Protos.UserAddress;
|
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
||||||
using BackOffice.BFF.UserOrder.Protobuf.Protos.UserOrder;
|
using CMSMicroservice.Protobuf.Protos.UserOrder;
|
||||||
using BackOffice.BFF.UserRole.Protobuf.Protos.UserRole;
|
using CMSMicroservice.Protobuf.Protos.UserRole;
|
||||||
using BackOffice.BFF.Category.Protobuf.Protos.Category;
|
using CMSMicroservice.Protobuf.Protos.Category;
|
||||||
using BackOffice.BFF.Commission.Protobuf;
|
using CMSMicroservice.Protobuf.Protos.Commission;
|
||||||
using BackOffice.BFF.NetworkMembership.Protobuf;
|
using CMSMicroservice.Protobuf.Protos.ClubMembership;
|
||||||
using BackOffice.BFF.ClubMembership.Protobuf;
|
using CMSMicroservice.Protobuf.Protos.Configuration;
|
||||||
using BackOffice.BFF.Configuration.Protobuf;
|
using CMSMicroservice.Protobuf.Protos.NetworkMembership;
|
||||||
using BackOffice.BFF.Health.Protobuf;
|
using CMSMicroservice.Protobuf.Protos.Health;
|
||||||
using BackOffice.BFF.DiscountProduct.Protobuf.Protos.DiscountProduct;
|
using CMSMicroservice.Protobuf.Protos.AppVersion;
|
||||||
using BackOffice.BFF.DiscountCategory.Protobuf.Protos.DiscountCategory;
|
|
||||||
using BackOffice.BFF.DiscountOrder.Protobuf.Protos.DiscountOrder;
|
// Discount Shop Proto Clients
|
||||||
using BackOffice.BFF.Tag.Protobuf.Protos.Tag;
|
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
|
||||||
using BackOffice.BFF.ProductTag.Protobuf.Protos.ProductTag;
|
using CMSMicroservice.Protobuf.Protos.DiscountCategory;
|
||||||
using BackOffice.BFF.PublicMessage.Protobuf.Protos.PublicMessage;
|
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
|
||||||
|
using CMSMicroservice.Protobuf.Protos.Tag;
|
||||||
|
using CMSMicroservice.Protobuf.Protos.ProductTag;
|
||||||
|
using CMSMicroservice.Protobuf.Protos.ManualPayment;
|
||||||
|
using CMSMicroservice.Protobuf.Protos;
|
||||||
|
using CMSMicroservice.Protobuf.Protos.Inventory;
|
||||||
|
using CMSMicroservice.Protobuf.Protos.BlogPost;
|
||||||
|
using CMSMicroservice.Protobuf.Protos.BlogCategory;
|
||||||
|
using CMSMicroservice.Protobuf.Protos.BlogPostImage;
|
||||||
|
using CMSMicroservice.Protobuf.Protos.SitePage;
|
||||||
|
using CMSMicroservice.Protobuf.Protos.SitePageSettings;
|
||||||
|
using CMSMicroservice.Protobuf.Protos.UserContract;
|
||||||
|
using CMSMicroservice.Protobuf.Protos.UserWallet;
|
||||||
|
using CMSMicroservice.Protobuf.Protos.UserWalletHistory;
|
||||||
|
using CMSMicroservice.Protobuf.Protos.PaymentTransaction;
|
||||||
|
using CMSMicroservice.Protobuf.Protos.Transactions;
|
||||||
|
using CMSMicroservice.Protobuf.Protos.UserPackagePurchase;
|
||||||
|
using CMSMicroservice.Protobuf.Protos.ClubMembershipCycle;
|
||||||
|
using CMSMicroservice.Protobuf.Protos.ClubMembershipCycleHistory;
|
||||||
using BackOffice.Common.Utilities;
|
using BackOffice.Common.Utilities;
|
||||||
using BackOffice.Services.DiscountProduct;
|
using BackOffice.Services.DiscountProduct;
|
||||||
using BackOffice.Services.DiscountCategory;
|
using BackOffice.Services.DiscountCategory;
|
||||||
using BackOffice.Services.DiscountOrder;
|
using BackOffice.Services.DiscountOrder;
|
||||||
using BackOffice.Services.PublicMessage;
|
using BackOffice.Services.PublicMessage;
|
||||||
using BackOffice.Services.Tag;
|
using BackOffice.Services.Tag;
|
||||||
|
using BackOffice.Services.Blog;
|
||||||
|
using BackOffice.Services.Content;
|
||||||
using Blazored.LocalStorage;
|
using Blazored.LocalStorage;
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
using Grpc.Core.Interceptors;
|
using Grpc.Core.Interceptors;
|
||||||
@@ -58,13 +78,27 @@ public static class ConfigureServices
|
|||||||
services.AddMudServices();
|
services.AddMudServices();
|
||||||
services.AddGrpcServices(configuration);
|
services.AddGrpcServices(configuration);
|
||||||
|
|
||||||
|
// Persian DateTime Service
|
||||||
|
services.AddSingleton<BackOffice.Services.IPersianDateTimeService, BackOffice.Services.PersianDateTimeService>();
|
||||||
|
|
||||||
// Application Services
|
// Application Services
|
||||||
|
services.AddScoped<BackOffice.Services.Authorization.IAuthorizationService, BackOffice.Services.Authorization.AuthorizationService>();
|
||||||
|
services.AddScoped<BackOffice.Services.AppVersion.IAppVersionService, BackOffice.Services.AppVersion.AppVersionService>();
|
||||||
|
|
||||||
|
// Discount Shop Services
|
||||||
services.AddScoped<IDiscountProductService, DiscountProductService>();
|
services.AddScoped<IDiscountProductService, DiscountProductService>();
|
||||||
services.AddScoped<IDiscountCategoryService, DiscountCategoryService>();
|
services.AddScoped<IDiscountCategoryService, DiscountCategoryService>();
|
||||||
services.AddScoped<IDiscountOrderService, DiscountOrderService>();
|
services.AddScoped<IDiscountOrderService, DiscountOrderService>();
|
||||||
services.AddScoped<IPublicMessageService, PublicMessageService>();
|
services.AddScoped<IPublicMessageService, PublicMessageService>();
|
||||||
services.AddScoped<ITagService, TagService>();
|
services.AddScoped<ITagService, TagService>();
|
||||||
|
|
||||||
|
// Blog & Content Management Services
|
||||||
|
services.AddScoped<IBlogPostService, BlogPostService>();
|
||||||
|
services.AddScoped<IBlogCategoryService, BlogCategoryService>();
|
||||||
|
services.AddScoped<IBlogPostImageService, BlogPostImageService>();
|
||||||
|
services.AddScoped<ISitePageService, SitePageService>();
|
||||||
|
services.AddScoped<ISitePageSettingsService, SitePageSettingsService>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +124,7 @@ public static class ConfigureServices
|
|||||||
return CreateAuthenticatedChannel(baseUri, httpClient, sp);
|
return CreateAuthenticatedChannel(baseUri, httpClient, sp);
|
||||||
});
|
});
|
||||||
|
|
||||||
services.AddTransient(sp => new OtpContract.OtpContractClient(sp.GetRequiredService<CallInvoker>()));
|
services.AddTransient(sp => new OtpTokenContract.OtpTokenContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
services.AddTransient(sp => new PackageContract.PackageContractClient(sp.GetRequiredService<CallInvoker>()));
|
services.AddTransient(sp => new PackageContract.PackageContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
services.AddTransient(sp => new ProductsContract.ProductsContractClient(sp.GetRequiredService<CallInvoker>()));
|
services.AddTransient(sp => new ProductsContract.ProductsContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
services.AddTransient(sp => new RoleContract.RoleContractClient(sp.GetRequiredService<CallInvoker>()));
|
services.AddTransient(sp => new RoleContract.RoleContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
@@ -106,19 +140,47 @@ public static class ConfigureServices
|
|||||||
services.AddTransient(sp => new ClubMembershipContract.ClubMembershipContractClient(sp.GetRequiredService<CallInvoker>()));
|
services.AddTransient(sp => new ClubMembershipContract.ClubMembershipContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
services.AddTransient(sp => new ConfigurationContract.ConfigurationContractClient(sp.GetRequiredService<CallInvoker>()));
|
services.AddTransient(sp => new ConfigurationContract.ConfigurationContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
services.AddTransient(sp => new HealthContract.HealthContractClient(sp.GetRequiredService<CallInvoker>()));
|
services.AddTransient(sp => new HealthContract.HealthContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
|
services.AddTransient(sp => new AppVersionContract.AppVersionContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
|
|
||||||
// Discount Shop Services
|
// Discount Shop Services
|
||||||
services.AddTransient(sp => new DiscountProductsContract.DiscountProductsContractClient(sp.GetRequiredService<CallInvoker>()));
|
services.AddTransient(sp => new DiscountProductContract.DiscountProductContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
services.AddTransient(sp => new DiscountCategoriesContract.DiscountCategoriesContractClient(sp.GetRequiredService<CallInvoker>()));
|
services.AddTransient(sp => new DiscountCategoryContract.DiscountCategoryContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
services.AddTransient(sp => new DiscountOrdersContract.DiscountOrdersContractClient(sp.GetRequiredService<CallInvoker>()));
|
services.AddTransient(sp => new DiscountOrderContract.DiscountOrderContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
|
|
||||||
// Public Message Service
|
// Public Message Service
|
||||||
services.AddTransient(sp => new PublicMessagesContract.PublicMessagesContractClient(sp.GetRequiredService<CallInvoker>()));
|
services.AddTransient(sp => new PublicMessageContract.PublicMessageContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
|
|
||||||
// Tag Management Services
|
// Tag Management Services
|
||||||
services.AddTransient(sp => new TagContract.TagContractClient(sp.GetRequiredService<CallInvoker>()));
|
services.AddTransient(sp => new TagContract.TagContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
services.AddTransient(sp => new ProductTagContract.ProductTagContractClient(sp.GetRequiredService<CallInvoker>()));
|
services.AddTransient(sp => new ProductTagContract.ProductTagContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
|
|
||||||
|
// Manual Payment Service
|
||||||
|
services.AddTransient(sp => new ManualPaymentContract.ManualPaymentContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
|
|
||||||
|
// Inventory Management Service
|
||||||
|
services.AddTransient(sp => new InventoryContract.InventoryContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
|
|
||||||
|
// Blog & Content Management Services
|
||||||
|
services.AddTransient(sp => new BlogPostContract.BlogPostContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
|
services.AddTransient(sp => new BlogCategoryContract.BlogCategoryContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
|
services.AddTransient(sp => new BlogPostImageContract.BlogPostImageContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
|
services.AddTransient(sp => new SitePageContract.SitePageContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
|
services.AddTransient(sp => new SitePageSettingsContract.SitePageSettingsContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
|
|
||||||
|
// User Contracts Service
|
||||||
|
services.AddTransient(sp => new UserContractContract.UserContractContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
|
|
||||||
|
// User Wallet Services
|
||||||
|
services.AddTransient(sp => new UserWalletContract.UserWalletContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
|
services.AddTransient(sp => new UserWalletHistoryContract.UserWalletHistoryContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
|
|
||||||
|
// Admin Report Services
|
||||||
|
services.AddTransient(sp => new PaymentTransactionContract.PaymentTransactionContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
|
services.AddTransient(sp => new TransactionsContract.TransactionsContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
|
services.AddTransient(sp => new UserPackagePurchaseContract.UserPackagePurchaseContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
|
services.AddTransient(sp => new ClubMembershipCycleContract.ClubMembershipCycleContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
|
services.AddTransient(sp => new ClubMembershipCycleHistoryContract.ClubMembershipCycleHistoryContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,27 +189,28 @@ public static class ConfigureServices
|
|||||||
var credentials = CallCredentials.FromInterceptor(async (context, metadata) =>
|
var credentials = CallCredentials.FromInterceptor(async (context, metadata) =>
|
||||||
{
|
{
|
||||||
var provider = serviceProvider.GetRequiredService<ITokenProvider>();
|
var provider = serviceProvider.GetRequiredService<ITokenProvider>();
|
||||||
// var accessToken = await provider.RequestAccessToken();
|
|
||||||
// accessToken.TryGetToken(out var token);
|
|
||||||
var token = await provider.GetTokenAsync();
|
var token = await provider.GetTokenAsync();
|
||||||
if (!string.IsNullOrEmpty(token))
|
if (!string.IsNullOrEmpty(token))
|
||||||
{
|
{
|
||||||
// Console.WriteLine($"Authorization Bearer {token.Value}");
|
|
||||||
metadata.Add("Authorization", $"Bearer {token}");
|
metadata.Add("Authorization", $"Bearer {token}");
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.CompletedTask;
|
await Task.CompletedTask;
|
||||||
});
|
});
|
||||||
|
|
||||||
// SslCredentials is used here because this channel is using TLS.
|
// تشخیص HTTP یا HTTPS
|
||||||
// CallCredentials can't be used with ChannelCredentials.Insecure on non-TLS channels.
|
var isHttps = address.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var channelCredentials = isHttps
|
||||||
|
? ChannelCredentials.Create(new SslCredentials(), credentials)
|
||||||
|
: ChannelCredentials.Create(ChannelCredentials.Insecure, credentials);
|
||||||
|
|
||||||
var channel = GrpcChannel.ForAddress(address, new GrpcChannelOptions
|
var channel = GrpcChannel.ForAddress(address, new GrpcChannelOptions
|
||||||
{
|
{
|
||||||
UnsafeUseInsecureChannelCallCredentials = true,
|
UnsafeUseInsecureChannelCallCredentials = !isHttps, // فقط برای HTTP
|
||||||
Credentials = ChannelCredentials.Create(new SslCredentials(), credentials),
|
Credentials = channelCredentials,
|
||||||
HttpClient = httpClient,
|
HttpClient = httpClient,
|
||||||
MaxReceiveMessageSize = 1000 * 1024 * 1024, // 1 GB
|
MaxReceiveMessageSize = 1000 * 1024 * 1024,
|
||||||
MaxSendMessageSize = 1000 * 1024 * 1024 // 1 GB
|
MaxSendMessageSize = 1000 * 1024 * 1024
|
||||||
});
|
});
|
||||||
return channel.Intercept(new ErrorHandlerInterceptor());
|
return channel.Intercept(new ErrorHandlerInterceptor());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Blazored.LocalStorage;
|
using Blazored.LocalStorage;
|
||||||
@@ -27,12 +27,53 @@ public class ApiAuthenticationStateProvider : AuthenticationStateProvider
|
|||||||
var handler = new JwtSecurityTokenHandler();
|
var handler = new JwtSecurityTokenHandler();
|
||||||
var token = handler.ReadJwtToken(savedToken);
|
var token = handler.ReadJwtToken(savedToken);
|
||||||
|
|
||||||
|
// Parse the token payload to handle array-based roles
|
||||||
|
var claims = new List<Claim>();
|
||||||
|
|
||||||
|
foreach (var claim in token.Claims)
|
||||||
|
{
|
||||||
|
// Handle role claims specially to support multiple roles
|
||||||
|
if (claim.Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/role" ||
|
||||||
|
claim.Type == ClaimTypes.Role ||
|
||||||
|
claim.Type == "role")
|
||||||
|
{
|
||||||
|
// Check if the value is a JSON array
|
||||||
|
if (claim.Value.TrimStart().StartsWith("["))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var roles = JsonSerializer.Deserialize<string[]>(claim.Value);
|
||||||
|
if (roles != null)
|
||||||
|
{
|
||||||
|
foreach (var role in roles)
|
||||||
|
{
|
||||||
|
claims.Add(new Claim(ClaimTypes.Role, role));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// If parsing fails, add as single claim
|
||||||
|
claims.Add(new Claim(ClaimTypes.Role, claim.Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Single role value
|
||||||
|
claims.Add(new Claim(ClaimTypes.Role, claim.Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
claims.Add(claim);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var AuthenticationState = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(token.Claims, "jwt")));
|
var authenticationState = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims, "jwt")));
|
||||||
|
|
||||||
return AuthenticationState;
|
return authenticationState;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch
|
||||||
{
|
{
|
||||||
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using BackOffice.Common.Utilities;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using BackOffice.Common.Utilities;
|
||||||
using Blazored.LocalStorage;
|
using Blazored.LocalStorage;
|
||||||
|
|
||||||
namespace BackOffice.Common.Utilities;
|
namespace BackOffice.Common.Utilities;
|
||||||
@@ -19,9 +20,41 @@ public class AppTokenProvider : ITokenProvider
|
|||||||
{
|
{
|
||||||
var authorizationToken = await _localStorage.GetItemAsync<string>(GlobalConstants.JwtTokenKey);
|
var authorizationToken = await _localStorage.GetItemAsync<string>(GlobalConstants.JwtTokenKey);
|
||||||
if (!string.IsNullOrEmpty(authorizationToken))
|
if (!string.IsNullOrEmpty(authorizationToken))
|
||||||
_token = authorizationToken.ToString().Replace("Bearer ", "");
|
{
|
||||||
|
var raw = authorizationToken.ToString().Replace("Bearer ", "");
|
||||||
|
// Don't send expired tokens — they cause 401 on anonymous endpoints like login
|
||||||
|
if (IsTokenExpired(raw))
|
||||||
|
{
|
||||||
|
await _localStorage.RemoveItemAsync(GlobalConstants.JwtTokenKey);
|
||||||
|
_token = null;
|
||||||
|
return _token;
|
||||||
|
}
|
||||||
|
_token = raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (IsTokenExpired(_token))
|
||||||
|
{
|
||||||
|
// Cached token has expired since last check
|
||||||
|
await _localStorage.RemoveItemAsync(GlobalConstants.JwtTokenKey);
|
||||||
|
_token = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _token;
|
return _token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsTokenExpired(string token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var handler = new JwtSecurityTokenHandler();
|
||||||
|
var jwt = handler.ReadJwtToken(token);
|
||||||
|
// Add 30-second buffer to avoid edge-case race
|
||||||
|
return jwt.ValidTo < DateTime.UtcNow.AddSeconds(-30);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Malformed token — treat as expired
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace BackOffice.Common.Utilities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// خروجی سازگار با Excel فارسی:
|
||||||
|
/// - XML Spreadsheet (.xls) برای ستونبندی مطمئن + متن فارسی
|
||||||
|
/// - CSV با جداکننده ; بهعنوان جایگزین سبک
|
||||||
|
/// </summary>
|
||||||
|
public static class CsvExportHelper
|
||||||
|
{
|
||||||
|
/// <summary>جداکننده پیشفرض ویندوز فارسی برای CSV.</summary>
|
||||||
|
public const char Separator = ';';
|
||||||
|
|
||||||
|
public static string Escape(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
// همیشه quote تا Excel فارسی ستون را نشکند
|
||||||
|
return $"\"{value.Replace("\"", "\"\"")}\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Cell(object? value) => value switch
|
||||||
|
{
|
||||||
|
null => Escape(string.Empty),
|
||||||
|
string s => Escape(s),
|
||||||
|
bool b => b ? "1" : "0",
|
||||||
|
byte or sbyte or short or ushort or int or uint or long or ulong or float or double or decimal
|
||||||
|
=> Convert.ToString(value, CultureInfo.InvariantCulture) ?? "0",
|
||||||
|
IFormattable f => Escape(f.ToString(null, CultureInfo.InvariantCulture) ?? string.Empty),
|
||||||
|
_ => Escape(Convert.ToString(value, CultureInfo.InvariantCulture))
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string Row(params object?[] cells) =>
|
||||||
|
string.Join(Separator, cells.Select(Cell));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نام نمایشی مشتری؛ اگر خالی باشد موبایل یا شناسه کاربر.
|
||||||
|
/// </summary>
|
||||||
|
public static string CustomerName(string? fullName, string? mobile = null, long? userId = null)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(fullName))
|
||||||
|
return fullName.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(mobile))
|
||||||
|
return mobile.Trim();
|
||||||
|
if (userId is > 0)
|
||||||
|
return $"کاربر {userId}";
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToBase64(StringBuilder body)
|
||||||
|
{
|
||||||
|
// CRLF صریح — Excel روی ویندوز با \n لینوکس گاهی بههم میریزد
|
||||||
|
var text = body.ToString().Replace("\r\n", "\n").Replace("\r", "\n").Replace("\n", "\r\n");
|
||||||
|
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(text)).ToArray();
|
||||||
|
return Convert.ToBase64String(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// خروجی SpreadsheetML که Excel بهصورت ستونستون و با UTF-8 درست باز میکند.
|
||||||
|
/// فایل را با پسوند .xls ذخیره کنید.
|
||||||
|
/// </summary>
|
||||||
|
public static string ToExcelXmlBase64(string sheetName, IReadOnlyList<string> headers, IEnumerable<object?[]> rows)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine(@"<?xml version=""1.0"" encoding=""UTF-8""?>");
|
||||||
|
sb.AppendLine(@"<?mso-application progid=""Excel.Sheet""?>");
|
||||||
|
sb.AppendLine(@"<Workbook xmlns=""urn:schemas-microsoft-com:office:spreadsheet""");
|
||||||
|
sb.AppendLine(@" xmlns:o=""urn:schemas-microsoft-com:office:office""");
|
||||||
|
sb.AppendLine(@" xmlns:x=""urn:schemas-microsoft-com:office:excel""");
|
||||||
|
sb.AppendLine(@" xmlns:ss=""urn:schemas-microsoft-com:office:spreadsheet""");
|
||||||
|
sb.AppendLine(@" xmlns:html=""http://www.w3.org/TR/REC-html40"">");
|
||||||
|
sb.AppendLine(@"<Styles>");
|
||||||
|
sb.AppendLine(@"<Style ss:ID=""Header""><Font ss:Bold=""1""/></Style>");
|
||||||
|
sb.AppendLine(@"<Style ss:ID=""Text""><NumberFormat ss:Format=""@""/></Style>");
|
||||||
|
sb.AppendLine(@"</Styles>");
|
||||||
|
sb.Append("<Worksheet ss:Name=\"").Append(XmlEscape(sheetName)).AppendLine("\">");
|
||||||
|
sb.AppendLine("<Table>");
|
||||||
|
|
||||||
|
sb.AppendLine("<Row>");
|
||||||
|
foreach (var header in headers)
|
||||||
|
{
|
||||||
|
sb.Append("<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">")
|
||||||
|
.Append(XmlEscape(header))
|
||||||
|
.AppendLine("</Data></Cell>");
|
||||||
|
}
|
||||||
|
sb.AppendLine("</Row>");
|
||||||
|
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
sb.AppendLine("<Row>");
|
||||||
|
foreach (var cell in row)
|
||||||
|
AppendExcelCell(sb, cell);
|
||||||
|
sb.AppendLine("</Row>");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine("</Table>");
|
||||||
|
sb.AppendLine("</Worksheet>");
|
||||||
|
sb.AppendLine("</Workbook>");
|
||||||
|
|
||||||
|
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
|
||||||
|
return Convert.ToBase64String(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendExcelCell(StringBuilder sb, object? value)
|
||||||
|
{
|
||||||
|
switch (value)
|
||||||
|
{
|
||||||
|
case null:
|
||||||
|
sb.AppendLine(@"<Cell ss:StyleID=""Text""><Data ss:Type=""String""></Data></Cell>");
|
||||||
|
break;
|
||||||
|
case string s:
|
||||||
|
sb.Append(@"<Cell ss:StyleID=""Text""><Data ss:Type=""String"">")
|
||||||
|
.Append(XmlEscape(s))
|
||||||
|
.AppendLine("</Data></Cell>");
|
||||||
|
break;
|
||||||
|
case bool b:
|
||||||
|
sb.Append(@"<Cell><Data ss:Type=""Boolean"">")
|
||||||
|
.Append(b ? "1" : "0")
|
||||||
|
.AppendLine("</Data></Cell>");
|
||||||
|
break;
|
||||||
|
case byte or sbyte or short or ushort or int or uint or long or ulong:
|
||||||
|
sb.Append(@"<Cell><Data ss:Type=""Number"">")
|
||||||
|
.Append(Convert.ToString(value, CultureInfo.InvariantCulture))
|
||||||
|
.AppendLine("</Data></Cell>");
|
||||||
|
break;
|
||||||
|
case float or double or decimal:
|
||||||
|
sb.Append(@"<Cell><Data ss:Type=""Number"">")
|
||||||
|
.Append(Convert.ToString(value, CultureInfo.InvariantCulture))
|
||||||
|
.AppendLine("</Data></Cell>");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
sb.Append(@"<Cell ss:StyleID=""Text""><Data ss:Type=""String"">")
|
||||||
|
.Append(XmlEscape(Convert.ToString(value, CultureInfo.InvariantCulture)))
|
||||||
|
.AppendLine("</Data></Cell>");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string XmlEscape(string? value) =>
|
||||||
|
WebUtility.HtmlEncode(value ?? string.Empty);
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using DateTimeConverterCL;
|
||||||
|
|
||||||
|
namespace BackOffice.Common.Utilities;
|
||||||
|
|
||||||
|
/// <summary>ساخت HTML فاکتور برای چاپ مرورگر (jsHtmlToPdf).</summary>
|
||||||
|
public static class OrderInvoicePrint
|
||||||
|
{
|
||||||
|
public sealed class InvoiceModel
|
||||||
|
{
|
||||||
|
public string ShopTitle { get; set; } = "فروشگاه";
|
||||||
|
public string OrderId { get; set; } = "";
|
||||||
|
public string? CustomerName { get; set; }
|
||||||
|
public string? CustomerMobile { get; set; }
|
||||||
|
public string? CustomerNationalCode { get; set; }
|
||||||
|
public string? Address { get; set; }
|
||||||
|
public string? PostalCode { get; set; }
|
||||||
|
public string? PaymentStatus { get; set; }
|
||||||
|
public string? PaymentMethod { get; set; }
|
||||||
|
public string? TransactionId { get; set; }
|
||||||
|
public string? TrackingCode { get; set; }
|
||||||
|
public DateTime? Created { get; set; }
|
||||||
|
public DateTime? PaymentDate { get; set; }
|
||||||
|
public long Subtotal { get; set; }
|
||||||
|
public long DiscountTotal { get; set; }
|
||||||
|
public long CreditUsed { get; set; }
|
||||||
|
public long GatewayAmount { get; set; }
|
||||||
|
public long VatAmount { get; set; }
|
||||||
|
public long TotalAmount { get; set; }
|
||||||
|
public List<InvoiceLine> Lines { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class InvoiceLine
|
||||||
|
{
|
||||||
|
public string Title { get; set; } = "";
|
||||||
|
public int Quantity { get; set; }
|
||||||
|
public long UnitPrice { get; set; }
|
||||||
|
public long Discount { get; set; }
|
||||||
|
public long LineTotal { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string BuildHtml(InvoiceModel model)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.Append("<!DOCTYPE html><html lang=\"fa\" dir=\"rtl\"><head><meta charset=\"utf-8\"/>");
|
||||||
|
sb.Append("<title>").Append(Esc($"فاکتور {model.OrderId}")).Append("</title>");
|
||||||
|
sb.Append("""
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { font-family: Tahoma, "Segoe UI", sans-serif; color: #111; margin: 24px; font-size: 13px; }
|
||||||
|
h1 { font-size: 20px; margin: 0 0 4px; }
|
||||||
|
.muted { color: #555; }
|
||||||
|
.header { display: flex; justify-content: space-between; align-items: flex-start; border-bottom: 2px solid #222; padding-bottom: 12px; margin-bottom: 16px; }
|
||||||
|
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 24px; margin-bottom: 16px; }
|
||||||
|
.label { color: #666; font-size: 11px; }
|
||||||
|
.value { font-weight: 600; }
|
||||||
|
table { width: 100%; border-collapse: collapse; margin: 12px 0 16px; }
|
||||||
|
th, td { border: 1px solid #ccc; padding: 8px; text-align: right; }
|
||||||
|
th { background: #f3f3f3; }
|
||||||
|
.num { text-align: left; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
||||||
|
.totals { width: 320px; margin-right: auto; }
|
||||||
|
.totals td { border: none; padding: 4px 0; }
|
||||||
|
.totals .grand { font-size: 15px; font-weight: 700; border-top: 2px solid #222; padding-top: 8px; }
|
||||||
|
.footer { margin-top: 28px; font-size: 11px; color: #666; border-top: 1px dashed #bbb; padding-top: 8px; }
|
||||||
|
@media print {
|
||||||
|
body { margin: 0; }
|
||||||
|
.no-print { display: none !important; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
""");
|
||||||
|
sb.Append("</head><body>");
|
||||||
|
sb.Append("<div class=\"header\"><div>");
|
||||||
|
sb.Append("<h1>").Append(Esc(model.ShopTitle)).Append("</h1>");
|
||||||
|
sb.Append("<div class=\"muted\">فاکتور فروش</div></div><div>");
|
||||||
|
sb.Append("<div><span class=\"label\">شماره فاکتور: </span><span class=\"value\">").Append(Esc(model.OrderId)).Append("</span></div>");
|
||||||
|
if (model.Created.HasValue)
|
||||||
|
sb.Append("<div><span class=\"label\">تاریخ ثبت: </span>").Append(Esc(FormatDate(model.Created.Value))).Append("</div>");
|
||||||
|
if (model.PaymentDate.HasValue)
|
||||||
|
sb.Append("<div><span class=\"label\">تاریخ پرداخت: </span>").Append(Esc(FormatDate(model.PaymentDate.Value))).Append("</div>");
|
||||||
|
sb.Append("</div></div>");
|
||||||
|
|
||||||
|
sb.Append("<div class=\"grid\">");
|
||||||
|
AppendField(sb, "نام مشتری", model.CustomerName);
|
||||||
|
AppendField(sb, "موبایل", model.CustomerMobile);
|
||||||
|
AppendField(sb, "کد ملی", model.CustomerNationalCode);
|
||||||
|
AppendField(sb, "کد پستی", model.PostalCode);
|
||||||
|
AppendField(sb, "وضعیت پرداخت", model.PaymentStatus);
|
||||||
|
AppendField(sb, "روش پرداخت", model.PaymentMethod);
|
||||||
|
AppendField(sb, "کد تراکنش", model.TransactionId);
|
||||||
|
AppendField(sb, "کد رهگیری", model.TrackingCode);
|
||||||
|
if (!string.IsNullOrWhiteSpace(model.Address))
|
||||||
|
{
|
||||||
|
sb.Append("<div style=\"grid-column:1/-1\"><div class=\"label\">آدرس</div><div class=\"value\">")
|
||||||
|
.Append(Esc(model.Address)).Append("</div></div>");
|
||||||
|
}
|
||||||
|
sb.Append("</div>");
|
||||||
|
|
||||||
|
sb.Append("<table><thead><tr>");
|
||||||
|
sb.Append("<th>#</th><th>کالا</th><th>تعداد</th><th>قیمت واحد</th><th>تخفیف</th><th>مبلغ</th>");
|
||||||
|
sb.Append("</tr></thead><tbody>");
|
||||||
|
var i = 1;
|
||||||
|
foreach (var line in model.Lines)
|
||||||
|
{
|
||||||
|
sb.Append("<tr>");
|
||||||
|
sb.Append("<td class=\"num\">").Append(i++).Append("</td>");
|
||||||
|
sb.Append("<td>").Append(Esc(line.Title)).Append("</td>");
|
||||||
|
sb.Append("<td class=\"num\">").Append(line.Quantity).Append("</td>");
|
||||||
|
sb.Append("<td class=\"num\">").Append(Money(line.UnitPrice)).Append("</td>");
|
||||||
|
sb.Append("<td class=\"num\">").Append(Money(line.Discount)).Append("</td>");
|
||||||
|
sb.Append("<td class=\"num\">").Append(Money(line.LineTotal)).Append("</td>");
|
||||||
|
sb.Append("</tr>");
|
||||||
|
}
|
||||||
|
if (model.Lines.Count == 0)
|
||||||
|
sb.Append("<tr><td colspan=\"6\" class=\"muted\">آیتمی ثبت نشده است</td></tr>");
|
||||||
|
sb.Append("</tbody></table>");
|
||||||
|
|
||||||
|
sb.Append("<table class=\"totals\"><tbody>");
|
||||||
|
if (model.Subtotal > 0)
|
||||||
|
AppendTotalRow(sb, "جمع اقلام", model.Subtotal);
|
||||||
|
if (model.DiscountTotal > 0)
|
||||||
|
AppendTotalRow(sb, "تخفیف", model.DiscountTotal);
|
||||||
|
if (model.VatAmount > 0)
|
||||||
|
AppendTotalRow(sb, "مالیات بر ارزش افزوده", model.VatAmount);
|
||||||
|
if (model.CreditUsed > 0)
|
||||||
|
AppendTotalRow(sb, "پرداخت از کیف اعتباری", model.CreditUsed);
|
||||||
|
if (model.GatewayAmount > 0)
|
||||||
|
AppendTotalRow(sb, "پرداخت درگاه / نقدی", model.GatewayAmount);
|
||||||
|
sb.Append("<tr class=\"grand\"><td>مبلغ قابل پرداخت</td><td class=\"num\">")
|
||||||
|
.Append(Money(model.TotalAmount)).Append(" تومان</td></tr>");
|
||||||
|
sb.Append("</tbody></table>");
|
||||||
|
|
||||||
|
sb.Append("<div class=\"footer\">چاپشده از پنل ادمین — ").Append(Esc(FormatDate(DateTime.Now))).Append("</div>");
|
||||||
|
sb.Append("</body></html>");
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendField(StringBuilder sb, string label, string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value)) return;
|
||||||
|
sb.Append("<div><div class=\"label\">").Append(Esc(label)).Append("</div><div class=\"value\">")
|
||||||
|
.Append(Esc(value)).Append("</div></div>");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendTotalRow(StringBuilder sb, string label, long amount)
|
||||||
|
{
|
||||||
|
sb.Append("<tr><td>").Append(Esc(label)).Append("</td><td class=\"num\">")
|
||||||
|
.Append(Money(amount)).Append(" تومان</td></tr>");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Money(long amount) => amount.ToString("N0", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
private static string FormatDate(DateTime dt)
|
||||||
|
{
|
||||||
|
try { return dt.ToLocalTime().MiladiToJalaliWithTime(); }
|
||||||
|
catch { return dt.ToLocalTime().ToString("yyyy/MM/dd HH:mm"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Esc(string? s) => WebUtility.HtmlEncode(s ?? "");
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
namespace BackOffice.Common.Utilities;
|
||||||
|
|
||||||
|
using CMSMicroservice.Protobuf.Protos;
|
||||||
|
using MudBlazor;
|
||||||
|
|
||||||
|
public static class ReportLabels
|
||||||
|
{
|
||||||
|
public static string GetPaymentStatusLabel(bool success) => success ? "موفق" : "در انتظار / ناموفق";
|
||||||
|
|
||||||
|
public static string GetTransactionPaymentStatusLabel(PaymentStatus? status) => status switch
|
||||||
|
{
|
||||||
|
PaymentStatus.Success => "موفق",
|
||||||
|
PaymentStatus.Pending => "در انتظار",
|
||||||
|
PaymentStatus.Reject => "رد شده",
|
||||||
|
_ => "—"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static Color GetTransactionPaymentStatusColor(PaymentStatus? status) => status switch
|
||||||
|
{
|
||||||
|
PaymentStatus.Success => Color.Success,
|
||||||
|
PaymentStatus.Pending => Color.Warning,
|
||||||
|
PaymentStatus.Reject => Color.Error,
|
||||||
|
_ => Color.Default
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string GetTransactionTypeLabel(TransactionType? type) => type switch
|
||||||
|
{
|
||||||
|
TransactionType.Buy => "خرید",
|
||||||
|
TransactionType.DepositIpg => "واریز درگاه",
|
||||||
|
TransactionType.DepositExternal1 => "واریز خارجی",
|
||||||
|
TransactionType.Withdraw => "برداشت",
|
||||||
|
TransactionType.NetworkCommission => "کمیسیون شبکه",
|
||||||
|
TransactionType.ClubActivation => "فعالسازی باشگاه",
|
||||||
|
TransactionType.DiscountWalletCharge => "شارژ کیف تخفیف",
|
||||||
|
TransactionType.DiscountShopPurchase => "خرید فروشگاه تخفیف",
|
||||||
|
TransactionType.MagicWalletDeposit => "واریز کیف جادویی",
|
||||||
|
TransactionType.MagicWalletBonus => "بونوس کیف جادویی",
|
||||||
|
TransactionType.CreditWalletCharge => "شارژ کیف اصلی",
|
||||||
|
TransactionType.ManualCreditWalletCharge => "شارژ دستی کیف اصلی",
|
||||||
|
_ => type?.ToString() ?? "—"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string GetPurchaseMethodLabel(int method) => method switch
|
||||||
|
{
|
||||||
|
1 => "دایا",
|
||||||
|
2 => "درگاه",
|
||||||
|
3 => "دستی",
|
||||||
|
_ => "نامشخص"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string GetClubMembershipActionLabel(int action) => action switch
|
||||||
|
{
|
||||||
|
0 => "فعالسازی",
|
||||||
|
1 => "غیرفعالسازی",
|
||||||
|
2 => "ویرایش",
|
||||||
|
3 => "اصلاح دستی",
|
||||||
|
_ => action.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string GetClubCycleActionLabel(int action) => action switch
|
||||||
|
{
|
||||||
|
0 => "شروع دوره",
|
||||||
|
1 => "ورود به حالت جادویی",
|
||||||
|
2 => "پایان حالت جادویی",
|
||||||
|
3 => "تکمیل دوره",
|
||||||
|
4 => "لغو",
|
||||||
|
5 => "اصلاح دستی",
|
||||||
|
_ => action.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string GetMagicStateLabel(int? magicStarted, DateTime? started, DateTime? completed)
|
||||||
|
{
|
||||||
|
if (completed.HasValue) return "جادویی تکمیلشده";
|
||||||
|
if (started.HasValue) return "در حالت جادویی";
|
||||||
|
return "عادی";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string FormatActiveChange(bool oldActive, bool newActive) =>
|
||||||
|
$"{(oldActive ? "فعال" : "غیرفعال")} → {(newActive ? "فعال" : "غیرفعال")}";
|
||||||
|
|
||||||
|
public static string FormatBoolChange(bool oldValue, bool newValue) =>
|
||||||
|
$"{(oldValue ? "بله" : "خیر")} → {(newValue ? "بله" : "خیر")}";
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ public static class RouteConstance
|
|||||||
public const string UserPage = "/UserPage/";
|
public const string UserPage = "/UserPage/";
|
||||||
public const string UserOrder = "/UserOrderPage/";
|
public const string UserOrder = "/UserOrderPage/";
|
||||||
public const string Orders = "/OrdersPage/";
|
public const string Orders = "/OrdersPage/";
|
||||||
|
public const string OrdersSalesReports = "/OrdersSalesReportsPage/";
|
||||||
public const string UserRole = "/UserRolePage/";
|
public const string UserRole = "/UserRolePage/";
|
||||||
public const string UserAddress = "/UserAddressPage/";
|
public const string UserAddress = "/UserAddressPage/";
|
||||||
public const string Products = "/ProductsPage/";
|
public const string Products = "/ProductsPage/";
|
||||||
@@ -17,4 +18,11 @@ public static class RouteConstance
|
|||||||
public const string ProductCategories = "/ProductCategoriesPage/";
|
public const string ProductCategories = "/ProductCategoriesPage/";
|
||||||
public const string ProductsBulkEdit = "/ProductsBulkEditPage/";
|
public const string ProductsBulkEdit = "/ProductsBulkEditPage/";
|
||||||
public const string CategoryProducts = "/CategoryProductsPage/";
|
public const string CategoryProducts = "/CategoryProductsPage/";
|
||||||
|
|
||||||
|
// Inventory Management
|
||||||
|
public const string Inventory = "/InventoryPage/";
|
||||||
|
public const string InventoryLowStock = "/InventoryLowStockPage/";
|
||||||
|
public const string InventoryWarehouses = "/InventoryWarehousesPage/";
|
||||||
|
public const string InventoryAddStock = "/InventoryAddStockPage/";
|
||||||
|
public const string InventoryMovements = "/InventoryMovementsPage/";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
FROM 194.5.195.53:32082/dotnet/sdk:9.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# Copy NuGet config and project file
|
||||||
|
COPY ["BackOffice/NuGet.config", "NuGet.config"]
|
||||||
|
COPY ["BackOffice/BackOffice.csproj", "BackOffice/"]
|
||||||
|
|
||||||
|
# Restore dependencies
|
||||||
|
RUN dotnet restore "BackOffice/BackOffice.csproj" --configfile NuGet.config
|
||||||
|
|
||||||
|
# Copy all source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build and publish
|
||||||
|
WORKDIR "/src/BackOffice"
|
||||||
|
RUN dotnet publish "BackOffice.csproj" -c Release -o /app/publish
|
||||||
|
|
||||||
|
# Runtime stage - nginx for Blazor WebAssembly
|
||||||
|
FROM 194.5.195.53:32082/nginx:alpine AS final
|
||||||
|
WORKDIR /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Remove default nginx files
|
||||||
|
RUN rm -rf /usr/share/nginx/html/*
|
||||||
|
|
||||||
|
# Copy published wwwroot (Blazor WASM output)
|
||||||
|
COPY --from=build /app/publish/wwwroot .
|
||||||
|
|
||||||
|
# Copy entrypoint script that sets blazor-environment header
|
||||||
|
COPY ["BackOffice/docker-entrypoint.sh", "/docker-entrypoint.sh"]
|
||||||
|
RUN chmod +x /docker-entrypoint.sh
|
||||||
|
|
||||||
|
# Default Blazor environment (override via K8s env var BLAZOR_ENVIRONMENT)
|
||||||
|
ENV BLAZOR_ENVIRONMENT=Staging
|
||||||
|
# Default gateway URL for gRPC calls (injected into appsettings at startup)
|
||||||
|
ENV GW_URL=https://cms.kbs2.ir
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["/docker-entrypoint.sh"]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<clear />
|
||||||
|
<!-- Local packages (dev builds) -->
|
||||||
|
<!-- <add key="Local" value="/home/masoud/Apps/project/FourSat/nupkg" /> -->
|
||||||
|
<!-- Nexus (hosts FourSat packages + proxies nuget.org) -->
|
||||||
|
<add key="Nexus" value="http://194.5.195.53:32081/repository/nuget-all/index.json" allowInsecureConnections="true" />
|
||||||
|
</packageSources>
|
||||||
|
<packageSourceCredentials>
|
||||||
|
<Nexus>
|
||||||
|
<add key="Username" value="admin" />
|
||||||
|
<add key="ClearTextPassword" value="87zH26nbqT" />
|
||||||
|
</Nexus>
|
||||||
|
</packageSourceCredentials>
|
||||||
|
</configuration>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@using BackOffice.BFF.Category.Protobuf.Protos.Category
|
@using CMSMicroservice.Protobuf.Protos.Category
|
||||||
|
|
||||||
<MudAutocomplete T="GetAllCategoryByFilterResponseModel"
|
<MudAutocomplete T="GetAllCategoryByFilterResponseModel"
|
||||||
Label="@Label"
|
Label="@Label"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using BackOffice.BFF.Category.Protobuf.Protos.Category;
|
using CMSMicroservice.Protobuf.Protos;
|
||||||
|
using CMSMicroservice.Protobuf.Protos.Category;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
namespace BackOffice.Pages.AutoComplete;
|
namespace BackOffice.Pages.AutoComplete;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@using BackOffice.BFF.Category.Protobuf.Protos.Category
|
@using CMSMicroservice.Protobuf.Protos.Category
|
||||||
|
|
||||||
<MudAutocomplete T="GetAllCategoryByFilterResponseModel"
|
<MudAutocomplete T="GetAllCategoryByFilterResponseModel"
|
||||||
Label="@Label"
|
Label="@Label"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using BackOffice.BFF.Category.Protobuf.Protos.Category;
|
using CMSMicroservice.Protobuf.Protos;
|
||||||
|
using CMSMicroservice.Protobuf.Protos.Category;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
namespace BackOffice.Pages.AutoComplete;
|
namespace BackOffice.Pages.AutoComplete;
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
@using BackOffice.BFF.Category.Protobuf.Protos.Category
|
@using CMSMicroservice.Protobuf.Protos.Category
|
||||||
|
|
||||||
<MudSelect T="long"
|
<MudSelect T="long"
|
||||||
@ref="_selectRef"
|
@ref="_selectRef"
|
||||||
Label="@Label"
|
Label="@Label"
|
||||||
Variant="Variant.Outlined"
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense"
|
||||||
Disabled="@Disabled"
|
Disabled="@Disabled"
|
||||||
MultiSelection="true"
|
MultiSelection="true"
|
||||||
|
MultiSelectionTextFunc="@GetSelectedText"
|
||||||
SelectedValues="_internalSelectedIds"
|
SelectedValues="_internalSelectedIds"
|
||||||
SelectedValuesChanged="@(_ => OnSelectedValuesChangedAsync())">
|
SelectedValuesChanged="OnSelectedValuesChangedAsync">
|
||||||
@foreach (var item in _items)
|
@foreach (var item in _items)
|
||||||
{
|
{
|
||||||
<MudSelectItem T="long" Value="@item.Id">
|
<MudSelectItem T="long" Value="@item.Id">
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BackOffice.BFF.Category.Protobuf.Protos.Category;
|
using CMSMicroservice.Protobuf.Protos;
|
||||||
|
using CMSMicroservice.Protobuf.Protos.Category;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
namespace BackOffice.Pages.AutoComplete;
|
namespace BackOffice.Pages.AutoComplete;
|
||||||
@@ -61,11 +62,9 @@ public partial class CategoryMultiSelectCombo
|
|||||||
: new HashSet<long>();
|
: new HashSet<long>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnSelectedValuesChangedAsync()
|
private async Task OnSelectedValuesChangedAsync(IEnumerable<long> values)
|
||||||
{
|
{
|
||||||
var current = _selectRef?.SelectedValues ?? System.Array.Empty<long>();
|
_internalSelectedIds = values.Where(id => id > 0).Distinct().ToHashSet();
|
||||||
|
|
||||||
_internalSelectedIds = current.Where(id => id > 0).Distinct().ToHashSet();
|
|
||||||
SelectedIds = _internalSelectedIds.ToList();
|
SelectedIds = _internalSelectedIds.ToList();
|
||||||
|
|
||||||
if (SelectedIdsChanged.HasDelegate)
|
if (SelectedIdsChanged.HasDelegate)
|
||||||
@@ -73,5 +72,27 @@ public partial class CategoryMultiSelectCombo
|
|||||||
await SelectedIdsChanged.InvokeAsync(SelectedIds);
|
await SelectedIdsChanged.InvokeAsync(SelectedIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GetSelectedText(List<string> selectedValues)
|
||||||
|
{
|
||||||
|
if (selectedValues == null || selectedValues.Count == 0)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var titles = new List<string>();
|
||||||
|
foreach (var val in selectedValues)
|
||||||
|
{
|
||||||
|
if (long.TryParse(val, out var id))
|
||||||
|
{
|
||||||
|
var item = _items.FirstOrDefault(x => x.Id == id);
|
||||||
|
titles.Add(item != null ? item.Title : val);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
titles.Add(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join("، ", titles);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
@using BackOffice.Services.DiscountCategory
|
||||||
|
|
||||||
|
<MudSelect @ref="_selectRef"
|
||||||
|
T="long"
|
||||||
|
MultiSelection="true"
|
||||||
|
MultiSelectionTextFunc="@GetSelectedText"
|
||||||
|
SelectedValues="_internalSelectedIds"
|
||||||
|
SelectedValuesChanged="OnSelectedValuesChangedAsync"
|
||||||
|
Label="@Label"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense"
|
||||||
|
Disabled="@Disabled">
|
||||||
|
@foreach (var item in _items)
|
||||||
|
{
|
||||||
|
<MudSelectItem T="long" Value="@item.Id">@item.Title</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
using BackOffice.Services.DiscountCategory;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using MudBlazor;
|
||||||
|
|
||||||
|
namespace BackOffice.Pages.AutoComplete;
|
||||||
|
|
||||||
|
public partial class DiscountCategoryMultiSelectCombo
|
||||||
|
{
|
||||||
|
[Inject] public IDiscountCategoryService CategoryService { get; set; } = default!;
|
||||||
|
|
||||||
|
[Parameter] public List<long> SelectedIds { get; set; } = new();
|
||||||
|
[Parameter] public EventCallback<List<long>> SelectedIdsChanged { get; set; }
|
||||||
|
[Parameter] public string? Label { get; set; } = "انتخاب دستهبندیها";
|
||||||
|
[Parameter] public bool Disabled { get; set; }
|
||||||
|
|
||||||
|
private List<DiscountCategoryDto> _items = new();
|
||||||
|
private HashSet<long> _internalSelectedIds = new();
|
||||||
|
private MudSelect<long>? _selectRef;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
await LoadCategoriesAsync();
|
||||||
|
SyncInternalFromParameter();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task OnParametersSetAsync()
|
||||||
|
{
|
||||||
|
SyncInternalFromParameter();
|
||||||
|
return base.OnParametersSetAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadCategoriesAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tree = await CategoryService.GetCategoriesAsync(isActive: true);
|
||||||
|
_items = FlattenCategories(tree ?? new List<DiscountCategoryDto>());
|
||||||
|
|
||||||
|
// Log first few items
|
||||||
|
if (_items.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var item in _items.Take(5))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (ex.InnerException != null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
_items = new List<DiscountCategoryDto>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<DiscountCategoryDto> FlattenCategories(List<DiscountCategoryDto> categories, string prefix = "")
|
||||||
|
{
|
||||||
|
var result = new List<DiscountCategoryDto>();
|
||||||
|
foreach (var category in categories)
|
||||||
|
{
|
||||||
|
var catCopy = new DiscountCategoryDto
|
||||||
|
{
|
||||||
|
Id = category.Id,
|
||||||
|
Title = prefix + category.Title,
|
||||||
|
Name = category.Name,
|
||||||
|
ParentCategoryId = category.ParentCategoryId,
|
||||||
|
IsActive = category.IsActive
|
||||||
|
};
|
||||||
|
result.Add(catCopy);
|
||||||
|
|
||||||
|
if (category.Children?.Any() == true)
|
||||||
|
{
|
||||||
|
result.AddRange(FlattenCategories(category.Children.ToList(), prefix + " "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SyncInternalFromParameter()
|
||||||
|
{
|
||||||
|
_internalSelectedIds = SelectedIds != null
|
||||||
|
? SelectedIds.Where(id => id > 0).Distinct().ToHashSet()
|
||||||
|
: new HashSet<long>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnSelectedValuesChangedAsync(IEnumerable<long> values)
|
||||||
|
{
|
||||||
|
_internalSelectedIds = values.Where(id => id > 0).Distinct().ToHashSet();
|
||||||
|
SelectedIds = _internalSelectedIds.ToList();
|
||||||
|
|
||||||
|
if (SelectedIdsChanged.HasDelegate)
|
||||||
|
{
|
||||||
|
await SelectedIdsChanged.InvokeAsync(SelectedIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetSelectedText(List<string> selectedValues)
|
||||||
|
{
|
||||||
|
if (selectedValues == null || selectedValues.Count == 0)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var titles = new List<string>();
|
||||||
|
foreach (var val in selectedValues)
|
||||||
|
{
|
||||||
|
if (long.TryParse(val, out var id))
|
||||||
|
{
|
||||||
|
var item = _items.FirstOrDefault(x => x.Id == id);
|
||||||
|
titles.Add(item != null ? item.Title : val);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
titles.Add(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join("، ", titles);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
@using CMSMicroservice.Protobuf.Protos.DiscountProduct
|
||||||
|
|
||||||
|
<MudAutocomplete T="DiscountProductDto"
|
||||||
|
Label="@Label"
|
||||||
|
Value="@_item"
|
||||||
|
DebounceInterval="700"
|
||||||
|
ValueChanged="@((e) => OnSelected(e))"
|
||||||
|
ToStringFunc="@(e => e == null ? null : e.Title)"
|
||||||
|
SearchFunc="@Search"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Clearable="true"
|
||||||
|
ShowProgressIndicator="true"
|
||||||
|
CoerceText="false"
|
||||||
|
OnClearButtonClick="()=> OnSelected(null)" />
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace BackOffice.Pages.AutoComplete;
|
||||||
|
|
||||||
|
public partial class DiscountProductsAutoComplete
|
||||||
|
{
|
||||||
|
[Inject] public DiscountProductContract.DiscountProductContractClient DiscountProductService { get; set; } = default!;
|
||||||
|
|
||||||
|
[Parameter] public long? Value { get; set; }
|
||||||
|
[Parameter] public string? Label { get; set; } = "محصول تخفیفی";
|
||||||
|
[Parameter] public EventCallback<long?> ValueChanged { get; set; }
|
||||||
|
|
||||||
|
private List<DiscountProductDto> _items = new();
|
||||||
|
private DiscountProductDto? _item;
|
||||||
|
|
||||||
|
protected override async void OnParametersSet()
|
||||||
|
{
|
||||||
|
await base.OnParametersSetAsync();
|
||||||
|
|
||||||
|
if (Value.HasValue && Value.Value > 0 && (_item == null || _item.Id != Value.Value))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var getAll = await DiscountProductService.GetDiscountProductsAsync(new GetDiscountProductsRequest
|
||||||
|
{
|
||||||
|
PageNumber = 1,
|
||||||
|
PageSize = 50
|
||||||
|
});
|
||||||
|
|
||||||
|
if (getAll?.Models != null)
|
||||||
|
{
|
||||||
|
_item = getAll.Models.FirstOrDefault(x => x.Id == Value.Value);
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Silently handle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnSelected(DiscountProductDto? model)
|
||||||
|
{
|
||||||
|
if (model == null)
|
||||||
|
{
|
||||||
|
_item = null;
|
||||||
|
await ValueChanged.InvokeAsync(null);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_item = model;
|
||||||
|
await ValueChanged.InvokeAsync(model.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IEnumerable<DiscountProductDto>> Search(string value, CancellationToken token)
|
||||||
|
{
|
||||||
|
var request = new GetDiscountProductsRequest
|
||||||
|
{
|
||||||
|
PageNumber = 1,
|
||||||
|
PageSize = 9
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
request.SearchQuery = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var getAll = await DiscountProductService.GetDiscountProductsAsync(request);
|
||||||
|
_items = getAll?.Models?.ToList() ?? new List<DiscountProductDto>();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_items = new List<DiscountProductDto>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _items;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
@using CMSMicroservice.Protobuf.Protos.Package
|
||||||
|
|
||||||
|
<MudSelect T="long?"
|
||||||
|
Label="@Label"
|
||||||
|
Value="@SelectedPackageId"
|
||||||
|
ValueChanged="OnValueChanged"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense"
|
||||||
|
Clearable="true"
|
||||||
|
Style="min-width: 160px;">
|
||||||
|
<MudSelectItem T="long?" Value="@((long?)null)">همه پکیجها</MudSelectItem>
|
||||||
|
@foreach (var pkg in _packages)
|
||||||
|
{
|
||||||
|
<MudSelectItem T="long?" Value="@((long?)pkg.Id)">@pkg.Title</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using CMSMicroservice.Protobuf.Protos.Package;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace BackOffice.Pages.AutoComplete;
|
||||||
|
|
||||||
|
public partial class PackageSelect
|
||||||
|
{
|
||||||
|
[Inject] public PackageContract.PackageContractClient PackageClient { get; set; }
|
||||||
|
|
||||||
|
[Parameter] public string Label { get; set; } = "پکیج";
|
||||||
|
[Parameter] public long? SelectedPackageId { get; set; }
|
||||||
|
[Parameter] public EventCallback<long?> SelectedPackageIdChanged { get; set; }
|
||||||
|
|
||||||
|
private List<GetAllPackageByFilterResponseModel> _packages = new();
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await LoadPackages();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadPackages()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var request = new GetAllPackageByFilterRequest
|
||||||
|
{
|
||||||
|
PaginationState = new() { PageNumber = 1, PageSize = 100 },
|
||||||
|
Filter = new()
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await PackageClient.GetAllPackageByFilterAsync(request);
|
||||||
|
_packages = result.Models?.OrderBy(p => p.SortOrder).ToList()
|
||||||
|
?? new List<GetAllPackageByFilterResponseModel>();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_packages = new List<GetAllPackageByFilterResponseModel>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnValueChanged(long? value)
|
||||||
|
{
|
||||||
|
SelectedPackageId = value;
|
||||||
|
await SelectedPackageIdChanged.InvokeAsync(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@using BackOffice.BFF.Products.Protobuf.Protos.Products
|
@using CMSMicroservice.Protobuf.Protos.Products
|
||||||
|
|
||||||
<MudAutocomplete T="GetAllProductsByFilterResponseModel"
|
<MudAutocomplete T="GetAllProductsByFilterResponseModel"
|
||||||
Label="@Label"
|
Label="@Label"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using BackOffice.BFF.Products.Protobuf.Protos.Products;
|
using CMSMicroservice.Protobuf.Protos.Products;
|
||||||
|
using CMSMicroservice.Protobuf.Protos;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
namespace BackOffice.Pages.AutoComplete;
|
namespace BackOffice.Pages.AutoComplete;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@using BackOffice.BFF.Role.Protobuf.Protos.Role
|
@using CMSMicroservice.Protobuf.Protos.Role
|
||||||
|
|
||||||
<MudAutocomplete T="GetAllRoleByFilterResponseModel"
|
<MudAutocomplete T="GetAllRoleByFilterResponseModel"
|
||||||
Label="@Label"
|
Label="@Label"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using BackOffice.BFF.Role.Protobuf.Protos.Role;
|
using CMSMicroservice.Protobuf.Protos.Role;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
@using CMSMicroservice.Protobuf.Protos.User
|
||||||
|
|
||||||
|
<MudAutocomplete T="GetAllUserByFilterResponseModel"
|
||||||
|
Label="@Label"
|
||||||
|
Value="@_item"
|
||||||
|
DebounceInterval="500"
|
||||||
|
ValueChanged="@((e) => OnSelected(e))"
|
||||||
|
ToStringFunc="@(e=> e == null ? null : $"{e.FirstName} {e.LastName} ({e.Mobile})")"
|
||||||
|
SearchFunc="@Search"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Clearable="true"
|
||||||
|
ShowProgressIndicator="true"
|
||||||
|
CoerceText="false"
|
||||||
|
ResetValueOnEmptyText="true"
|
||||||
|
OnClearButtonClick="()=> OnSelected(null)"/>
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
using CMSMicroservice.Protobuf.Protos.User;
|
||||||
|
using Grpc.Core;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace BackOffice.Pages.AutoComplete;
|
||||||
|
|
||||||
|
public partial class UserAutoComplete
|
||||||
|
{
|
||||||
|
[Inject] public UserContract.UserContractClient UserContract { get; set; }
|
||||||
|
|
||||||
|
[Parameter] public string Label { get; set; } = "انتخاب کاربر";
|
||||||
|
[Parameter] public long? SelectedUserId { get; set; }
|
||||||
|
[Parameter] public EventCallback<long?> SelectedUserIdChanged { get; set; }
|
||||||
|
|
||||||
|
private GetAllUserByFilterResponseModel _item;
|
||||||
|
|
||||||
|
protected override async Task OnParametersSetAsync()
|
||||||
|
{
|
||||||
|
if (SelectedUserId.HasValue && SelectedUserId.Value > 0 && _item?.Id != SelectedUserId.Value)
|
||||||
|
{
|
||||||
|
await LoadUser(SelectedUserId.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IEnumerable<GetAllUserByFilterResponseModel>> Search(string value, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value) || value.Length < 2)
|
||||||
|
return new List<GetAllUserByFilterResponseModel>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var request = new GetAllUserByFilterRequest
|
||||||
|
{
|
||||||
|
PaginationState = new() { PageNumber = 1, PageSize = 100 },
|
||||||
|
Filter = new()
|
||||||
|
{
|
||||||
|
|
||||||
|
SearchText = value,
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await UserContract.GetAllUserByFilterAsync(request, cancellationToken: cancellationToken);
|
||||||
|
return response.Models?.ToList() ?? new List<GetAllUserByFilterResponseModel>();
|
||||||
|
}
|
||||||
|
catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled)
|
||||||
|
{
|
||||||
|
return new List<GetAllUserByFilterResponseModel>();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return new List<GetAllUserByFilterResponseModel>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnSelected(GetAllUserByFilterResponseModel selected)
|
||||||
|
{
|
||||||
|
_item = selected;
|
||||||
|
var userId = selected?.Id;
|
||||||
|
SelectedUserId = userId;
|
||||||
|
await SelectedUserIdChanged.InvokeAsync(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadUser(long userId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var request = new GetAllUserByFilterRequest
|
||||||
|
{
|
||||||
|
PaginationState = new() { PageNumber = 1, PageSize = 1 },
|
||||||
|
Filter = new()
|
||||||
|
{
|
||||||
|
Id = userId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await UserContract.GetAllUserByFilterAsync(request);
|
||||||
|
|
||||||
|
if (response.Models != null && response.Models.Count > 0)
|
||||||
|
{
|
||||||
|
_item = response.Models[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Silent fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
@using CMSMicroservice.Protobuf.Protos.Commission
|
||||||
|
|
||||||
|
<MudAutocomplete T="WeekInfo"
|
||||||
|
Label="@Label"
|
||||||
|
Value="@_selectedWeek"
|
||||||
|
DebounceInterval="300"
|
||||||
|
ValueChanged="@OnSelected"
|
||||||
|
ToStringFunc="@(e => e?.DisplayName ?? string.Empty)"
|
||||||
|
SearchFunc="@Search"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Clearable="true"
|
||||||
|
ShowProgressIndicator="true"
|
||||||
|
CoerceText="@CoerceText"
|
||||||
|
ResetValueOnEmptyText="true"
|
||||||
|
OnClearButtonClick="@OnClear">
|
||||||
|
<ItemTemplate Context="week">
|
||||||
|
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||||
|
<MudText>@week.DisplayText</MudText>
|
||||||
|
</MudStack>
|
||||||
|
</ItemTemplate>
|
||||||
|
</MudAutocomplete>
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
using BackOffice.Services;
|
||||||
|
using CMSMicroservice.Protobuf.Protos.Commission;
|
||||||
|
using Grpc.Core;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace BackOffice.Pages.AutoComplete;
|
||||||
|
|
||||||
|
public partial class WeekNumberPicker
|
||||||
|
{
|
||||||
|
[Inject] public CommissionContract.CommissionContractClient CommissionContract { get; set; } = default!;
|
||||||
|
[Inject] public IPersianDateTimeService PersianDateTime { get; set; } = default!;
|
||||||
|
|
||||||
|
[Parameter] public string Label { get; set; } = "انتخاب هفته";
|
||||||
|
|
||||||
|
// WeekDefinitionId based binding (preferred)
|
||||||
|
[Parameter] public long? SelectedWeekDefinitionId { get; set; }
|
||||||
|
[Parameter] public EventCallback<long?> SelectedWeekDefinitionIdChanged { get; set; }
|
||||||
|
|
||||||
|
// Legacy string-based binding (deprecated, kept for compatibility)
|
||||||
|
// [Parameter] public long? SelectedWeekNumber { get; set; }
|
||||||
|
// [Parameter] public EventCallback<long?> SelectedWeekNumberChanged { get; set; }
|
||||||
|
|
||||||
|
[Parameter] public bool CoerceText { get; set; } = true; // اجازه ورود دستی
|
||||||
|
[Parameter] public int FutureWeeksCount { get; set; } = 4;
|
||||||
|
[Parameter] public int PastWeeksCount { get; set; } = 12;
|
||||||
|
|
||||||
|
private WeekInfo? _selectedWeek;
|
||||||
|
private List<WeekInfo> _allWeeks = new();
|
||||||
|
private bool _isLoaded = false;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await LoadWeeks();
|
||||||
|
|
||||||
|
if (SelectedWeekDefinitionId.HasValue && SelectedWeekDefinitionId.Value > 0 && _allWeeks.Any())
|
||||||
|
{
|
||||||
|
_selectedWeek = _allWeeks.FirstOrDefault(w => w.WeekDefinitionId == SelectedWeekDefinitionId.Value);
|
||||||
|
}
|
||||||
|
// else if (SelectedWeekNumber!=null && SelectedWeekNumber>0 && _allWeeks.Any())
|
||||||
|
// {
|
||||||
|
// _selectedWeek = _allWeeks.FirstOrDefault(w => w.WeekDefinitionId == SelectedWeekNumber);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnParametersSetAsync()
|
||||||
|
{
|
||||||
|
if (SelectedWeekDefinitionId.HasValue && SelectedWeekDefinitionId.Value > 0 &&
|
||||||
|
_selectedWeek?.WeekDefinitionId != SelectedWeekDefinitionId.Value &&
|
||||||
|
_isLoaded)
|
||||||
|
{
|
||||||
|
_selectedWeek = _allWeeks.FirstOrDefault(w => w.WeekDefinitionId == SelectedWeekDefinitionId.Value);
|
||||||
|
}
|
||||||
|
// else if (SelectedWeekNumber!=null && SelectedWeekNumber>0 &&
|
||||||
|
// _selectedWeek?.WeekDefinitionId != SelectedWeekNumber &&
|
||||||
|
// _isLoaded)
|
||||||
|
// {
|
||||||
|
// _selectedWeek = _allWeeks.FirstOrDefault(w => w.WeekDefinitionId == SelectedWeekNumber);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadWeeks()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var request = new GetAvailableWeeksRequest
|
||||||
|
{
|
||||||
|
FutureWeeksCount = FutureWeeksCount,
|
||||||
|
PastWeeksCount = PastWeeksCount
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await CommissionContract.GetAvailableWeeksAsync(request);
|
||||||
|
|
||||||
|
_allWeeks = new List<WeekInfo>();
|
||||||
|
|
||||||
|
// ترتیب: هفته جاری، محاسبه شده، در انتظار، آینده
|
||||||
|
if (response.CurrentWeek != null)
|
||||||
|
_allWeeks.Add(response.CurrentWeek);
|
||||||
|
|
||||||
|
if (response.CalculatedWeeks != null)
|
||||||
|
_allWeeks.AddRange(response.CalculatedWeeks);
|
||||||
|
|
||||||
|
if (response.PendingWeeks != null)
|
||||||
|
_allWeeks.AddRange(response.PendingWeeks);
|
||||||
|
|
||||||
|
if (response.FutureWeeks != null)
|
||||||
|
_allWeeks.AddRange(response.FutureWeeks);
|
||||||
|
|
||||||
|
_isLoaded = true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_allWeeks = new List<WeekInfo>();
|
||||||
|
_isLoaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IEnumerable<WeekInfo>> Search(string value, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!_isLoaded)
|
||||||
|
await LoadWeeks();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return _allWeeks;
|
||||||
|
|
||||||
|
// جستجو در DisplayName یا DisplayText
|
||||||
|
return _allWeeks
|
||||||
|
.Where(w => w.DisplayName.Contains(value, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
w.DisplayText.Contains(value, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnSelected(WeekInfo? selected)
|
||||||
|
{
|
||||||
|
_selectedWeek = selected;
|
||||||
|
SelectedWeekDefinitionId = selected?.WeekDefinitionId;
|
||||||
|
// SelectedWeekNumber = selected.WeekDefinitionId;
|
||||||
|
await SelectedWeekDefinitionIdChanged.InvokeAsync(SelectedWeekDefinitionId);
|
||||||
|
// await SelectedWeekNumberChanged.InvokeAsync(SelectedWeekNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnClear()
|
||||||
|
{
|
||||||
|
_selectedWeek = null;
|
||||||
|
SelectedWeekDefinitionId = null;
|
||||||
|
// SelectedWeekNumber = null;
|
||||||
|
await SelectedWeekDefinitionIdChanged.InvokeAsync(null);
|
||||||
|
// await SelectedWeekNumberChanged.InvokeAsync(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
@* Merged into /blog *@
|
||||||
|
@attribute [Authorize(Roles = "Administrator")]
|
||||||
|
|
||||||
|
@using BackOffice.Services.Blog
|
||||||
|
|
||||||
|
@inject IBlogCategoryService BlogCategoryService
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<MudStack Spacing="2">
|
||||||
|
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||||
|
<MudText Typo="Typo.h5">مدیریت دستهبندیهای بلاگ</MudText>
|
||||||
|
<MudButton Variant="Variant.Filled"
|
||||||
|
Color="Color.Primary"
|
||||||
|
StartIcon="@Icons.Material.Filled.Add"
|
||||||
|
OnClick="CreateCategory">
|
||||||
|
افزودن دستهبندی
|
||||||
|
</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
<MudDataGrid T="BlogCategoryItemDto"
|
||||||
|
@ref="_grid"
|
||||||
|
ServerData="LoadServerData"
|
||||||
|
Hover="true"
|
||||||
|
Elevation="1"
|
||||||
|
Dense="true"
|
||||||
|
Height="calc(100vh - 240px)">
|
||||||
|
<Columns>
|
||||||
|
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||||
|
<PropertyColumn Property="x => x.Title" Title="عنوان" />
|
||||||
|
<PropertyColumn Property="x => x.Slug" Title="اسلاگ" />
|
||||||
|
<PropertyColumn Property="x => x.IconName" Title="آیکون" />
|
||||||
|
<PropertyColumn Property="x => x.PostCount" Title="تعداد پست" />
|
||||||
|
<PropertyColumn Property="x => x.IsActive" Title="وضعیت">
|
||||||
|
<CellTemplate>
|
||||||
|
<MudChip Color="@(context.Item.IsActive ? Color.Success : Color.Error)"
|
||||||
|
Size="Size.Small">
|
||||||
|
@(context.Item.IsActive ? "فعال" : "غیرفعال")
|
||||||
|
</MudChip>
|
||||||
|
</CellTemplate>
|
||||||
|
</PropertyColumn>
|
||||||
|
<PropertyColumn Property="x => x.SortOrder" Title="ترتیب" />
|
||||||
|
<TemplateColumn Title="عملیات" Sortable="false">
|
||||||
|
<CellTemplate>
|
||||||
|
<MudStack Row="true" Spacing="1">
|
||||||
|
<MudTooltip Text="ویرایش">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||||
|
Size="Size.Small"
|
||||||
|
Color="Color.Primary"
|
||||||
|
OnClick="@(() => EditCategory(context.Item))" />
|
||||||
|
</MudTooltip>
|
||||||
|
<MudTooltip Text="حذف">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||||
|
Size="Size.Small"
|
||||||
|
Color="Color.Error"
|
||||||
|
OnClick="@(() => DeleteCategory(context.Item))" />
|
||||||
|
</MudTooltip>
|
||||||
|
</MudStack>
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
</Columns>
|
||||||
|
<NoRecordsContent>
|
||||||
|
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||||
|
</NoRecordsContent>
|
||||||
|
<PagerContent>
|
||||||
|
<MudDataGridPager T="BlogCategoryItemDto" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||||
|
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||||
|
RowsPerPageString="تعداد در صفحه" />
|
||||||
|
</PagerContent>
|
||||||
|
</MudDataGrid>
|
||||||
|
</MudStack>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
using BackOffice.Services.Blog;
|
||||||
|
using BackOffice.Pages.Blog.Components;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
|
using MudBlazor;
|
||||||
|
|
||||||
|
namespace BackOffice.Pages.Blog;
|
||||||
|
|
||||||
|
public partial class BlogCategoryManagementPage
|
||||||
|
{
|
||||||
|
private MudDataGrid<BlogCategoryItemDto> _grid = default!;
|
||||||
|
|
||||||
|
private async Task<GridData<BlogCategoryItemDto>> LoadServerData(GridState<BlogCategoryItemDto> state)
|
||||||
|
{
|
||||||
|
var filter = new BlogCategoryFilterDto
|
||||||
|
{
|
||||||
|
PageNumber = state.Page + 1,
|
||||||
|
PageSize = state.PageSize
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await BlogCategoryService.GetAllAsync(filter);
|
||||||
|
|
||||||
|
return new GridData<BlogCategoryItemDto>
|
||||||
|
{
|
||||||
|
Items = result.Items,
|
||||||
|
TotalItems = result.TotalCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReloadAsync()
|
||||||
|
{
|
||||||
|
if (_grid != null)
|
||||||
|
{
|
||||||
|
await _grid.ReloadServerData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateCategory()
|
||||||
|
{
|
||||||
|
var parameters = new DialogParameters<BlogCategoryEditDialog>
|
||||||
|
{
|
||||||
|
{ nameof(BlogCategoryEditDialog.Model), new BlogCategoryEditDto() },
|
||||||
|
{ nameof(BlogCategoryEditDialog.IsEditMode), false }
|
||||||
|
};
|
||||||
|
|
||||||
|
var dialog = await DialogService.ShowAsync<BlogCategoryEditDialog>("ایجاد دستهبندی جدید", parameters,
|
||||||
|
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true });
|
||||||
|
|
||||||
|
var result = await dialog.Result;
|
||||||
|
if (!result.Canceled)
|
||||||
|
{
|
||||||
|
Snackbar.Add("دستهبندی با موفقیت ایجاد شد", Severity.Success);
|
||||||
|
await ReloadAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EditCategory(BlogCategoryItemDto item)
|
||||||
|
{
|
||||||
|
var dto = new BlogCategoryEditDto
|
||||||
|
{
|
||||||
|
Title = item.Title,
|
||||||
|
Slug = item.Slug,
|
||||||
|
Description = item.Description,
|
||||||
|
IconName = item.IconName,
|
||||||
|
SortOrder = item.SortOrder,
|
||||||
|
IsActive = item.IsActive
|
||||||
|
};
|
||||||
|
|
||||||
|
var parameters = new DialogParameters<BlogCategoryEditDialog>
|
||||||
|
{
|
||||||
|
{ nameof(BlogCategoryEditDialog.Model), dto },
|
||||||
|
{ nameof(BlogCategoryEditDialog.CategoryId), item.Id },
|
||||||
|
{ nameof(BlogCategoryEditDialog.IsEditMode), true }
|
||||||
|
};
|
||||||
|
|
||||||
|
var dialog = await DialogService.ShowAsync<BlogCategoryEditDialog>("ویرایش دستهبندی", parameters,
|
||||||
|
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true });
|
||||||
|
|
||||||
|
var result = await dialog.Result;
|
||||||
|
if (!result.Canceled)
|
||||||
|
{
|
||||||
|
Snackbar.Add("دستهبندی با موفقیت بهروزرسانی شد", Severity.Success);
|
||||||
|
await ReloadAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeleteCategory(BlogCategoryItemDto item)
|
||||||
|
{
|
||||||
|
var confirmed = await DialogService.ShowMessageBox(
|
||||||
|
"حذف دستهبندی",
|
||||||
|
$"آیا از حذف دستهبندی «{item.Title}» مطمئن هستید؟",
|
||||||
|
yesText: "حذف",
|
||||||
|
cancelText: "لغو");
|
||||||
|
|
||||||
|
if (confirmed == true)
|
||||||
|
{
|
||||||
|
await BlogCategoryService.DeleteAsync(item.Id);
|
||||||
|
Snackbar.Add("دستهبندی حذف شد", Severity.Success);
|
||||||
|
await ReloadAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
@page "/blog"
|
||||||
|
@page "/blog/posts"
|
||||||
|
@page "/blog/categories"
|
||||||
|
@attribute [Authorize(Roles = "Administrator")]
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<MudText Typo="Typo.h5" Class="mb-3">مدیریت بلاگ</MudText>
|
||||||
|
|
||||||
|
<MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true"
|
||||||
|
PanelClass="pt-4" @bind-ActivePanelIndex="_activeTab">
|
||||||
|
<MudTabPanel Text="پستها" Icon="@Icons.Material.Filled.PostAdd">
|
||||||
|
<BlogPostManagementPage />
|
||||||
|
</MudTabPanel>
|
||||||
|
<MudTabPanel Text="دستهبندیها" Icon="@Icons.Material.Filled.Category">
|
||||||
|
<BlogCategoryManagementPage />
|
||||||
|
</MudTabPanel>
|
||||||
|
</MudTabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private int _activeTab;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
var uri = Navigation.ToBaseRelativePath(Navigation.Uri).ToLowerInvariant().TrimEnd('/');
|
||||||
|
_activeTab = uri switch
|
||||||
|
{
|
||||||
|
"blog/categories" => 1,
|
||||||
|
_ => 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
@* Merged into /blog *@
|
||||||
|
@attribute [Authorize(Roles = "Administrator")]
|
||||||
|
|
||||||
|
@using BackOffice.Services.Blog
|
||||||
|
|
||||||
|
@inject IBlogPostService BlogPostService
|
||||||
|
@inject IBlogCategoryService BlogCategoryService
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<MudStack Spacing="2">
|
||||||
|
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||||
|
<MudText Typo="Typo.h5">مدیریت پستهای بلاگ</MudText>
|
||||||
|
<MudButton Variant="Variant.Filled"
|
||||||
|
Color="Color.Primary"
|
||||||
|
StartIcon="@Icons.Material.Filled.Add"
|
||||||
|
OnClick="CreatePost">
|
||||||
|
افزودن پست
|
||||||
|
</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
||||||
|
<MudTextField @bind-Value="_search"
|
||||||
|
Label="جستجو بر اساس عنوان"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense"
|
||||||
|
Adornment="Adornment.Start"
|
||||||
|
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||||
|
Immediate="true"
|
||||||
|
OnBlur="ReloadAsync"
|
||||||
|
OnKeyDown="OnSearchKeyDown" />
|
||||||
|
|
||||||
|
<MudSelect @bind-Value="_statusFilter"
|
||||||
|
Label="وضعیت"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense"
|
||||||
|
Style="width: 160px;">
|
||||||
|
<MudSelectItem T="int?" Value="@( (int?)null )">همه</MudSelectItem>
|
||||||
|
<MudSelectItem T="int?" Value="0">پیشنویس</MudSelectItem>
|
||||||
|
<MudSelectItem T="int?" Value="1">منتشر شده</MudSelectItem>
|
||||||
|
<MudSelectItem T="int?" Value="2">آرشیو شده</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
|
||||||
|
<MudSelect @bind-Value="_categoryFilter"
|
||||||
|
Label="دستهبندی"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense"
|
||||||
|
Style="width: 200px;">
|
||||||
|
<MudSelectItem T="long?" Value="@( (long?)null )">همه</MudSelectItem>
|
||||||
|
@foreach (var cat in _categories)
|
||||||
|
{
|
||||||
|
<MudSelectItem T="long?" Value="@( (long?)cat.Id )">@cat.Title</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
|
||||||
|
<MudButton Variant="Variant.Outlined"
|
||||||
|
Color="Color.Primary"
|
||||||
|
OnClick="ReloadAsync">
|
||||||
|
اعمال فیلتر
|
||||||
|
</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
<MudDataGrid T="BlogPostListItemDto"
|
||||||
|
@ref="_grid"
|
||||||
|
ServerData="LoadServerData"
|
||||||
|
Hover="true"
|
||||||
|
Elevation="1"
|
||||||
|
Dense="true"
|
||||||
|
Height="calc(100vh - 240px)">
|
||||||
|
<Columns>
|
||||||
|
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||||
|
<PropertyColumn Property="x => x.Title" Title="عنوان" />
|
||||||
|
<PropertyColumn Property="x => x.Slug" Title="اسلاگ" />
|
||||||
|
<PropertyColumn Property="x => x.StatusName" Title="وضعیت">
|
||||||
|
<CellTemplate>
|
||||||
|
<MudChip Color="@GetStatusColor(context.Item.Status)"
|
||||||
|
Size="Size.Small">
|
||||||
|
@context.Item.StatusName
|
||||||
|
</MudChip>
|
||||||
|
</CellTemplate>
|
||||||
|
</PropertyColumn>
|
||||||
|
<PropertyColumn Property="x => x.IsFeatured" Title="ویژه">
|
||||||
|
<CellTemplate>
|
||||||
|
@if (context.Item.IsFeatured)
|
||||||
|
{
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.Star" Color="Color.Warning" Size="Size.Small" />
|
||||||
|
}
|
||||||
|
</CellTemplate>
|
||||||
|
</PropertyColumn>
|
||||||
|
<PropertyColumn Property="x => x.ViewCount" Title="بازدید" />
|
||||||
|
<TemplateColumn Title="دستهبندیها" Sortable="false">
|
||||||
|
<CellTemplate>
|
||||||
|
<MudStack Row="true" Spacing="1">
|
||||||
|
@foreach (var cat in context.Item.Categories)
|
||||||
|
{
|
||||||
|
<MudChip Size="Size.Small" Color="Color.Info">@cat.Title</MudChip>
|
||||||
|
}
|
||||||
|
</MudStack>
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<TemplateColumn Title="تاریخ ایجاد">
|
||||||
|
<CellTemplate>
|
||||||
|
@context.Item.Created.MiladiToJalali()
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<TemplateColumn Title="عملیات" Sortable="false">
|
||||||
|
<CellTemplate>
|
||||||
|
<MudStack Row="true" Spacing="1">
|
||||||
|
@if (context.Item.Status == 0)
|
||||||
|
{
|
||||||
|
<MudTooltip Text="انتشار">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Publish"
|
||||||
|
Size="Size.Small"
|
||||||
|
Color="Color.Success"
|
||||||
|
OnClick="@(() => PublishPost(context.Item))" />
|
||||||
|
</MudTooltip>
|
||||||
|
}
|
||||||
|
@if (context.Item.Status == 1)
|
||||||
|
{
|
||||||
|
<MudTooltip Text="آرشیو">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Archive"
|
||||||
|
Size="Size.Small"
|
||||||
|
Color="Color.Warning"
|
||||||
|
OnClick="@(() => ArchivePost(context.Item))" />
|
||||||
|
</MudTooltip>
|
||||||
|
}
|
||||||
|
<MudTooltip Text="ویرایش">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||||
|
Size="Size.Small"
|
||||||
|
Color="Color.Primary"
|
||||||
|
OnClick="@(() => EditPost(context.Item))" />
|
||||||
|
</MudTooltip>
|
||||||
|
<MudTooltip Text="حذف">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||||
|
Size="Size.Small"
|
||||||
|
Color="Color.Error"
|
||||||
|
OnClick="@(() => DeletePost(context.Item))" />
|
||||||
|
</MudTooltip>
|
||||||
|
</MudStack>
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
</Columns>
|
||||||
|
<NoRecordsContent>
|
||||||
|
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||||
|
</NoRecordsContent>
|
||||||
|
<PagerContent>
|
||||||
|
<MudDataGridPager T="BlogPostListItemDto" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||||
|
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||||
|
RowsPerPageString="تعداد در صفحه" />
|
||||||
|
</PagerContent>
|
||||||
|
</MudDataGrid>
|
||||||
|
</MudStack>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
using BackOffice.Services.Blog;
|
||||||
|
using BackOffice.Pages.Blog.Components;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
|
using MudBlazor;
|
||||||
|
|
||||||
|
namespace BackOffice.Pages.Blog;
|
||||||
|
|
||||||
|
public partial class BlogPostManagementPage
|
||||||
|
{
|
||||||
|
private MudDataGrid<BlogPostListItemDto> _grid = default!;
|
||||||
|
private string? _search;
|
||||||
|
private int? _statusFilter;
|
||||||
|
private long? _categoryFilter;
|
||||||
|
private List<BlogCategoryItemDto> _categories = new();
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
_categories = await BlogCategoryService.GetActiveAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<GridData<BlogPostListItemDto>> LoadServerData(GridState<BlogPostListItemDto> state)
|
||||||
|
{
|
||||||
|
var filter = new BlogPostFilterDto
|
||||||
|
{
|
||||||
|
PageNumber = state.Page + 1,
|
||||||
|
PageSize = state.PageSize,
|
||||||
|
SearchTerm = _search,
|
||||||
|
Status = _statusFilter,
|
||||||
|
CategoryId = _categoryFilter
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await BlogPostService.GetAllAsync(filter);
|
||||||
|
|
||||||
|
return new GridData<BlogPostListItemDto>
|
||||||
|
{
|
||||||
|
Items = result.Items,
|
||||||
|
TotalItems = result.TotalCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReloadAsync()
|
||||||
|
{
|
||||||
|
if (_grid != null)
|
||||||
|
{
|
||||||
|
await _grid.ReloadServerData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSearchKeyDown(KeyboardEventArgs args)
|
||||||
|
{
|
||||||
|
if (args.Key == "Enter")
|
||||||
|
{
|
||||||
|
_ = ReloadAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Color GetStatusColor(int status) => status switch
|
||||||
|
{
|
||||||
|
0 => Color.Default, // Draft
|
||||||
|
1 => Color.Success, // Published
|
||||||
|
2 => Color.Warning, // Archived
|
||||||
|
_ => Color.Default
|
||||||
|
};
|
||||||
|
|
||||||
|
private async Task CreatePost()
|
||||||
|
{
|
||||||
|
var parameters = new DialogParameters<BlogPostEditDialog>
|
||||||
|
{
|
||||||
|
{ nameof(BlogPostEditDialog.Model), new BlogPostEditDto() },
|
||||||
|
{ nameof(BlogPostEditDialog.IsEditMode), false },
|
||||||
|
{ nameof(BlogPostEditDialog.Categories), _categories }
|
||||||
|
};
|
||||||
|
|
||||||
|
var dialog = await DialogService.ShowAsync<BlogPostEditDialog>("ایجاد پست جدید", parameters,
|
||||||
|
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Large, FullWidth = true });
|
||||||
|
|
||||||
|
var result = await dialog.Result;
|
||||||
|
if (!result.Canceled)
|
||||||
|
{
|
||||||
|
Snackbar.Add("پست با موفقیت ایجاد شد", Severity.Success);
|
||||||
|
await ReloadAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EditPost(BlogPostListItemDto item)
|
||||||
|
{
|
||||||
|
var details = await BlogPostService.GetByIdAsync(item.Id);
|
||||||
|
if (details == null)
|
||||||
|
{
|
||||||
|
Snackbar.Add("خطا در دریافت اطلاعات پست", Severity.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dto = new BlogPostEditDto
|
||||||
|
{
|
||||||
|
Title = details.Title,
|
||||||
|
Slug = details.Slug,
|
||||||
|
Summary = details.Summary,
|
||||||
|
HtmlContent = details.HtmlContent,
|
||||||
|
FeaturedImagePath = details.FeaturedImagePath,
|
||||||
|
FeaturedImageThumbnailPath = details.FeaturedImageThumbnailPath,
|
||||||
|
CategoryIds = details.Categories.Select(c => c.Id).ToList(),
|
||||||
|
TagIds = details.Tags.Select(t => t.Id).ToList(),
|
||||||
|
IsFeatured = details.IsFeatured,
|
||||||
|
SortOrder = details.SortOrder
|
||||||
|
};
|
||||||
|
|
||||||
|
var parameters = new DialogParameters<BlogPostEditDialog>
|
||||||
|
{
|
||||||
|
{ nameof(BlogPostEditDialog.Model), dto },
|
||||||
|
{ nameof(BlogPostEditDialog.PostId), item.Id },
|
||||||
|
{ nameof(BlogPostEditDialog.IsEditMode), true },
|
||||||
|
{ nameof(BlogPostEditDialog.Categories), _categories }
|
||||||
|
};
|
||||||
|
|
||||||
|
var dialog = await DialogService.ShowAsync<BlogPostEditDialog>("ویرایش پست", parameters,
|
||||||
|
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Large, FullWidth = true });
|
||||||
|
|
||||||
|
var result = await dialog.Result;
|
||||||
|
if (!result.Canceled)
|
||||||
|
{
|
||||||
|
Snackbar.Add("پست با موفقیت بهروزرسانی شد", Severity.Success);
|
||||||
|
await ReloadAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PublishPost(BlogPostListItemDto item)
|
||||||
|
{
|
||||||
|
var confirmed = await DialogService.ShowMessageBox(
|
||||||
|
"انتشار پست",
|
||||||
|
$"آیا از انتشار پست «{item.Title}» مطمئن هستید؟",
|
||||||
|
yesText: "انتشار",
|
||||||
|
cancelText: "لغو");
|
||||||
|
|
||||||
|
if (confirmed == true)
|
||||||
|
{
|
||||||
|
var result = await BlogPostService.PublishAsync(item.Id);
|
||||||
|
if (result.Success)
|
||||||
|
{
|
||||||
|
Snackbar.Add("پست با موفقیت منتشر شد", Severity.Success);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Snackbar.Add(result.Message, Severity.Error);
|
||||||
|
}
|
||||||
|
await ReloadAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ArchivePost(BlogPostListItemDto item)
|
||||||
|
{
|
||||||
|
var confirmed = await DialogService.ShowMessageBox(
|
||||||
|
"آرشیو پست",
|
||||||
|
$"آیا از آرشیو کردن پست «{item.Title}» مطمئن هستید؟",
|
||||||
|
yesText: "آرشیو",
|
||||||
|
cancelText: "لغو");
|
||||||
|
|
||||||
|
if (confirmed == true)
|
||||||
|
{
|
||||||
|
var result = await BlogPostService.ArchiveAsync(item.Id);
|
||||||
|
if (result.Success)
|
||||||
|
{
|
||||||
|
Snackbar.Add("پست آرشیو شد", Severity.Success);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Snackbar.Add(result.Message, Severity.Error);
|
||||||
|
}
|
||||||
|
await ReloadAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeletePost(BlogPostListItemDto item)
|
||||||
|
{
|
||||||
|
var confirmed = await DialogService.ShowMessageBox(
|
||||||
|
"حذف پست",
|
||||||
|
$"آیا از حذف پست «{item.Title}» مطمئن هستید؟",
|
||||||
|
yesText: "حذف",
|
||||||
|
cancelText: "لغو");
|
||||||
|
|
||||||
|
if (confirmed == true)
|
||||||
|
{
|
||||||
|
await BlogPostService.DeleteAsync(item.Id);
|
||||||
|
Snackbar.Add("پست حذف شد", Severity.Success);
|
||||||
|
await ReloadAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
@using BackOffice.Services.Blog
|
||||||
|
|
||||||
|
<MudDialog>
|
||||||
|
<DialogContent>
|
||||||
|
<EditForm Model="@Model" OnValidSubmit="HandleValidSubmit">
|
||||||
|
<MudStack Spacing="2">
|
||||||
|
<MudTextField @bind-Value="Model.Title"
|
||||||
|
Label="عنوان"
|
||||||
|
Required="true"
|
||||||
|
Variant="Variant.Outlined" />
|
||||||
|
|
||||||
|
<MudTextField @bind-Value="Model.Slug"
|
||||||
|
Label="اسلاگ (URL)"
|
||||||
|
Required="true"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
HelperText="مثال: tech-news" />
|
||||||
|
|
||||||
|
<MudTextField @bind-Value="Model.Description"
|
||||||
|
Label="توضیحات"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Lines="3" />
|
||||||
|
|
||||||
|
<MudTextField @bind-Value="Model.IconName"
|
||||||
|
Label="نام آیکون"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
HelperText="مثال: category" />
|
||||||
|
|
||||||
|
<MudNumericField T="int"
|
||||||
|
@bind-Value="Model.SortOrder"
|
||||||
|
Label="ترتیب نمایش"
|
||||||
|
Variant="Variant.Outlined" />
|
||||||
|
|
||||||
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||||
|
<MudSwitch T="bool" @bind-Value="Model.IsActive" Color="Color.Primary" />
|
||||||
|
<MudText Typo="Typo.body2">فعال</MudText>
|
||||||
|
</MudStack>
|
||||||
|
</MudStack>
|
||||||
|
</EditForm>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="SubmitAsync">
|
||||||
|
@(_isEditMode ? "ذخیره تغییرات" : "ایجاد دستهبندی")
|
||||||
|
</MudButton>
|
||||||
|
<MudButton Color="Color.Default" Variant="Variant.Text" OnClick="Cancel">
|
||||||
|
انصراف
|
||||||
|
</MudButton>
|
||||||
|
</DialogActions>
|
||||||
|
</MudDialog>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using BackOffice.Services.Blog;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using MudBlazor;
|
||||||
|
|
||||||
|
namespace BackOffice.Pages.Blog.Components;
|
||||||
|
|
||||||
|
public partial class BlogCategoryEditDialog
|
||||||
|
{
|
||||||
|
[CascadingParameter] IMudDialogInstance DialogInstance { get; set; } = default!;
|
||||||
|
[Inject] public IBlogCategoryService BlogCategoryService { get; set; } = default!;
|
||||||
|
|
||||||
|
[Parameter] public BlogCategoryEditDto Model { get; set; } = new();
|
||||||
|
[Parameter] public long? CategoryId { get; set; }
|
||||||
|
[Parameter] public bool IsEditMode { get; set; }
|
||||||
|
|
||||||
|
private bool _isEditMode => IsEditMode && CategoryId.HasValue;
|
||||||
|
|
||||||
|
private async Task HandleValidSubmit()
|
||||||
|
{
|
||||||
|
await SubmitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SubmitAsync()
|
||||||
|
{
|
||||||
|
if (_isEditMode && CategoryId.HasValue)
|
||||||
|
{
|
||||||
|
await BlogCategoryService.UpdateAsync(CategoryId.Value, Model);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await BlogCategoryService.CreateAsync(Model);
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogInstance.Close(DialogResult.Ok(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Cancel()
|
||||||
|
{
|
||||||
|
DialogInstance.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
@using BackOffice.Services.Blog
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using Tizzani.MudBlazor.HtmlEditor
|
||||||
|
@using BackOffice.Common.BaseComponents
|
||||||
|
|
||||||
|
<MudDialog>
|
||||||
|
<DialogContent>
|
||||||
|
<MudForm @ref="_form" @bind-IsValid="@_isValid">
|
||||||
|
<MudStack Spacing="2">
|
||||||
|
<MudTextField @bind-Value="Model.Title"
|
||||||
|
Label="عنوان پست"
|
||||||
|
Required="true"
|
||||||
|
Variant="Variant.Outlined" />
|
||||||
|
|
||||||
|
<MudTextField @bind-Value="Model.Slug"
|
||||||
|
Label="نشانی URL (slug)"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
HelperText="اختیاری — خالی بگذارید تا خودکار از عنوان ساخته شود. فقط حروف کوچک انگلیسی، عدد و خط تیره." />
|
||||||
|
|
||||||
|
<MudTextField @bind-Value="Model.Summary"
|
||||||
|
Label="خلاصه"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Lines="3" />
|
||||||
|
|
||||||
|
<MudText Typo="Typo.subtitle2">محتوای HTML</MudText>
|
||||||
|
<MudHtmlEditor @bind-Html="Model.HtmlContent" MinHeight="300px">
|
||||||
|
<MudHtmlToolbarOptions InsertImage="false" />
|
||||||
|
</MudHtmlEditor>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.subtitle2">تصویر شاخص</MudText>
|
||||||
|
<MudStack Justify="Justify.Center" AlignItems="AlignItems.Center">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(_imagePreview))
|
||||||
|
{
|
||||||
|
<Image Src="@_imagePreview" Width="280" Height="180" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(Model.FeaturedImagePath))
|
||||||
|
{
|
||||||
|
<Image Src="@Model.FeaturedImagePath" Width="280" Height="180" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudPaper Class="d-flex align-center justify-center" Style="width:280px;height:180px;">
|
||||||
|
<MudText Typo="Typo.caption">تصویری انتخاب نشده است</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
}
|
||||||
|
|
||||||
|
<MudFileUpload T="IBrowserFile" Accept="image/*" FilesChanged="OnImageSelected">
|
||||||
|
<ActivatorContent>
|
||||||
|
<MudButton HtmlTag="label"
|
||||||
|
Variant="Variant.Filled"
|
||||||
|
Color="Color.Primary"
|
||||||
|
ButtonType="ButtonType.Button"
|
||||||
|
StartIcon="@Icons.Material.Filled.Image">
|
||||||
|
انتخاب تصویر شاخص
|
||||||
|
</MudButton>
|
||||||
|
</ActivatorContent>
|
||||||
|
<SelectedTemplate>
|
||||||
|
@if (context != null)
|
||||||
|
{
|
||||||
|
<MudText Class="mt-1" Typo="Typo.caption">@context.Name</MudText>
|
||||||
|
}
|
||||||
|
</SelectedTemplate>
|
||||||
|
</MudFileUpload>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
<MudSelect T="long"
|
||||||
|
Label="دستهبندیها"
|
||||||
|
MultiSelection="true"
|
||||||
|
@bind-SelectedValues="_selectedCategoryIds"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
MultiSelectionTextFunc="@(new Func<List<string>, string>(GetCategorySelectionText))">
|
||||||
|
@foreach (var cat in Categories)
|
||||||
|
{
|
||||||
|
<MudSelectItem T="long" Value="@cat.Id">@cat.Title</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
|
||||||
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||||
|
<MudSwitch T="bool" @bind-Value="Model.IsFeatured" Color="Color.Warning" />
|
||||||
|
<MudText Typo="Typo.body2">پست ویژه</MudText>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
<MudNumericField T="int"
|
||||||
|
@bind-Value="Model.SortOrder"
|
||||||
|
Label="ترتیب نمایش"
|
||||||
|
Variant="Variant.Outlined" />
|
||||||
|
</MudStack>
|
||||||
|
</MudForm>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="SubmitAsync" Disabled="@(!_isValid || _loading)">
|
||||||
|
@(_isEditMode ? "ذخیره تغییرات" : "ایجاد پست")
|
||||||
|
</MudButton>
|
||||||
|
<MudButton Color="Color.Default" Variant="Variant.Text" OnClick="Cancel">
|
||||||
|
انصراف
|
||||||
|
</MudButton>
|
||||||
|
</DialogActions>
|
||||||
|
</MudDialog>
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
using BackOffice.Services.Blog;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.AspNetCore.Components.Forms;
|
||||||
|
using MudBlazor;
|
||||||
|
|
||||||
|
namespace BackOffice.Pages.Blog.Components;
|
||||||
|
|
||||||
|
public partial class BlogPostEditDialog
|
||||||
|
{
|
||||||
|
[CascadingParameter] IMudDialogInstance DialogInstance { get; set; } = default!;
|
||||||
|
[Inject] public IBlogPostService BlogPostService { get; set; } = default!;
|
||||||
|
|
||||||
|
[Parameter] public BlogPostEditDto Model { get; set; } = new();
|
||||||
|
[Parameter] public long? PostId { get; set; }
|
||||||
|
[Parameter] public bool IsEditMode { get; set; }
|
||||||
|
[Parameter] public List<BlogCategoryItemDto> Categories { get; set; } = new();
|
||||||
|
|
||||||
|
private bool _isEditMode => IsEditMode && PostId.HasValue;
|
||||||
|
private MudForm? _form;
|
||||||
|
private bool _isValid = true;
|
||||||
|
private bool _loading;
|
||||||
|
|
||||||
|
private IBrowserFile? _imageFile;
|
||||||
|
private byte[]? _imageBuffer;
|
||||||
|
private string? _imagePreview;
|
||||||
|
private readonly long _maxAllowedSize = (1024 * 1024) * 5;
|
||||||
|
|
||||||
|
private IEnumerable<long> _selectedCategoryIds
|
||||||
|
{
|
||||||
|
get => Model.CategoryIds;
|
||||||
|
set => Model.CategoryIds = value.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetCategorySelectionText(List<string> selectedValues)
|
||||||
|
{
|
||||||
|
if (selectedValues == null || !selectedValues.Any())
|
||||||
|
return "انتخاب دستهبندی";
|
||||||
|
|
||||||
|
var names = new List<string>();
|
||||||
|
foreach (var val in selectedValues)
|
||||||
|
{
|
||||||
|
if (long.TryParse(val, out var id))
|
||||||
|
{
|
||||||
|
var cat = Categories.FirstOrDefault(c => c.Id == id);
|
||||||
|
if (cat != null) names.Add(cat.Title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return names.Any() ? string.Join("، ", names) : "انتخاب دستهبندی";
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnImageSelected(IBrowserFile? file)
|
||||||
|
{
|
||||||
|
_imageFile = file;
|
||||||
|
if (file != null)
|
||||||
|
{
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
await file.OpenReadStream(_maxAllowedSize).CopyToAsync(ms);
|
||||||
|
_imageBuffer = ms.ToArray();
|
||||||
|
_imagePreview = $"data:{file.ContentType};base64," + Convert.ToBase64String(_imageBuffer);
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleValidSubmit()
|
||||||
|
{
|
||||||
|
await SubmitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SubmitAsync()
|
||||||
|
{
|
||||||
|
if (_form != null)
|
||||||
|
{
|
||||||
|
await _form.Validate();
|
||||||
|
if (!_isValid) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_loading = true;
|
||||||
|
|
||||||
|
// Handle image upload
|
||||||
|
if (_imageFile != null && _imageBuffer != null)
|
||||||
|
{
|
||||||
|
Model.ImageFile = _imageBuffer;
|
||||||
|
Model.ImageMime = _imageFile.ContentType;
|
||||||
|
Model.ImageFileName = Path.GetFileNameWithoutExtension(_imageFile.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_isEditMode && PostId.HasValue)
|
||||||
|
{
|
||||||
|
await BlogPostService.UpdateAsync(PostId.Value, Model);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await BlogPostService.CreateAsync(Model);
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogInstance.Close(DialogResult.Ok(true));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Cancel()
|
||||||
|
{
|
||||||
|
DialogInstance.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,31 @@
|
|||||||
@attribute [Route(RouteConstance.Category)]
|
@attribute [Route(RouteConstance.Category)]
|
||||||
|
|
||||||
@using BackOffice.BFF.Category.Protobuf.Protos.Category
|
@using CMSMicroservice.Protobuf.Protos.Category
|
||||||
@using BackOffice.Common.BaseComponents
|
@using BackOffice.Common.BaseComponents
|
||||||
@using BackOffice.Pages.AutoComplete
|
@using BackOffice.Pages.AutoComplete
|
||||||
@using DataModel = BackOffice.BFF.Category.Protobuf.Protos.Category.GetAllCategoryByFilterResponseModel
|
@using DataModel = CMSMicroservice.Protobuf.Protos.Category.GetAllCategoryByFilterResponseModel
|
||||||
|
|
||||||
<BasePageComponent @ref="_basePage" OnClearFilterClick="OnFilterCleared" OnSubmitClick="OnFilterSubmit">
|
<BasePageComponent @ref="_basePage" OnClearFilterClick="OnFilterCleared" OnSubmitClick="OnFilterSubmit">
|
||||||
<Filters>
|
<Filters>
|
||||||
<MudTextField T="string" Clearable="true" Label="عنوان" @bind-Value="@_request.Filter.Title" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
<MudItem xs="12" sm="6" md="3">
|
||||||
<CategoryAutoComplete Value="_parentFilterId"
|
<MudTextField T="string" Clearable="true" Label="عنوان" @bind-Value="@_request.Filter.Title" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||||
ValueChanged="OnParentFilterChanged"
|
</MudItem>
|
||||||
Label="فیلتر بر اساس دستهبندی والد" />
|
<MudItem xs="12" sm="6" md="3">
|
||||||
|
<CategoryAutoComplete Value="_parentFilterId"
|
||||||
|
ValueChanged="OnParentFilterChanged"
|
||||||
|
Label="فیلتر بر اساس دستهبندی والد" />
|
||||||
|
</MudItem>
|
||||||
</Filters>
|
</Filters>
|
||||||
<Content>
|
<Content>
|
||||||
<MudGrid>
|
<MudGrid>
|
||||||
<MudItem xs="12" md="3">
|
<MudItem xs="12" md="3">
|
||||||
<MudPaper Class="pa-2" Style="height:72vh; overflow:auto;">
|
<MudPaper Class="pa-2" Style="height:calc(100vh - 240px); overflow:auto;">
|
||||||
<MudText Typo="Typo.subtitle2" Class="mb-2">درخت دستهبندیها</MudText>
|
<MudText Typo="Typo.subtitle2" Class="mb-2">درخت دستهبندیها</MudText>
|
||||||
<MudList T="string" Dense="true">
|
<MudList T="string" Dense="true">
|
||||||
<MudListItem T="string"
|
<MudListItem T="string"
|
||||||
OnClick="@(() => OnTreeNodeSelected(null))"
|
OnClick="@(() => OnTreeNodeSelected(null))"
|
||||||
Selected="@(_parentFilterId == null)"
|
Selected="@(_parentFilterId == null)"
|
||||||
Style="cursor:pointer;">
|
>
|
||||||
<MudText Typo="Typo.body2">همه دستهبندیها</MudText>
|
<MudText Typo="Typo.body2">همه دستهبندیها</MudText>
|
||||||
</MudListItem>
|
</MudListItem>
|
||||||
@foreach (var node in _treeNodes)
|
@foreach (var node in _treeNodes)
|
||||||
@@ -29,7 +33,7 @@
|
|||||||
<MudListItem T="string"
|
<MudListItem T="string"
|
||||||
OnClick="@(() => OnTreeNodeSelected(node.Id))"
|
OnClick="@(() => OnTreeNodeSelected(node.Id))"
|
||||||
Selected="@(_parentFilterId == node.Id)"
|
Selected="@(_parentFilterId == node.Id)"
|
||||||
Style="cursor:pointer;">
|
>
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||||
<div style="width:@(node.Level * 16)px;"></div>
|
<div style="width:@(node.Level * 16)px;"></div>
|
||||||
<MudText Typo="Typo.body2">@node.Title</MudText>
|
<MudText Typo="Typo.body2">@node.Title</MudText>
|
||||||
@@ -44,13 +48,14 @@
|
|||||||
ServerData="@(new Func<GridState<DataModel>, Task<GridData<DataModel>>>(ServerReload))"
|
ServerData="@(new Func<GridState<DataModel>, Task<GridData<DataModel>>>(ServerReload))"
|
||||||
Hover="true"
|
Hover="true"
|
||||||
@ref="_gridData"
|
@ref="_gridData"
|
||||||
Height="72vh">
|
Height="calc(100vh - 240px)">
|
||||||
<ColGroup>
|
<ColGroup>
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
<col />
|
<col />
|
||||||
|
<col />
|
||||||
<col style="width: 80px;" />
|
<col style="width: 80px;" />
|
||||||
</ColGroup>
|
</ColGroup>
|
||||||
<ToolBarContent>
|
<ToolBarContent>
|
||||||
@@ -62,7 +67,7 @@
|
|||||||
Size="Size.Large"
|
Size="Size.Large"
|
||||||
ButtonType="ButtonType.Button"
|
ButtonType="ButtonType.Button"
|
||||||
OnClick="CreateNew"
|
OnClick="CreateNew"
|
||||||
Style="cursor:pointer;">
|
>
|
||||||
افزودن
|
افزودن
|
||||||
</MudButton>
|
</MudButton>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
@@ -85,6 +90,8 @@
|
|||||||
<MudText Typo="Typo.body2">@parentTitle</MudText>
|
<MudText Typo="Typo.body2">@parentTitle</MudText>
|
||||||
</CellTemplate>
|
</CellTemplate>
|
||||||
</TemplateColumn>
|
</TemplateColumn>
|
||||||
|
<PropertyColumn Property="x => x.SortOrder" Title="ترتیب" />
|
||||||
|
<PropertyColumn Property="x => x.ProductCount" Title="تعداد محصولات" />
|
||||||
<TemplateColumn Title="فعال؟">
|
<TemplateColumn Title="فعال؟">
|
||||||
<CellTemplate>
|
<CellTemplate>
|
||||||
<MudChip Color="@(context.Item.IsActive ? Color.Success : Color.Error)"
|
<MudChip Color="@(context.Item.IsActive ? Color.Success : Color.Error)"
|
||||||
@@ -102,15 +109,16 @@
|
|||||||
Size="Size.Small"
|
Size="Size.Small"
|
||||||
ButtonType="ButtonType.Button"
|
ButtonType="ButtonType.Button"
|
||||||
OnClick="@(() => Update(context.Item))"
|
OnClick="@(() => Update(context.Item))"
|
||||||
Style="cursor:pointer;" />
|
/>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
|
|
||||||
<MudTooltip Text="حذف">
|
<MudTooltip Text="حذف">
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.DeleteOutline"
|
<MudIconButton Icon="@Icons.Material.Filled.DeleteOutline"
|
||||||
Size="Size.Small"
|
Size="Size.Small"
|
||||||
ButtonType="ButtonType.Button"
|
ButtonType="ButtonType.Button"
|
||||||
|
Disabled="@HasChildren(context.Item.Id)"
|
||||||
OnClick="@(() => OnDelete(context.Item))"
|
OnClick="@(() => OnDelete(context.Item))"
|
||||||
Style="cursor:pointer;" />
|
/>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
|
|
||||||
<MudTooltip Text="مدیریت محصولات این دسته (درگ و دراپ)">
|
<MudTooltip Text="مدیریت محصولات این دسته (درگ و دراپ)">
|
||||||
@@ -118,17 +126,29 @@
|
|||||||
Size="Size.Small"
|
Size="Size.Small"
|
||||||
ButtonType="ButtonType.Button"
|
ButtonType="ButtonType.Button"
|
||||||
OnClick="@(() => OpenCategoryProducts(context.Item))"
|
OnClick="@(() => OpenCategoryProducts(context.Item))"
|
||||||
Style="cursor:pointer;" />
|
/>
|
||||||
|
</MudTooltip>
|
||||||
|
|
||||||
|
<MudTooltip Text="افزودن زیردسته">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.CreateNewFolder"
|
||||||
|
Size="Size.Small"
|
||||||
|
Color="Color.Primary"
|
||||||
|
ButtonType="ButtonType.Button"
|
||||||
|
OnClick="@(() => CreateSubcategory(context.Item))"
|
||||||
|
/>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</CellTemplate>
|
</CellTemplate>
|
||||||
</TemplateColumn>
|
</TemplateColumn>
|
||||||
</Columns>
|
</Columns>
|
||||||
|
<NoRecordsContent>
|
||||||
|
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||||
|
</NoRecordsContent>
|
||||||
<PagerContent>
|
<PagerContent>
|
||||||
<MudDataGridPager T="DataModel"
|
<MudDataGridPager T="DataModel"
|
||||||
PageSizeOptions="@(new int[] { 30, 60, 90 })"
|
PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||||
RowsPerPageString="تعداد سطرهای صفحه" />
|
RowsPerPageString="تعداد در صفحه" />
|
||||||
</PagerContent>
|
</PagerContent>
|
||||||
</MudDataGrid>
|
</MudDataGrid>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using BackOffice.BFF.Category.Protobuf.Protos.Category;
|
using CMSMicroservice.Protobuf.Protos;
|
||||||
|
using CMSMicroservice.Protobuf.Protos.Category;
|
||||||
using BackOffice.Common.BaseComponents;
|
using BackOffice.Common.BaseComponents;
|
||||||
using BackOffice.Common.Utilities;
|
using BackOffice.Common.Utilities;
|
||||||
using Mapster;
|
using Mapster;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
using DataModel = BackOffice.BFF.Category.Protobuf.Protos.Category.GetAllCategoryByFilterResponseModel;
|
using DataModel = CMSMicroservice.Protobuf.Protos.Category.GetAllCategoryByFilterResponseModel;
|
||||||
|
|
||||||
namespace BackOffice.Pages.Category;
|
namespace BackOffice.Pages.Category;
|
||||||
|
|
||||||
@@ -151,12 +152,30 @@ public partial class CategoryMainPage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task CreateSubcategory(DataModel parent)
|
||||||
|
{
|
||||||
|
var dialog = await DialogService.ShowAsync<CreateOrUpdateCategoryDialog>(
|
||||||
|
$"افزودن زیردسته برای «{parent.Title}»",
|
||||||
|
new DialogParameters<CreateOrUpdateCategoryDialog>
|
||||||
|
{
|
||||||
|
{ x => x.Model, new CreateNewCategoryRequest { IsActive = true, ParentId = parent.Id } }
|
||||||
|
},
|
||||||
|
new DialogOptions { CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.Small });
|
||||||
|
|
||||||
|
var result = await dialog.Result;
|
||||||
|
if (!result.Canceled)
|
||||||
|
{
|
||||||
|
await ReloadData();
|
||||||
|
await LoadTreeAsync();
|
||||||
|
Snackbar.Add("زیردسته با موفقیت ایجاد شد.", Severity.Success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task Update(DataModel model)
|
public async Task Update(DataModel model)
|
||||||
{
|
{
|
||||||
var getResponse = await CategoryContract.GetCategoryAsync(new GetCategoryRequest { Id = model.Id });
|
var getResponse = await CategoryContract.GetCategoryAsync(new GetCategoryRequest { Id = model.Id });
|
||||||
|
|
||||||
var editModel = getResponse.Adapt<UpdateCategoryRequest>();
|
var editModel = getResponse.Adapt<UpdateCategoryRequest>();
|
||||||
// Console.WriteLine(JsonSerializer.Serialize(editModel));
|
|
||||||
var dialog = await DialogService.ShowAsync<CreateOrUpdateCategoryDialog>(
|
var dialog = await DialogService.ShowAsync<CreateOrUpdateCategoryDialog>(
|
||||||
"ویرایش دستهبندی",
|
"ویرایش دستهبندی",
|
||||||
new DialogParameters<CreateOrUpdateCategoryDialog>
|
new DialogParameters<CreateOrUpdateCategoryDialog>
|
||||||
@@ -179,7 +198,7 @@ public partial class CategoryMainPage
|
|||||||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small };
|
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small };
|
||||||
bool? result = await DialogService.ShowMessageBox(
|
bool? result = await DialogService.ShowMessageBox(
|
||||||
"اخطار",
|
"اخطار",
|
||||||
"آیا از حذف این دستهبندی مطمئن هستید؟",
|
$"آیا از حذف دستهبندی «{model.Title}» مطمئن هستید؟",
|
||||||
yesText: "حذف",
|
yesText: "حذف",
|
||||||
cancelText: "لغو",
|
cancelText: "لغو",
|
||||||
options: options);
|
options: options);
|
||||||
@@ -237,4 +256,9 @@ public partial class CategoryMainPage
|
|||||||
{
|
{
|
||||||
Navigation.NavigateTo($"{RouteConstance.CategoryProducts}{model.Id}");
|
Navigation.NavigateTo($"{RouteConstance.CategoryProducts}{model.Id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool HasChildren(long categoryId)
|
||||||
|
{
|
||||||
|
return _treeNodes.Any(n => n.ParentId == categoryId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
@attribute [Route(RouteConstance.CategoryProducts + "{CategoryId:long?}")]
|
@attribute [Route(RouteConstance.CategoryProducts + "{CategoryId:long?}")]
|
||||||
|
|
||||||
@using BackOffice.BFF.Products.Protobuf.Protos.Products
|
@using CMSMicroservice.Protobuf.Protos.Products
|
||||||
@using BackOffice.Common.BaseComponents
|
@using BackOffice.Common.BaseComponents
|
||||||
@using BackOffice.Pages.AutoComplete
|
@using BackOffice.Pages.AutoComplete
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using BackOffice.BFF.Products.Protobuf.Protos.Products;
|
using CMSMicroservice.Protobuf.Protos.Products;
|
||||||
using BackOffice.BFF.Category.Protobuf.Protos.Category;
|
using CMSMicroservice.Protobuf.Protos.Category;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
|
|||||||
@@ -1,46 +1,75 @@
|
|||||||
@using BackOffice.BFF.Category.Protobuf.Protos.Category
|
@using CMSMicroservice.Protobuf.Protos.Category
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
@using BackOffice.Pages.AutoComplete
|
@using BackOffice.Pages.AutoComplete
|
||||||
|
|
||||||
<MudDialog>
|
<MudDialog>
|
||||||
|
<TitleContent>
|
||||||
|
<MudText Typo="Typo.h6">
|
||||||
|
<MudIcon Icon="@(IsEdit ? Icons.Material.Filled.Edit : Icons.Material.Filled.Add)" Class="ml-2" />
|
||||||
|
@(IsEdit ? "ویرایش دستهبندی" : "ایجاد دستهبندی جدید")
|
||||||
|
</MudText>
|
||||||
|
</TitleContent>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<MudStack Spacing="2">
|
<MudForm @ref="_form" @bind-IsValid="@_isValid">
|
||||||
<MudTextField T="string"
|
<MudStack Spacing="2">
|
||||||
@bind-Value="Name"
|
<MudTextField T="string"
|
||||||
Label="نام لاتین"
|
@bind-Value="Name"
|
||||||
Variant="Variant.Outlined"
|
Label="نام لاتین"
|
||||||
Margin="Margin.Dense" />
|
Required="true"
|
||||||
|
RequiredError="نام لاتین الزامی است"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense" />
|
||||||
|
|
||||||
<MudTextField T="string"
|
<MudTextField T="string"
|
||||||
@bind-Value="Title"
|
@bind-Value="Title"
|
||||||
Label="عنوان"
|
Label="عنوان"
|
||||||
Variant="Variant.Outlined"
|
Required="true"
|
||||||
Margin="Margin.Dense" />
|
RequiredError="عنوان الزامی است"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense" />
|
||||||
|
|
||||||
<MudTextField T="string"
|
<MudTextField T="string"
|
||||||
@bind-Value="Description"
|
@bind-Value="Description"
|
||||||
Label="توضیحات"
|
Label="توضیحات"
|
||||||
Variant="Variant.Outlined"
|
Variant="Variant.Outlined"
|
||||||
Margin="Margin.Dense"
|
Margin="Margin.Dense"
|
||||||
Lines="3"
|
Lines="3"
|
||||||
TextArea="true" />
|
TextArea="true" />
|
||||||
|
|
||||||
<CategoryAutoComplete Value="ParentId"
|
<MudTextField T="string"
|
||||||
ValueChanged="OnParentChanged"
|
@bind-Value="ImagePath"
|
||||||
Label="دستهبندی والد (اختیاری)" />
|
Label="آدرس تصویر"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense" />
|
||||||
|
|
||||||
<MudNumericField T="int"
|
<CategoryAutoComplete Value="ParentId"
|
||||||
@bind-Value="SortOrder"
|
ValueChanged="OnParentChanged"
|
||||||
HideSpinButtons="true"
|
Label="دستهبندی والد (اختیاری)" />
|
||||||
Label="ترتیب نمایش"
|
<MudText Typo="Typo.caption" Color="Color.Secondary">برای دسته اصلی خالی بگذارید</MudText>
|
||||||
Variant="Variant.Outlined"
|
|
||||||
Margin="Margin.Dense" />
|
|
||||||
|
|
||||||
<MudCheckBox T="bool" @bind-Value="IsActive" Label="فعال" />
|
<MudNumericField T="int"
|
||||||
</MudStack>
|
@bind-Value="SortOrder"
|
||||||
|
HideSpinButtons="true"
|
||||||
|
Label="ترتیب نمایش"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense"
|
||||||
|
HelperText="عدد کمتر = اولویت بالاتر" />
|
||||||
|
|
||||||
|
<MudSwitch T="bool" @bind-Value="IsActive" Label="فعال" Color="Color.Success" />
|
||||||
|
</MudStack>
|
||||||
|
</MudForm>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<MudButton OnClick="Cancel">لغو</MudButton>
|
<MudButton OnClick="Cancel" Variant="Variant.Text">لغو</MudButton>
|
||||||
<MudButton Color="Color.Primary" OnClick="Save">ثبت</MudButton>
|
<MudButton Color="Color.Primary"
|
||||||
|
Variant="Variant.Filled"
|
||||||
|
OnClick="Save"
|
||||||
|
Disabled="@(!_isValid || _isSaving)">
|
||||||
|
@if (_isSaving)
|
||||||
|
{
|
||||||
|
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="ml-2" />
|
||||||
|
}
|
||||||
|
@(IsEdit ? "ذخیره تغییرات" : "ایجاد دستهبندی")
|
||||||
|
</MudButton>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</MudDialog>
|
</MudDialog>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using BackOffice.BFF.Category.Protobuf.Protos.Category;
|
using CMSMicroservice.Protobuf.Protos.Category;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
|
|
||||||
@@ -14,9 +14,14 @@ public partial class CreateOrUpdateCategoryDialog
|
|||||||
|
|
||||||
private bool IsEdit => EditModel != null;
|
private bool IsEdit => EditModel != null;
|
||||||
|
|
||||||
|
private MudForm? _form;
|
||||||
|
private bool _isValid;
|
||||||
|
private bool _isSaving;
|
||||||
|
|
||||||
private string Name { get; set; } = string.Empty;
|
private string Name { get; set; } = string.Empty;
|
||||||
private string Title { get; set; } = string.Empty;
|
private string Title { get; set; } = string.Empty;
|
||||||
private string? Description { get; set; }
|
private string? Description { get; set; }
|
||||||
|
private string? ImagePath { get; set; }
|
||||||
private long? ParentId { get; set; }
|
private long? ParentId { get; set; }
|
||||||
private bool IsActive { get; set; } = true;
|
private bool IsActive { get; set; } = true;
|
||||||
private int SortOrder { get; set; }
|
private int SortOrder { get; set; }
|
||||||
@@ -28,9 +33,9 @@ public partial class CreateOrUpdateCategoryDialog
|
|||||||
Name = EditModel.Name;
|
Name = EditModel.Name;
|
||||||
Title = EditModel.Title;
|
Title = EditModel.Title;
|
||||||
Description = EditModel.Description;
|
Description = EditModel.Description;
|
||||||
|
ImagePath = EditModel.ImagePath;
|
||||||
ParentId = EditModel.ParentId;
|
ParentId = EditModel.ParentId;
|
||||||
IsActive = EditModel.IsActive;
|
IsActive = EditModel.IsActive;
|
||||||
Console.WriteLine(EditModel.IsActive);
|
|
||||||
SortOrder = EditModel.SortOrder;
|
SortOrder = EditModel.SortOrder;
|
||||||
}
|
}
|
||||||
else if (Model != null)
|
else if (Model != null)
|
||||||
@@ -38,6 +43,7 @@ public partial class CreateOrUpdateCategoryDialog
|
|||||||
Name = Model.Name;
|
Name = Model.Name;
|
||||||
Title = Model.Title;
|
Title = Model.Title;
|
||||||
Description = Model.Description;
|
Description = Model.Description;
|
||||||
|
ImagePath = Model.ImagePath;
|
||||||
ParentId = Model.ParentId;
|
ParentId = Model.ParentId;
|
||||||
IsActive = Model.IsActive;
|
IsActive = Model.IsActive;
|
||||||
SortOrder = Model.SortOrder;
|
SortOrder = Model.SortOrder;
|
||||||
@@ -46,37 +52,60 @@ public partial class CreateOrUpdateCategoryDialog
|
|||||||
|
|
||||||
private async Task Save()
|
private async Task Save()
|
||||||
{
|
{
|
||||||
if (IsEdit && EditModel != null)
|
if (_form != null)
|
||||||
{
|
{
|
||||||
var request = new UpdateCategoryRequest
|
await _form.Validate();
|
||||||
{
|
if (!_isValid) return;
|
||||||
Id = EditModel.Id,
|
|
||||||
Name = Name,
|
|
||||||
Title = Title,
|
|
||||||
Description = Description,
|
|
||||||
ParentId = ParentId,
|
|
||||||
IsActive = IsActive,
|
|
||||||
SortOrder = SortOrder
|
|
||||||
};
|
|
||||||
|
|
||||||
await CategoryContract.UpdateCategoryAsync(request);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var request = new CreateNewCategoryRequest
|
|
||||||
{
|
|
||||||
Name = Name,
|
|
||||||
Title = Title,
|
|
||||||
Description = Description,
|
|
||||||
ParentId = ParentId,
|
|
||||||
IsActive = IsActive,
|
|
||||||
SortOrder = SortOrder
|
|
||||||
};
|
|
||||||
|
|
||||||
await CategoryContract.CreateNewCategoryAsync(request);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MudDialog.Close(DialogResult.Ok(true));
|
_isSaving = true;
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (IsEdit && EditModel != null)
|
||||||
|
{
|
||||||
|
var request = new UpdateCategoryRequest
|
||||||
|
{
|
||||||
|
Id = EditModel.Id,
|
||||||
|
Name = Name,
|
||||||
|
Title = Title,
|
||||||
|
Description = Description,
|
||||||
|
ImagePath = ImagePath,
|
||||||
|
ParentId = ParentId,
|
||||||
|
IsActive = IsActive,
|
||||||
|
SortOrder = SortOrder
|
||||||
|
};
|
||||||
|
|
||||||
|
await CategoryContract.UpdateCategoryAsync(request);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var request = new CreateNewCategoryRequest
|
||||||
|
{
|
||||||
|
Name = Name,
|
||||||
|
Title = Title,
|
||||||
|
Description = Description,
|
||||||
|
ImagePath = ImagePath,
|
||||||
|
ParentId = ParentId,
|
||||||
|
IsActive = IsActive,
|
||||||
|
SortOrder = SortOrder
|
||||||
|
};
|
||||||
|
|
||||||
|
await CategoryContract.CreateNewCategoryAsync(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
MudDialog.Close(DialogResult.Ok(true));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isSaving = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task OnParentChanged(long? parentId)
|
private Task OnParentChanged(long? parentId)
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
@page "/club/features"
|
||||||
|
@attribute [Authorize(Roles = "Administrator")]
|
||||||
|
|
||||||
|
@using CMSMicroservice.Protobuf.Protos.Configuration
|
||||||
|
@using CMSMicroservice.Protobuf.Protos.ClubMembership
|
||||||
|
@using BackOffice.Pages.AutoComplete
|
||||||
|
|
||||||
|
@inject ConfigurationContract.ConfigurationContractClient ConfigClient
|
||||||
|
@inject ClubMembershipContract.ClubMembershipContractClient ClubClient
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<MudText Typo="Typo.h5" Class="mb-3">فیچرهای باشگاه</MudText>
|
||||||
|
|
||||||
|
@* ── G9: راهنمای ماتریس فیچر (Q28) ── *@
|
||||||
|
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info">
|
||||||
|
فیچرهای تیکخورده برای خریداران این پکیج فعال میشود. تغییر فیچرها روی کاربران فعلی بلافاصله اعمال میشود.
|
||||||
|
</MudAlert>
|
||||||
|
|
||||||
|
<MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true"
|
||||||
|
PanelClass="pt-4" @bind-ActivePanelIndex="_activeTab">
|
||||||
|
|
||||||
|
@* ── تب اول: لیست فیچرها ── *@
|
||||||
|
<MudTabPanel Text="فیچرهای تعریفشده" Icon="@Icons.Material.Filled.Star">
|
||||||
|
@if (_loadingFeatures)
|
||||||
|
{
|
||||||
|
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
|
||||||
|
}
|
||||||
|
else if (_features.Count == 0)
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Info" Variant="Variant.Text">فیچری تعریف نشده است.</MudAlert>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudGrid Spacing="2">
|
||||||
|
@foreach (var feature in _features)
|
||||||
|
{
|
||||||
|
<MudItem xs="12" sm="6" md="4">
|
||||||
|
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
|
||||||
|
<MudStack Spacing="1">
|
||||||
|
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
||||||
|
<MudText Typo="Typo.subtitle1"><strong>@feature.Title</strong></MudText>
|
||||||
|
<MudChip T="string" Size="Size.Small"
|
||||||
|
Color="@(feature.IsEnabled ? Color.Success : Color.Error)">
|
||||||
|
@(feature.IsEnabled ? "فعال" : "غیرفعال")
|
||||||
|
</MudChip>
|
||||||
|
</MudStack>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(feature.Description))
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body2" Color="Color.Secondary">@feature.Description</MudText>
|
||||||
|
}
|
||||||
|
<MudText Typo="Typo.caption">ترتیب: @feature.DisplayOrder</MudText>
|
||||||
|
</MudStack>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
}
|
||||||
|
</MudGrid>
|
||||||
|
}
|
||||||
|
</MudTabPanel>
|
||||||
|
|
||||||
|
@* ── تب دوم: فیچرهای کاربران ── *@
|
||||||
|
<MudTabPanel Text="فیچرهای کاربران" Icon="@Icons.Material.Filled.PeopleAlt">
|
||||||
|
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center" Class="mb-3">
|
||||||
|
<UserAutoComplete Label="جستجوی کاربر" @bind-SelectedUserId="_featureFilterUserId" />
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
||||||
|
StartIcon="@Icons.Material.Filled.Search"
|
||||||
|
OnClick="LoadUserFeatures">جستجو</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
@if (_loadingUserFeatures)
|
||||||
|
{
|
||||||
|
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
|
||||||
|
}
|
||||||
|
else if (_featureFilterUserId == null || _featureFilterUserId == 0)
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Info" Variant="Variant.Text">لطفاً یک کاربر انتخاب کنید.</MudAlert>
|
||||||
|
}
|
||||||
|
else if (_userFeatures.Count == 0)
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Info" Variant="Variant.Text">فیچری برای این کاربر یافت نشد.</MudAlert>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudDataGrid T="UserClubFeatureModel" Items="_userFeatures"
|
||||||
|
Hover="true" Dense="true"
|
||||||
|
Height="calc(100vh - 340px)">
|
||||||
|
<Columns>
|
||||||
|
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||||
|
<PropertyColumn Property="x => x.FeatureTitle" Title="عنوان فیچر" />
|
||||||
|
<TemplateColumn Title="توضیحات">
|
||||||
|
<CellTemplate>
|
||||||
|
<MudText Typo="Typo.body2">@(string.IsNullOrWhiteSpace(context.Item.FeatureDescription) ? "—" : context.Item.FeatureDescription)</MudText>
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<TemplateColumn Title="وضعیت">
|
||||||
|
<CellTemplate>
|
||||||
|
<MudSwitch T="bool" Value="context.Item.IsActive"
|
||||||
|
Color="Color.Success"
|
||||||
|
ValueChanged="@(val => ToggleFeature(context.Item, val))" />
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<TemplateColumn Title="تاریخ اختصاص">
|
||||||
|
<CellTemplate>
|
||||||
|
@if (context.Item.GrantedAt != null)
|
||||||
|
{
|
||||||
|
@context.Item.GrantedAt.ToDateTime().ToLocalTime().MiladiToJalali()
|
||||||
|
}
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<PropertyColumn Property="x => x.Notes" Title="یادداشت" />
|
||||||
|
</Columns>
|
||||||
|
<NoRecordsContent>
|
||||||
|
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||||
|
</NoRecordsContent>
|
||||||
|
<PagerContent>
|
||||||
|
<MudDataGridPager T="UserClubFeatureModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||||
|
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||||
|
RowsPerPageString="تعداد در صفحه" />
|
||||||
|
</PagerContent>
|
||||||
|
</MudDataGrid>
|
||||||
|
}
|
||||||
|
</MudTabPanel>
|
||||||
|
</MudTabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private int _activeTab;
|
||||||
|
private bool _loadingFeatures;
|
||||||
|
private bool _loadingUserFeatures;
|
||||||
|
|
||||||
|
// Features tab
|
||||||
|
private List<ClubFeatureModel> _features = new();
|
||||||
|
|
||||||
|
// User features tab
|
||||||
|
private long? _featureFilterUserId;
|
||||||
|
private List<UserClubFeatureModel> _userFeatures = new();
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await LoadFeatures();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadFeatures()
|
||||||
|
{
|
||||||
|
_loadingFeatures = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await ConfigClient.GetClubFeaturesAsync(new Google.Protobuf.WellKnownTypes.Empty());
|
||||||
|
_features = result?.Features?.ToList() ?? new();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"خطا در بارگذاری فیچرها: {ex.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_loadingFeatures = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadUserFeatures()
|
||||||
|
{
|
||||||
|
if (_featureFilterUserId == null || _featureFilterUserId == 0) return;
|
||||||
|
|
||||||
|
_loadingUserFeatures = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await ClubClient.GetUserClubFeaturesAsync(new GetUserClubFeaturesRequest
|
||||||
|
{
|
||||||
|
UserId = _featureFilterUserId.Value
|
||||||
|
});
|
||||||
|
_userFeatures = result?.Features?.ToList() ?? new();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_loadingUserFeatures = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ToggleFeature(UserClubFeatureModel feature, bool isActive)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ClubClient.ToggleUserClubFeatureAsync(new ToggleUserClubFeatureRequest
|
||||||
|
{
|
||||||
|
UserId = feature.UserId,
|
||||||
|
ClubFeatureId = feature.ClubFeatureId,
|
||||||
|
IsActive = isActive
|
||||||
|
});
|
||||||
|
|
||||||
|
feature.IsActive = isActive;
|
||||||
|
Snackbar.Add($"فیچر «{feature.FeatureTitle}» {(isActive ? "فعال" : "غیرفعال")} شد", Severity.Success);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
@page "/club"
|
||||||
|
@page "/club/members"
|
||||||
|
@page "/club/purchases"
|
||||||
|
@page "/club/customer-packages"
|
||||||
|
@page "/club/cycles"
|
||||||
|
@page "/club/history"
|
||||||
|
@page "/club/statistics"
|
||||||
|
@attribute [Authorize]
|
||||||
|
@using BackOffice.Pages.Club.Reports
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<MudText Typo="Typo.h5" Class="mb-3">باشگاه مشتریان</MudText>
|
||||||
|
|
||||||
|
<MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true"
|
||||||
|
PanelClass="pt-4" @bind-ActivePanelIndex="_activeTab">
|
||||||
|
<MudTabPanel Text="اعضا" Icon="@Icons.Material.Filled.Groups">
|
||||||
|
<ClubMembers />
|
||||||
|
</MudTabPanel>
|
||||||
|
<MudTabPanel Text="گزارش خرید پکیج" Icon="@Icons.Material.Filled.ShoppingBag">
|
||||||
|
<ClubPackagePurchasesReport />
|
||||||
|
</MudTabPanel>
|
||||||
|
<MudTabPanel Text="گزارش مشتریان پکیج" Icon="@Icons.Material.Filled.PeopleAlt">
|
||||||
|
<CustomerPackagePurchasesReport />
|
||||||
|
</MudTabPanel>
|
||||||
|
<MudTabPanel Text="گزارش دورهها" Icon="@Icons.Material.Filled.Loop">
|
||||||
|
<ClubCyclesReport />
|
||||||
|
</MudTabPanel>
|
||||||
|
<MudTabPanel Text="گزارش تاریخچه" Icon="@Icons.Material.Filled.History">
|
||||||
|
<ClubHistoriesReport />
|
||||||
|
</MudTabPanel>
|
||||||
|
<MudTabPanel Text="آمار" Icon="@Icons.Material.Filled.PieChart">
|
||||||
|
<Statistics />
|
||||||
|
</MudTabPanel>
|
||||||
|
</MudTabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private int _activeTab;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
var uri = Navigation.ToBaseRelativePath(Navigation.Uri).ToLowerInvariant().TrimEnd('/');
|
||||||
|
_activeTab = uri switch
|
||||||
|
{
|
||||||
|
"club/purchases" => 1,
|
||||||
|
"club/customer-packages" => 2,
|
||||||
|
"club/cycles" => 3,
|
||||||
|
"club/history" => 4,
|
||||||
|
"club/statistics" => 5,
|
||||||
|
_ => 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,30 @@
|
|||||||
@page "/club/members"
|
@* Merged into /club *@
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
|
|
||||||
@using BackOffice.BFF.ClubMembership.Protobuf
|
|
||||||
@using Google.Protobuf.WellKnownTypes
|
@using Google.Protobuf.WellKnownTypes
|
||||||
@using BackOffice.Pages.Club.Components
|
@using BackOffice.Pages.Club.Components
|
||||||
|
@using BackOffice.Pages.AutoComplete
|
||||||
|
@using CMSMicroservice.Protobuf.Protos.ClubMembership
|
||||||
|
|
||||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
<div>
|
||||||
<MudText Typo="Typo.h4" Class="mb-4">مدیریت اعضای باشگاه</MudText>
|
<MudText Typo="Typo.h4" Class="mb-4">مدیریت اعضای باشگاه</MudText>
|
||||||
|
|
||||||
|
@* ── G13: راهنمای اعضای باشگاه (Q28) ── *@
|
||||||
|
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info">
|
||||||
|
فیلتر بر اساس آخرین پکیج خریداریشده اعمال میشود. هر کاربر میتواند چندین دوره پکیج داشته باشد.
|
||||||
|
</MudAlert>
|
||||||
|
|
||||||
<MudDataGrid T="ClubMembershipModel"
|
<MudDataGrid T="ClubMembershipModel"
|
||||||
ServerData="@(new Func<GridState<ClubMembershipModel>, Task<GridData<ClubMembershipModel>>>(ServerReload))"
|
ServerData="@(new Func<GridState<ClubMembershipModel>, Task<GridData<ClubMembershipModel>>>(ServerReload))"
|
||||||
Hover="true"
|
Hover="true"
|
||||||
@ref="_gridData"
|
@ref="_gridData"
|
||||||
Height="75vh">
|
Height="calc(100vh - 240px)">
|
||||||
<ToolBarContent>
|
<ToolBarContent>
|
||||||
<MudText Typo="Typo.h6">لیست اعضا</MudText>
|
<MudText Typo="Typo.h6">لیست اعضا</MudText>
|
||||||
<MudSpacer />
|
<MudSpacer />
|
||||||
<MudStack Row="true" Spacing="2">
|
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
||||||
<MudSwitch @bind-Value="_filterIsActive"
|
<UserAutoComplete Label="جستجوی کاربر" @bind-SelectedUserId="_filterUserId" />
|
||||||
|
<MudSwitch T="bool" @bind-Value="_filterIsActive"
|
||||||
Color="Color.Success"
|
Color="Color.Success"
|
||||||
Label="فقط فعالها" />
|
Label="فقط فعالها" />
|
||||||
<MudButton Variant="Variant.Filled"
|
<MudButton Variant="Variant.Filled"
|
||||||
@@ -32,6 +39,12 @@
|
|||||||
StartIcon="@Icons.Material.Filled.Add">
|
StartIcon="@Icons.Material.Filled.Add">
|
||||||
فعالسازی عضو جدید
|
فعالسازی عضو جدید
|
||||||
</MudButton>
|
</MudButton>
|
||||||
|
<MudButton Variant="Variant.Outlined"
|
||||||
|
Color="Color.Info"
|
||||||
|
OnClick="ExportToExcel"
|
||||||
|
StartIcon="@Icons.Material.Filled.FileDownload">
|
||||||
|
خروجی Excel
|
||||||
|
</MudButton>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</ToolBarContent>
|
</ToolBarContent>
|
||||||
<Columns>
|
<Columns>
|
||||||
@@ -58,14 +71,14 @@
|
|||||||
|
|
||||||
<PropertyColumn Property="x => x.ActivatedAt" Title="تاریخ فعالسازی">
|
<PropertyColumn Property="x => x.ActivatedAt" Title="تاریخ فعالسازی">
|
||||||
<CellTemplate>
|
<CellTemplate>
|
||||||
@context.Item.ActivatedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd")
|
@(context.Item.ActivatedAt != null ? context.Item.ActivatedAt.ToDateTime().ToLocalTime().MiladiToJalali() : "—")
|
||||||
</CellTemplate>
|
</CellTemplate>
|
||||||
</PropertyColumn>
|
</PropertyColumn>
|
||||||
|
|
||||||
<PropertyColumn Property="x => x.ExpiresAt" Title="تاریخ انقضا">
|
<PropertyColumn Property="x => x.ExpiresAt" Title="تاریخ انقضا">
|
||||||
<CellTemplate>
|
<CellTemplate>
|
||||||
<MudText Color="@(context.Item.IsExpired ? Color.Error : Color.Success)">
|
<MudText Color="@(context.Item.IsExpired ? Color.Error : Color.Success)">
|
||||||
@context.Item.ExpiresAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd")
|
@(context.Item.ExpiresAt != null ? context.Item.ExpiresAt.ToDateTime().ToLocalTime().MiladiToJalali() : "—")
|
||||||
</MudText>
|
</MudText>
|
||||||
</CellTemplate>
|
</CellTemplate>
|
||||||
</PropertyColumn>
|
</PropertyColumn>
|
||||||
@@ -102,11 +115,14 @@
|
|||||||
</CellTemplate>
|
</CellTemplate>
|
||||||
</TemplateColumn>
|
</TemplateColumn>
|
||||||
</Columns>
|
</Columns>
|
||||||
|
<NoRecordsContent>
|
||||||
|
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||||
|
</NoRecordsContent>
|
||||||
<PagerContent>
|
<PagerContent>
|
||||||
<MudDataGridPager T="ClubMembershipModel"
|
<MudDataGridPager T="ClubMembershipModel"
|
||||||
PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||||
RowsPerPageString="تعداد سطرهای صفحه" />
|
RowsPerPageString="تعداد در صفحه" />
|
||||||
</PagerContent>
|
</PagerContent>
|
||||||
</MudDataGrid>
|
</MudDataGrid>
|
||||||
</MudContainer>
|
</div>
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
using BackOffice.BFF.ClubMembership.Protobuf;
|
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
using Google.Protobuf.WellKnownTypes;
|
using Google.Protobuf.WellKnownTypes;
|
||||||
using BackOffice.Pages.Club.Components;
|
using BackOffice.Pages.Club.Components;
|
||||||
|
using CMSMicroservice.Protobuf.Protos.ClubMembership;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
using System.Text;
|
||||||
|
using DateTimeConverterCL;
|
||||||
|
|
||||||
namespace BackOffice.Pages.Club;
|
namespace BackOffice.Pages.Club;
|
||||||
|
|
||||||
public partial class ClubMembers
|
public partial class ClubMembers
|
||||||
{
|
{
|
||||||
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubContract { get; set; }
|
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubContract { get; set; }
|
||||||
|
[Inject] public IJSRuntime JsRuntime { get; set; }
|
||||||
|
|
||||||
private MudDataGrid<ClubMembershipModel> _gridData;
|
private MudDataGrid<ClubMembershipModel> _gridData;
|
||||||
private bool? _filterIsActive = true;
|
private bool _filterIsActive = true;
|
||||||
|
private long? _filterUserId;
|
||||||
|
|
||||||
private async Task<GridData<ClubMembershipModel>> ServerReload(GridState<ClubMembershipModel> state)
|
private async Task<GridData<ClubMembershipModel>> ServerReload(GridState<ClubMembershipModel> state)
|
||||||
{
|
{
|
||||||
@@ -23,10 +28,11 @@ public partial class ClubMembers
|
|||||||
PageSize = state.PageSize
|
PageSize = state.PageSize
|
||||||
};
|
};
|
||||||
|
|
||||||
if (_filterIsActive.HasValue)
|
// Always use the filter value since it's now a bool (not nullable)
|
||||||
{
|
request.IsActive = _filterIsActive;
|
||||||
request.IsActive = _filterIsActive.Value;
|
|
||||||
}
|
if (_filterUserId.HasValue && _filterUserId.Value > 0)
|
||||||
|
request.UserId = _filterUserId.Value;
|
||||||
|
|
||||||
var result = await ClubContract.GetAllClubMembershipsAsync(request);
|
var result = await ClubContract.GetAllClubMembershipsAsync(request);
|
||||||
|
|
||||||
@@ -110,4 +116,61 @@ public partial class ClubMembers
|
|||||||
await ApplyFilter();
|
await ApplyFilter();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ExportToExcel()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var request = new GetAllClubMembershipsRequest
|
||||||
|
{
|
||||||
|
PageIndex = 1,
|
||||||
|
PageSize = 1000,
|
||||||
|
IsActive = _filterIsActive
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await ClubContract.GetAllClubMembershipsAsync(request);
|
||||||
|
|
||||||
|
if (result?.Models == null || !result.Models.Any())
|
||||||
|
{
|
||||||
|
Snackbar.Add("دادهای برای خروجی وجود ندارد.", Severity.Info);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("شناسه,کاربر,نام کاربر,پکیج,کد فعالسازی,تاریخ فعالسازی,تاریخ انقضا,فعال,منقضی");
|
||||||
|
|
||||||
|
foreach (var m in result.Models)
|
||||||
|
{
|
||||||
|
sb.AppendLine(string.Join(",",
|
||||||
|
m.Id,
|
||||||
|
m.UserId,
|
||||||
|
EscapeCsv(m.UserName),
|
||||||
|
EscapeCsv(m.PackageName),
|
||||||
|
EscapeCsv(m.ActivationCode),
|
||||||
|
m.ActivatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime(),
|
||||||
|
m.ExpiresAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime(),
|
||||||
|
m.IsActive ? "فعال" : "غیرفعال",
|
||||||
|
m.IsExpired ? "بله" : "خیر"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
|
||||||
|
var base64 = Convert.ToBase64String(bytes);
|
||||||
|
var filename = $"club-members-{DateTime.Now:yyyyMMddHHmmss}.csv";
|
||||||
|
|
||||||
|
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
|
||||||
|
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string EscapeCsv(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value)) return "";
|
||||||
|
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
|
||||||
|
return $"\"{value.Replace("\"", "\"\"")}\"";
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,39 @@
|
|||||||
@using BackOffice.BFF.ClubMembership.Protobuf
|
@using CMSMicroservice.Protobuf.Protos.ClubMembership
|
||||||
|
@using CMSMicroservice.Protobuf.Protos.Package
|
||||||
|
@using BackOffice.Pages.AutoComplete
|
||||||
|
|
||||||
<MudDialog>
|
<MudDialog>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<MudForm @ref="_form" @bind-IsValid="_formValid">
|
<MudForm @ref="_form" @bind-IsValid="_formValid">
|
||||||
<MudStack Spacing="3">
|
<MudStack Spacing="3">
|
||||||
<MudNumericField @bind-Value="_model.UserId"
|
<UserAutoComplete Label="انتخاب کاربر" @bind-SelectedUserId="_model.UserId" />
|
||||||
Label="شناسه کاربر"
|
|
||||||
Required="true"
|
|
||||||
RequiredError="شناسه کاربر الزامی است"
|
|
||||||
Variant="Variant.Outlined" />
|
|
||||||
|
|
||||||
<MudNumericField @bind-Value="_model.PackageId"
|
@if (_isLoadingPackages)
|
||||||
Label="شناسه پکیج"
|
{
|
||||||
Required="true"
|
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Height="56px" />
|
||||||
RequiredError="شناسه پکیج الزامی است"
|
}
|
||||||
Variant="Variant.Outlined" />
|
else
|
||||||
|
{
|
||||||
<MudNumericField @bind-Value="_model.DurationMonths"
|
<MudSelect T="long" @bind-Value="_model.PackageId"
|
||||||
Label="مدت زمان (ماه)"
|
Label="انتخاب پکیج"
|
||||||
Required="true"
|
Required="true"
|
||||||
RequiredError="مدت زمان الزامی است"
|
RequiredError="انتخاب پکیج الزامی است"
|
||||||
Min="1"
|
Variant="Variant.Outlined"
|
||||||
Max="12"
|
AnchorOrigin="Origin.BottomCenter">
|
||||||
Variant="Variant.Outlined" />
|
@foreach (var pkg in _packages)
|
||||||
|
{
|
||||||
|
<MudSelectItem T="long" Value="pkg.Id">
|
||||||
|
@pkg.Title — @($"{pkg.Price:N0}") ریال
|
||||||
|
</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
}
|
||||||
|
|
||||||
<MudAlert Severity="Severity.Info" Dense="true">
|
<MudAlert Severity="Severity.Info" Dense="true">
|
||||||
<MudText Typo="Typo.body2">
|
<MudText Typo="Typo.body2">
|
||||||
با تایید، عضویت باشگاه برای کاربر فعال میشود.
|
با تایید، عضویت باشگاه برای کاربر فعال میشود.
|
||||||
|
<br/>
|
||||||
|
<small>عضویت باشگاه بدون محدودیت زمانی است.</small>
|
||||||
</MudText>
|
</MudText>
|
||||||
</MudAlert>
|
</MudAlert>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
@@ -37,7 +44,7 @@
|
|||||||
<MudButton Color="Color.Success"
|
<MudButton Color="Color.Success"
|
||||||
Variant="Variant.Filled"
|
Variant="Variant.Filled"
|
||||||
OnClick="Submit"
|
OnClick="Submit"
|
||||||
Disabled="@(!_formValid || _isSubmitting)">
|
Disabled="@(!_formValid || _isSubmitting || _isLoadingPackages)">
|
||||||
@if (_isSubmitting)
|
@if (_isSubmitting)
|
||||||
{
|
{
|
||||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" />
|
<MudProgressCircular Size="Size.Small" Indeterminate="true" />
|
||||||
@@ -54,12 +61,49 @@
|
|||||||
@code {
|
@code {
|
||||||
[CascadingParameter] IMudDialogInstance MudDialog { get; set; }
|
[CascadingParameter] IMudDialogInstance MudDialog { get; set; }
|
||||||
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubContract { get; set; }
|
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubContract { get; set; }
|
||||||
|
[Inject] public PackageContract.PackageContractClient PackageClient { get; set; }
|
||||||
|
|
||||||
private MudForm _form;
|
private MudForm _form;
|
||||||
private bool _formValid;
|
private bool _formValid;
|
||||||
private bool _isSubmitting;
|
private bool _isSubmitting;
|
||||||
|
private bool _isLoadingPackages = true;
|
||||||
|
private List<GetAllPackageByFilterResponseModel> _packages = new();
|
||||||
private ActivateModel _model = new();
|
private ActivateModel _model = new();
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await LoadPackages();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadPackages()
|
||||||
|
{
|
||||||
|
_isLoadingPackages = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var request = new GetAllPackageByFilterRequest
|
||||||
|
{
|
||||||
|
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
|
||||||
|
{
|
||||||
|
PageNumber = 1,
|
||||||
|
PageSize = 50
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var result = await PackageClient.GetAllPackageByFilterAsync(request);
|
||||||
|
if (result?.Models != null)
|
||||||
|
{
|
||||||
|
_packages = result.Models.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"خطا در بارگذاری پکیجها: {ex.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isLoadingPackages = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void Cancel() => MudDialog.Cancel();
|
private void Cancel() => MudDialog.Cancel();
|
||||||
|
|
||||||
private async Task Submit()
|
private async Task Submit()
|
||||||
@@ -67,19 +111,31 @@
|
|||||||
await _form.Validate();
|
await _form.Validate();
|
||||||
if (!_formValid) return;
|
if (!_formValid) return;
|
||||||
|
|
||||||
|
if (!_model.UserId.HasValue || _model.UserId.Value <= 0)
|
||||||
|
{
|
||||||
|
Snackbar.Add("لطفاً کاربر را انتخاب کنید.", Severity.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_model.PackageId <= 0)
|
||||||
|
{
|
||||||
|
Snackbar.Add("لطفاً پکیج را انتخاب کنید.", Severity.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_isSubmitting = true;
|
_isSubmitting = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var request = new ActivateClubMembershipRequest
|
var request = new ActivateClubMembershipRequest
|
||||||
{
|
{
|
||||||
UserId = _model.UserId,
|
UserId = _model.UserId.Value,
|
||||||
PackageId = _model.PackageId,
|
PackageId = _model.PackageId,
|
||||||
DurationMonths = _model.DurationMonths
|
ForceActivation = true
|
||||||
};
|
};
|
||||||
|
|
||||||
var response = await ClubContract.ActivateClubMembershipAsync(request);
|
var response = await ClubContract.ActivateClubMembershipAsync(request);
|
||||||
|
|
||||||
Snackbar.Add($"عضویت با موفقیت فعال شد.", Severity.Success);
|
Snackbar.Add("عضویت با موفقیت فعال شد.", Severity.Success);
|
||||||
MudDialog.Close(DialogResult.Ok(true));
|
MudDialog.Close(DialogResult.Ok(true));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -94,8 +150,7 @@
|
|||||||
|
|
||||||
private class ActivateModel
|
private class ActivateModel
|
||||||
{
|
{
|
||||||
public long UserId { get; set; }
|
public long? UserId { get; set; }
|
||||||
public long PackageId { get; set; }
|
public long PackageId { get; set; }
|
||||||
public int DurationMonths { get; set; } = 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@using BackOffice.BFF.ClubMembership.Protobuf
|
@using CMSMicroservice.Protobuf.Protos.ClubMembership
|
||||||
|
|
||||||
<MudDialog>
|
<MudDialog>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@using BackOffice.BFF.ClubMembership.Protobuf
|
@using CMSMicroservice.Protobuf.Protos.ClubMembership
|
||||||
|
|
||||||
<MudDialog>
|
<MudDialog>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@@ -33,23 +33,11 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>پکیج:</strong></td>
|
<td><strong>پکیج:</strong></td>
|
||||||
<td>@Member.PackageName</td>
|
<td>@(string.IsNullOrEmpty(Member.PackageName) ? "—" : Member.PackageName)</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><strong>کد فعالسازی:</strong></td>
|
|
||||||
<td dir="ltr">@Member.ActivationCode</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>تاریخ فعالسازی:</strong></td>
|
<td><strong>تاریخ فعالسازی:</strong></td>
|
||||||
<td>@Member.ActivatedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm")</td>
|
<td>@(Member.ActivatedAt != null ? Member.ActivatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() : "—")</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><strong>تاریخ انقضا:</strong></td>
|
|
||||||
<td>
|
|
||||||
<MudText Color="@(Member.IsExpired ? Color.Error : Color.Success)">
|
|
||||||
@Member.ExpiresAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm")
|
|
||||||
</MudText>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>وضعیت:</strong></td>
|
<td><strong>وضعیت:</strong></td>
|
||||||
@@ -62,31 +50,16 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>منقضی شده:</strong></td>
|
<td><strong>تاریخ ایجاد:</strong></td>
|
||||||
<td>
|
<td>@Member.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td>
|
||||||
<MudChip T="string"
|
|
||||||
Color="@(Member.IsExpired ? Color.Warning : Color.Default)"
|
|
||||||
Size="Size.Small">
|
|
||||||
@(Member.IsExpired ? "بله" : "خیر")
|
|
||||||
</MudChip>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</MudSimpleTable>
|
</MudSimpleTable>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
<MudPaper Class="pa-3" Elevation="0">
|
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text">
|
||||||
<MudText Typo="Typo.h6" Color="Color.Primary">تاریخچه</MudText>
|
تاریخچه تغییرات عضویت در تب «گزارش تاریخچه» باشگاه قابل مشاهده است.
|
||||||
<MudDivider Class="my-2" />
|
</MudAlert>
|
||||||
<MudSimpleTable Dense="true">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td><strong>تاریخ ایجاد:</strong></td>
|
|
||||||
<td>@Member.Created.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss")</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</MudSimpleTable>
|
|
||||||
</MudPaper>
|
|
||||||
</MudStack>
|
</MudStack>
|
||||||
}
|
}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -98,6 +71,5 @@
|
|||||||
@code {
|
@code {
|
||||||
[CascadingParameter] IMudDialogInstance MudDialog { get; set; }
|
[CascadingParameter] IMudDialogInstance MudDialog { get; set; }
|
||||||
[Parameter] public ClubMembershipModel Member { get; set; }
|
[Parameter] public ClubMembershipModel Member { get; set; }
|
||||||
|
|
||||||
private void Close() => MudDialog.Close();
|
private void Close() => MudDialog.Close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
@using BackOffice.Common.BaseComponents
|
||||||
|
@using BackOffice.Pages.AutoComplete
|
||||||
|
@using CMSMicroservice.Protobuf.Protos.ClubMembershipCycle
|
||||||
|
@using Google.Protobuf.WellKnownTypes
|
||||||
|
|
||||||
|
@inject ClubMembershipCycleContract.ClubMembershipCycleContractClient CycleClient
|
||||||
|
|
||||||
|
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3">
|
||||||
|
گزارش دورههای عضویت باشگاه — هر خرید پکیج یک دور جدید ایجاد میکند.
|
||||||
|
</MudAlert>
|
||||||
|
|
||||||
|
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
|
||||||
|
<Filters>
|
||||||
|
<MudItem xs="12" sm="6" md="3">
|
||||||
|
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_userId" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6" md="3">
|
||||||
|
<MudSelect T="bool?" Clearable="true" Label="دوره جاری" Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense" @bind-Value="_isCurrent">
|
||||||
|
<MudSelectItem T="bool?" Value="@((bool?)true)">فقط دوره جاری</MudSelectItem>
|
||||||
|
<MudSelectItem T="bool?" Value="@((bool?)false)">دورههای قبلی</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6" md="3">
|
||||||
|
<MudSelect T="int?" Clearable="true" Label="وضعیت جادویی" Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense" @bind-Value="_magicState">
|
||||||
|
<MudSelectItem T="int?" Value="0">عادی</MudSelectItem>
|
||||||
|
<MudSelectItem T="int?" Value="1">در حالت جادویی</MudSelectItem>
|
||||||
|
<MudSelectItem T="int?" Value="2">جادویی تکمیلشده</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6" md="4">
|
||||||
|
<DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه خرید پکیج" />
|
||||||
|
</MudItem>
|
||||||
|
</Filters>
|
||||||
|
<Content>
|
||||||
|
<MudGrid Class="mb-3" Spacing="2">
|
||||||
|
<MudItem xs="4">
|
||||||
|
<MudPaper Class="pa-3" Outlined="true">
|
||||||
|
<MudText Typo="Typo.caption" Color="Color.Secondary">کل دورهها</MudText>
|
||||||
|
<MudText Typo="Typo.h6">@_totalCount.ToString("N0")</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="4">
|
||||||
|
<MudPaper Class="pa-3" Outlined="true">
|
||||||
|
<MudText Typo="Typo.caption" Color="Color.Secondary">دوره جاری</MudText>
|
||||||
|
<MudText Typo="Typo.h6">@_currentCount.ToString("N0")</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="4">
|
||||||
|
<MudPaper Class="pa-3" Outlined="true">
|
||||||
|
<MudText Typo="Typo.caption" Color="Color.Secondary">جادویی فعال</MudText>
|
||||||
|
<MudText Typo="Typo.h6">@_magicActive.ToString("N0")</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
|
||||||
|
<MudDataGrid @ref="_grid" T="ClubMembershipCycleModel" ServerData="LoadData" ReadOnly="true"
|
||||||
|
Hover="true" Dense="true" Height="calc(100vh - 420px)">
|
||||||
|
<Columns>
|
||||||
|
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||||
|
<PropertyColumn Property="x => x.UserName" Title="کاربر" />
|
||||||
|
<PropertyColumn Property="x => x.CycleNumber" Title="شماره دور" />
|
||||||
|
<PropertyColumn Property="x => x.PackageName" Title="پکیج" />
|
||||||
|
<TemplateColumn Title="مبلغ (ریال)">
|
||||||
|
<CellTemplate>@context.Item.PackageAmount.ToString("N0")</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<TemplateColumn Title="روش خرید">
|
||||||
|
<CellTemplate>@ReportLabels.GetPurchaseMethodLabel(context.Item.PurchaseMethod)</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<TemplateColumn Title="تاریخ خرید">
|
||||||
|
<CellTemplate>@context.Item.PackagePurchasedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<TemplateColumn Title="وضعیت جادویی">
|
||||||
|
<CellTemplate>
|
||||||
|
@ReportLabels.GetMagicStateLabel(null,
|
||||||
|
context.Item.MagicStartedAt?.ToDateTime(),
|
||||||
|
context.Item.MagicCompletedAt?.ToDateTime())
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<TemplateColumn Title="دوره جاری">
|
||||||
|
<CellTemplate>
|
||||||
|
<MudChip T="string" Size="Size.Small" Color="@(context.Item.IsCurrentCycle ? Color.Success : Color.Default)">
|
||||||
|
@(context.Item.IsCurrentCycle ? "بله" : "خیر")
|
||||||
|
</MudChip>
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
</Columns>
|
||||||
|
<NoRecordsContent>
|
||||||
|
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||||
|
</NoRecordsContent>
|
||||||
|
<PagerContent>
|
||||||
|
<MudDataGridPager T="ClubMembershipCycleModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||||
|
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||||
|
RowsPerPageString="تعداد در صفحه" />
|
||||||
|
</PagerContent>
|
||||||
|
</MudDataGrid>
|
||||||
|
</Content>
|
||||||
|
</BasePageComponent>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private BasePageComponent? _basePage;
|
||||||
|
private MudDataGrid<ClubMembershipCycleModel>? _grid;
|
||||||
|
private long? _userId;
|
||||||
|
private bool? _isCurrent;
|
||||||
|
private int? _magicState;
|
||||||
|
private DateTime? _fromDate, _toDate;
|
||||||
|
private long _totalCount, _currentCount, _magicActive;
|
||||||
|
|
||||||
|
private GetAllClubMembershipCycleByFilterFilter BuildFilter() => new()
|
||||||
|
{
|
||||||
|
UserId = _userId > 0 ? _userId : null,
|
||||||
|
IsCurrentCycle = _isCurrent,
|
||||||
|
MagicState = _magicState,
|
||||||
|
PackagePurchasedFrom = _fromDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_fromDate.Value, DateTimeKind.Utc)) : null,
|
||||||
|
PackagePurchasedTo = _toDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_toDate.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Utc)) : null
|
||||||
|
};
|
||||||
|
|
||||||
|
private async Task<GridData<ClubMembershipCycleModel>> LoadData(GridState<ClubMembershipCycleModel> state)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var filter = BuildFilter();
|
||||||
|
var summary = await CycleClient.GetClubMembershipCycleSummaryAsync(new GetClubMembershipCycleSummaryRequest { Filter = filter });
|
||||||
|
_totalCount = summary.TotalCount;
|
||||||
|
_currentCount = summary.CurrentCycleCount;
|
||||||
|
_magicActive = summary.MagicActiveCount;
|
||||||
|
|
||||||
|
var result = await CycleClient.GetAllClubMembershipCycleByFilterAsync(new GetAllClubMembershipCycleByFilterRequest
|
||||||
|
{
|
||||||
|
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize },
|
||||||
|
Filter = filter
|
||||||
|
});
|
||||||
|
return new GridData<ClubMembershipCycleModel>
|
||||||
|
{
|
||||||
|
Items = result?.Models?.ToList() ?? new(),
|
||||||
|
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||||
|
return new GridData<ClubMembershipCycleModel>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnFilterSubmit() { if (_grid != null) await _grid.ReloadServerData(); }
|
||||||
|
private async Task OnFilterCleared()
|
||||||
|
{
|
||||||
|
_userId = null; _isCurrent = null; _magicState = null; _fromDate = _toDate = null;
|
||||||
|
if (_grid != null) await _grid.ReloadServerData();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
@using BackOffice.Common.BaseComponents
|
||||||
|
@using BackOffice.Pages.AutoComplete
|
||||||
|
@using CMSMicroservice.Protobuf.Protos.ClubMembership
|
||||||
|
@using CMSMicroservice.Protobuf.Protos.ClubMembershipCycleHistory
|
||||||
|
@using Google.Protobuf.WellKnownTypes
|
||||||
|
|
||||||
|
@inject ClubMembershipContract.ClubMembershipContractClient ClubClient
|
||||||
|
@inject ClubMembershipCycleHistoryContract.ClubMembershipCycleHistoryContractClient CycleHistoryClient
|
||||||
|
|
||||||
|
<MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true" PanelClass="pt-4">
|
||||||
|
<MudTabPanel Text="تغییرات عضویت" Icon="@Icons.Material.Filled.History">
|
||||||
|
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3">
|
||||||
|
تاریخچه فعال/غیرفعال شدن عضویت باشگاه.
|
||||||
|
</MudAlert>
|
||||||
|
<MudStack Row="true" Spacing="2" Class="mb-3">
|
||||||
|
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_memUserId" />
|
||||||
|
<MudSelect T="int?" Clearable="true" Label="عملیات" Variant="Variant.Outlined" Margin="Margin.Dense"
|
||||||
|
@bind-Value="_memAction" Style="max-width:200px;">
|
||||||
|
<MudSelectItem T="int?" Value="0">فعالسازی</MudSelectItem>
|
||||||
|
<MudSelectItem T="int?" Value="1">غیرفعالسازی</MudSelectItem>
|
||||||
|
<MudSelectItem T="int?" Value="2">ویرایش</MudSelectItem>
|
||||||
|
<MudSelectItem T="int?" Value="3">اصلاح دستی</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ReloadMembership">جستجو</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
<MudDataGrid @ref="_memGrid" T="ClubMembershipHistoryModel" ServerData="LoadMembershipHistory" ReadOnly="true"
|
||||||
|
Hover="true" Dense="true" Height="calc(100vh - 400px)">
|
||||||
|
<Columns>
|
||||||
|
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||||
|
<PropertyColumn Property="x => x.UserName" Title="کاربر" />
|
||||||
|
<TemplateColumn Title="وضعیت">
|
||||||
|
<CellTemplate>@ReportLabels.FormatActiveChange(context.Item.OldIsActive, context.Item.NewIsActive)</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<TemplateColumn Title="مشارکت اولیه">
|
||||||
|
<CellTemplate>
|
||||||
|
@(context.Item.OldInitialContribution != null ? context.Item.OldInitialContribution.Value.ToString("N0") : "—")
|
||||||
|
→ @(context.Item.NewInitialContribution != null ? context.Item.NewInitialContribution.Value.ToString("N0") : "—")
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<TemplateColumn Title="عملیات">
|
||||||
|
<CellTemplate>@ReportLabels.GetClubMembershipActionLabel(context.Item.Action)</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<PropertyColumn Property="x => x.PerformedBy" Title="انجامدهنده" />
|
||||||
|
<PropertyColumn Property="x => x.Reason" Title="دلیل" />
|
||||||
|
<TemplateColumn Title="تاریخ">
|
||||||
|
<CellTemplate>@context.Item.CreatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
</Columns>
|
||||||
|
<NoRecordsContent>
|
||||||
|
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||||
|
</NoRecordsContent>
|
||||||
|
<PagerContent>
|
||||||
|
<MudDataGridPager T="ClubMembershipHistoryModel" PageSizeOptions="@(new int[] { 20, 50, 100 })" />
|
||||||
|
</PagerContent>
|
||||||
|
</MudDataGrid>
|
||||||
|
</MudTabPanel>
|
||||||
|
|
||||||
|
<MudTabPanel Text="تغییرات دوره" Icon="@Icons.Material.Filled.Timeline">
|
||||||
|
<MudAlert Severity="Severity.Warning" Variant="Variant.Text" Dense="true" Class="mb-3">
|
||||||
|
ردیابی تغییرات دوره از زمان deploy فعال شد. رکوردهای قدیمیتر ممکن است خالی باشند.
|
||||||
|
</MudAlert>
|
||||||
|
<MudStack Row="true" Spacing="2" Class="mb-3">
|
||||||
|
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_cycleUserId" />
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ReloadCycle">جستجو</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
<MudDataGrid @ref="_cycleGrid" T="ClubMembershipCycleHistoryModel" ServerData="LoadCycleHistory" ReadOnly="true"
|
||||||
|
Hover="true" Dense="true" Height="calc(100vh - 400px)">
|
||||||
|
<Columns>
|
||||||
|
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||||
|
<PropertyColumn Property="x => x.UserName" Title="کاربر" />
|
||||||
|
<PropertyColumn Property="x => x.CycleNumber" Title="دور" />
|
||||||
|
<TemplateColumn Title="دوره جاری">
|
||||||
|
<CellTemplate>@ReportLabels.FormatBoolChange(context.Item.OldIsCurrentCycle, context.Item.NewIsCurrentCycle)</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<TemplateColumn Title="عملیات">
|
||||||
|
<CellTemplate>@ReportLabels.GetClubCycleActionLabel(context.Item.Action)</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<PropertyColumn Property="x => x.PerformedBy" Title="انجامدهنده" />
|
||||||
|
<PropertyColumn Property="x => x.Reason" Title="دلیل" />
|
||||||
|
<TemplateColumn Title="تاریخ">
|
||||||
|
<CellTemplate>@context.Item.CreatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
</Columns>
|
||||||
|
<NoRecordsContent>
|
||||||
|
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||||
|
</NoRecordsContent>
|
||||||
|
<PagerContent>
|
||||||
|
<MudDataGridPager T="ClubMembershipCycleHistoryModel" PageSizeOptions="@(new int[] { 20, 50, 100 })" />
|
||||||
|
</PagerContent>
|
||||||
|
</MudDataGrid>
|
||||||
|
</MudTabPanel>
|
||||||
|
</MudTabs>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private MudDataGrid<ClubMembershipHistoryModel>? _memGrid;
|
||||||
|
private MudDataGrid<ClubMembershipCycleHistoryModel>? _cycleGrid;
|
||||||
|
private long? _memUserId, _cycleUserId;
|
||||||
|
private int? _memAction;
|
||||||
|
|
||||||
|
private async Task<GridData<ClubMembershipHistoryModel>> LoadMembershipHistory(GridState<ClubMembershipHistoryModel> state)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await ClubClient.GetClubMembershipHistoryAsync(new GetClubMembershipHistoryRequest
|
||||||
|
{
|
||||||
|
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize },
|
||||||
|
Filter = new GetClubMembershipHistoryFilter
|
||||||
|
{
|
||||||
|
UserId = _memUserId > 0 ? _memUserId : null,
|
||||||
|
Action = _memAction
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return new GridData<ClubMembershipHistoryModel>
|
||||||
|
{
|
||||||
|
Items = result?.Models?.ToList() ?? new(),
|
||||||
|
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||||
|
return new GridData<ClubMembershipHistoryModel>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<GridData<ClubMembershipCycleHistoryModel>> LoadCycleHistory(GridState<ClubMembershipCycleHistoryModel> state)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await CycleHistoryClient.GetAllClubMembershipCycleHistoryByFilterAsync(new GetAllClubMembershipCycleHistoryByFilterRequest
|
||||||
|
{
|
||||||
|
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize },
|
||||||
|
Filter = new GetAllClubMembershipCycleHistoryByFilterFilter
|
||||||
|
{
|
||||||
|
UserId = _cycleUserId > 0 ? _cycleUserId : null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return new GridData<ClubMembershipCycleHistoryModel>
|
||||||
|
{
|
||||||
|
Items = result?.Models?.ToList() ?? new(),
|
||||||
|
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||||
|
return new GridData<ClubMembershipCycleHistoryModel>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReloadMembership() { if (_memGrid != null) await _memGrid.ReloadServerData(); }
|
||||||
|
private async Task ReloadCycle() { if (_cycleGrid != null) await _cycleGrid.ReloadServerData(); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
@using BackOffice.Common.BaseComponents
|
||||||
|
@using BackOffice.Pages.AutoComplete
|
||||||
|
@using CMSMicroservice.Protobuf.Protos.UserPackagePurchase
|
||||||
|
@using Google.Protobuf.WellKnownTypes
|
||||||
|
|
||||||
|
@inject UserPackagePurchaseContract.UserPackagePurchaseContractClient PurchaseClient
|
||||||
|
|
||||||
|
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3">
|
||||||
|
گزارش read-only خریدهای پکیج — فقط مشاهده و فیلتر.
|
||||||
|
</MudAlert>
|
||||||
|
|
||||||
|
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
|
||||||
|
<Filters>
|
||||||
|
<MudItem xs="12" sm="6" md="3">
|
||||||
|
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_userId" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6" md="3">
|
||||||
|
<MudSelect T="int?" Clearable="true" Label="روش خرید" Variant="Variant.Outlined"
|
||||||
|
Margin="Margin.Dense" @bind-Value="_purchaseMethod">
|
||||||
|
<MudSelectItem T="int?" Value="1">دایا</MudSelectItem>
|
||||||
|
<MudSelectItem T="int?" Value="3">دستی</MudSelectItem>
|
||||||
|
<MudSelectItem T="int?" Value="2">درگاه</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6" md="4">
|
||||||
|
<DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه خرید" />
|
||||||
|
</MudItem>
|
||||||
|
</Filters>
|
||||||
|
<Content>
|
||||||
|
<MudGrid Class="mb-3" Spacing="2">
|
||||||
|
<MudItem xs="6" sm="4">
|
||||||
|
<MudPaper Class="pa-3" Outlined="true">
|
||||||
|
<MudText Typo="Typo.caption" Color="Color.Secondary">تعداد</MudText>
|
||||||
|
<MudText Typo="Typo.h6">@_totalCount.ToString("N0")</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="6" sm="4">
|
||||||
|
<MudPaper Class="pa-3" Outlined="true">
|
||||||
|
<MudText Typo="Typo.caption" Color="Color.Secondary">جمع مبلغ (ریال)</MudText>
|
||||||
|
<MudText Typo="Typo.h6">@_totalAmount.ToString("N0")</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
|
||||||
|
<MudDataGrid @ref="_grid" T="UserPackagePurchaseModel" ServerData="LoadData" ReadOnly="true"
|
||||||
|
Hover="true" Dense="true" Height="calc(100vh - 420px)">
|
||||||
|
<Columns>
|
||||||
|
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||||
|
<TemplateColumn Title="کاربر">
|
||||||
|
<CellTemplate>
|
||||||
|
<MudStack Spacing="0">
|
||||||
|
<MudText Typo="Typo.body2">@context.Item.UserName</MudText>
|
||||||
|
<MudText Typo="Typo.caption" Color="Color.Secondary">@context.Item.UserMobile</MudText>
|
||||||
|
</MudStack>
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<PropertyColumn Property="x => x.PackageName" Title="پکیج" />
|
||||||
|
<TemplateColumn Title="روش خرید">
|
||||||
|
<CellTemplate>@ReportLabels.GetPurchaseMethodLabel(context.Item.PurchaseMethod)</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<TemplateColumn Title="مبلغ (ریال)">
|
||||||
|
<CellTemplate>@context.Item.Amount.ToString("N0")</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<TemplateColumn Title="تاریخ خرید">
|
||||||
|
<CellTemplate>@context.Item.PurchasedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<PropertyColumn Property="x => x.OrderId" Title="OrderId" />
|
||||||
|
<PropertyColumn Property="x => x.TransactionId" Title="TransactionId" />
|
||||||
|
</Columns>
|
||||||
|
<NoRecordsContent>
|
||||||
|
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||||
|
</NoRecordsContent>
|
||||||
|
<PagerContent>
|
||||||
|
<MudDataGridPager T="UserPackagePurchaseModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||||
|
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||||
|
RowsPerPageString="تعداد در صفحه" />
|
||||||
|
</PagerContent>
|
||||||
|
</MudDataGrid>
|
||||||
|
</Content>
|
||||||
|
</BasePageComponent>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private BasePageComponent? _basePage;
|
||||||
|
private MudDataGrid<UserPackagePurchaseModel>? _grid;
|
||||||
|
private long? _userId;
|
||||||
|
private int? _purchaseMethod;
|
||||||
|
private DateTime? _fromDate, _toDate;
|
||||||
|
private long _totalCount, _totalAmount;
|
||||||
|
|
||||||
|
private GetAllUserPackagePurchaseByFilterFilter BuildFilter() => new()
|
||||||
|
{
|
||||||
|
UserId = _userId > 0 ? _userId : null,
|
||||||
|
PurchaseMethod = _purchaseMethod,
|
||||||
|
PurchasedFrom = _fromDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_fromDate.Value, DateTimeKind.Utc)) : null,
|
||||||
|
PurchasedTo = _toDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_toDate.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Utc)) : null
|
||||||
|
};
|
||||||
|
|
||||||
|
private async Task<GridData<UserPackagePurchaseModel>> LoadData(GridState<UserPackagePurchaseModel> state)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var filter = BuildFilter();
|
||||||
|
var summary = await PurchaseClient.GetUserPackagePurchaseSummaryAsync(new GetUserPackagePurchaseSummaryRequest { Filter = filter });
|
||||||
|
_totalCount = summary.TotalCount;
|
||||||
|
_totalAmount = summary.TotalAmount;
|
||||||
|
|
||||||
|
var result = await PurchaseClient.GetAllUserPackagePurchaseByFilterAsync(new GetAllUserPackagePurchaseByFilterRequest
|
||||||
|
{
|
||||||
|
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize },
|
||||||
|
Filter = filter
|
||||||
|
});
|
||||||
|
return new GridData<UserPackagePurchaseModel>
|
||||||
|
{
|
||||||
|
Items = result?.Models?.ToList() ?? new(),
|
||||||
|
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||||
|
return new GridData<UserPackagePurchaseModel>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnFilterSubmit() { if (_grid != null) await _grid.ReloadServerData(); }
|
||||||
|
private async Task OnFilterCleared()
|
||||||
|
{
|
||||||
|
_userId = null; _purchaseMethod = null; _fromDate = _toDate = null;
|
||||||
|
if (_grid != null) await _grid.ReloadServerData();
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user