Compare commits

..

16 Commits

Author SHA1 Message Date
masoodafar-web ece29956b2 feat(app-versions): Add functionality to create new app versions and update UI components
Build and Deploy / build (push) Successful in 2m26s
2025-12-27 02:45:47 +03:30
masoodafar-web d69b838a72 fix(dashboard): Disable DiscountShopWidget temporarily
Build and Deploy to Production / build-and-deploy (push) Successful in 3m51s
- Comment out DiscountShopWidget that requires unregistered IDiscountOrderService
- Add placeholder card with 'coming soon' message
- Fix null reference warnings with null! attributes
2025-12-26 06:19:57 +03:30
masoodafar-web 6b140c3bd8 feat(backoffice): Add App Version management UI
Build and Deploy to Production / build-and-deploy (push) Successful in 5m47s
- Add IAppVersionService interface and implementation
- Add AppVersions.razor page for managing app versions
- Add AppVersionEditDialog component for editing versions
- Register AppVersion gRPC client and service in DI
- Update Foursat.BackOffice.BFF.Configuration.Protobuf to 1.0.20
2025-12-26 06:07:02 +03:30
masoodafar-web 332fe0112c fix: update NetworkTreeViewer to change select item value from 20 to 100 2025-12-26 02:05:20 +03:30
masoodafar-web f846a927a7 Merge branch 'stage-new' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 5m49s
2025-12-25 02:04:20 +03:30
masoodafar-web 6dec03cf4d fix: update BackOffice gateway URL to secure production endpoint 2025-12-25 02:04:12 +03:30
masoodafar-web e808cf827e feat: Enhance week filter functionality with disabled state and target week highlight 2025-12-25 02:03:01 +03:30
masoodafar-web 8e98b1f3c7 Refactor code structure for improved readability and maintainability 2025-12-25 00:14:57 +03:30
admin 0ff1a17419 fix: correct dockerfile path and registry
Build and Deploy to Production / build-and-deploy (push) Successful in 2m22s
2025-12-23 22:34:17 +00:00
admin 0c5c30ca42 fix: correct dockerfile path and registry
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2025-12-23 22:34:09 +00:00
admin 0f99d4c80c feat: add production deployment workflow
Build and Deploy to Production / build-and-deploy (push) Failing after 15s
2025-12-23 22:24:58 +00:00
admin 33d4f4c543 fix: update appsettings for production K8s 2025-12-23 22:17:52 +00:00
admin 787fe6def6 fix: update appsettings for production K8s 2025-12-23 22:17:28 +00:00
masoodafar-web 51c5edc0ae feat: Fix multiple pages in BackOffice and enable product features 2025-12-24 01:06:55 +03:30
masoodafar-web ec923dc22e fix: Switch BackOffice gateway URL to production environment 2025-12-20 05:24:59 +03:30
masoodafar-web be0cb98222 feat: Update deployment branch to stage-new in workflow configuration 2025-12-20 05:11:45 +03:30
28 changed files with 2491 additions and 626 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ name: Push nuget and docker image Actions Workflow
on:
push:
branches:
- stage
- stage-new
jobs:
Deploy:
runs-on: windows
+83
View File
@@ -0,0 +1,83 @@
name: Build and Deploy to Production
on:
push:
branches:
- production
env:
REGISTRY: gitea-svc:3000
EXTERNAL_REGISTRY: git.foursat.afrino.co
IMAGE_NAME: admin/backoffice
jobs:
build-and-deploy:
runs-on: ubuntu-latest
container:
image: docker:latest
options: --privileged
env:
HTTP_PROXY: http://proxyuser:87zH26nbqT2@46.249.98.211:3128
HTTPS_PROXY: http://proxyuser:87zH26nbqT2@46.249.98.211:3128
NO_PROXY: localhost,127.0.0.1,gitea-svc,45.149.79.127,194.5.195.53,10.0.0.0/8
steps:
- name: Install dependencies
run: |
apk add --no-cache git curl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
mv kubectl /usr/local/bin/
- name: Start Docker daemon
run: |
mkdir -p /etc/docker
cat > /etc/docker/daemon.json << 'DAEMON'
{
"insecure-registries": ["git.foursat.afrino.co", "gitea-svc:3000"]
}
DAEMON
mkdir -p ~/.docker
cat > ~/.docker/config.json << 'CONF'
{
"proxies": {
"default": {
"httpProxy": "http://proxyuser:87zH26nbqT2@46.249.98.211:3128",
"httpsProxy": "http://proxyuser:87zH26nbqT2@46.249.98.211:3128",
"noProxy": "localhost,127.0.0.1,gitea-svc,194.5.195.53,10.0.0.0/8"
}
}
}
CONF
dockerd &
for i in $(seq 1 30); do docker info >/dev/null 2>&1 && break || sleep 2; done
- name: Checkout code
run: |
git clone --depth 1 --branch production http://gitea-svc:3000/admin/BackOffice.git .
git log -1 --format="%H %s"
- name: Build Docker Image
run: |
cd src
docker build -f BackOffice/Dockerfile \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod \
-t ${{ env.EXTERNAL_REGISTRY }}/${{ env.IMAGE_NAME }}:prod \
--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
run: |
echo "87zH26nbqT" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod
echo "87zH26nbqT" | docker login ${{ env.EXTERNAL_REGISTRY }} -u admin --password-stdin
docker push ${{ env.EXTERNAL_REGISTRY }}/${{ env.IMAGE_NAME }}:prod
- name: Deploy to Production
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBECONFIG_PROD }}" | base64 -d > ~/.kube/config
kubectl rollout restart deployment/backoffice || echo "Deployment not found"
kubectl rollout status deployment/backoffice --timeout=5m || echo "Rollout pending"
+3 -1
View File
@@ -1,6 +1,6 @@
# BackOffice Build Fix Status
> آخرین بروزرسانی: December 6, 2025
> آخرین بروزرسانی: December 20, 2025
## وضعیت فعلی
@@ -17,6 +17,8 @@
- ✅ BackOffice.BFF.DiscountShoppingCart.Protobuf
- ✅ BackOffice.BFF.PublicMessage.Protobuf
- ✅ BackOffice.BFF.ManualPayment.Protobuf
- ✅ BackOffice.BFF.ClubMembership.Protobuf
- ✅ BackOffice.BFF.Commission.Protobuf
### BackOffice UI:
- **Build**: ✅ موفق - 0 Error
+118
View File
@@ -0,0 +1,118 @@
# BackOffice Changelog
> تاریخچه تغییرات پروژه BackOffice
---
## December 20, 2025
### 🐛 Bug Fixes
#### 1. صفحه `/network/balances` - ValidationException
**مشکل**: خطای ValidationException هنگام لود صفحه
**راه‌حل**: اضافه کردن Mapster mapping در `CommissionProfile.cs`:
```csharp
config.NewConfig<GetUserWeeklyBalancesRequest, GetUserWeeklyBalancesQuery>()
.Map(dest => dest.PaginationState, src => src.PaginationState);
```
---
#### 2. صفحه `/club/members` - داده‌ها لود نمی‌شدند
**مشکل**: صفحه خالی بود و داده‌ای نمایش نمی‌داد
**راه‌حل**: ایجاد `ClubMembershipProfile.cs` در CMS و BFF با mappings کامل:
- `GetAllClubMembershipsRequest``GetAllClubMembershipsQuery`
- `GetAllClubMembershipsResponseDto``GetAllClubMembershipsResponse`
**فایل‌های جدید**:
- `CMS/WebApi/Common/Mappings/ClubMembershipProfile.cs`
- `BackOffice.BFF/WebApi/Common/Mappings/ClubMembershipProfile.cs` (بازنویسی)
---
#### 3. صفحه `/club/statistics` - Unimplemented Error
**مشکل**: خطای `Status(StatusCode="Unimplemented")`
**راه‌حل**:
1. اضافه کردن override `GetClubStatistics` در `ClubMembershipService.cs`
2. اضافه کردن mappings برای Statistics در هر دو Profile
---
### ✨ New Features
#### 4. فعال‌سازی قابلیت‌های Products
**قبل**: همه دکمه‌ها "در حال توسعه" نشان می‌دادند
**بعد**: همه قابلیت‌ها فعال شدند:
- ✅ ایجاد محصول جدید (CreateDialog)
- ✅ ویرایش محصول (UpdateDialog)
- ✅ گالری تصاویر (GalleryDialog)
- ✅ مدیریت تگ‌ها (AssignTagsDialog)
**فایل**: `ProductsMainPage.razor.cs`
---
#### 5. فیلد "تعداد موجودی" در Products
**اضافات**:
- فیلد موجودی در فرم ایجاد محصول
- فیلد موجودی در فرم ویرایش محصول
- ستون موجودی در لیست با رنگ‌بندی:
- 🔴 ناموجود (0 یا کمتر)
- 🟡 کم موجود (کمتر از 10)
- 🟢 موجود (10 یا بیشتر)
**فایل‌های تغییر یافته**:
- `CreateDialog.razor`
- `UpdateDialog.razor`
- `ProductsMainPage.razor`
- `CreateNewProductsCommand.cs` (BFF)
- `UpdateProductsCommand.cs` (BFF)
---
## December 6, 2025
### ✅ Major Milestones
- Build Errors: 60+ → 0
- MudBlazor 8 Migration Complete
- All Product Image Management APIs Implemented
- BulkEdit Module Enabled
- All Files Unexcluded
### 🔧 Technical Changes
- `IMudDialogInstance` جایگزین `MudDialogInstance`
- `MudSwitch T="bool"` اضافه شد
- `MudChip T="string"` اضافه شد
- Products از NuGet به ProjectReference تغییر کرد
---
## December 1, 2025
### ✅ Network & Commission System
- Commission Dashboard Complete
- Network Members Page Complete
- Club Members Page Complete
- Weekly Pool Management
- Withdrawal System
- Payout System
---
## November 29, 2025
### ✅ Initial Setup
- SystemConfigurations Table Created
- Base Configuration Values Added:
- `Network.MaxDepth`: 10
- `Club.DefaultMembershipDurationMonths`: 12
- `Commission.MinimumPayoutAmount`: 100000
- `System.MaintenanceMode`: false
-156
View File
@@ -1,156 +0,0 @@
# راهنمای ادامه کار - BackOffice Build Fix
> این فایل برای شروع چت جدید طراحی شده است
## وضعیت فعلی
**تاریخ**: December 6, 2025
**Build Status**: ✅ SUCCESS (0 خطا)
**پیشرفت**: 100% COMPLETE - آماده Production
---
## 🎉 پروژه کامل شد!
**همه چیز آماده است**:
- ✅ 0 Build Errors
- ✅ 9 Modules فعال
- ✅ 38+ صفحه و کامپوننت
- ✅ BulkEdit کامل
- ✅ Product Image Management کامل (Backend implemented)
- ✅ Proto Projects: 14 پروژه فعال
- ✅ MudBlazor 8.14.0 Migration کامل
---
## دستور بررسی وضعیت
```bash
# بررسی Build
cd /home/masoud/Apps/project/FourSat/BackOffice/src
dotnet build BackOffice.sln --no-incremental
# بررسی BackOffice.BFF
cd /home/masoud/Apps/project/FourSat/BackOffice.BFF/src
dotnet build BackOffice.BFF.sln --no-incremental
# مشاهده داکیومنت‌ها
cat /home/masoud/Apps/project/FourSat/BackOffice/docs/BUILD-FIX-STATUS.md
cat /home/masoud/Apps/project/FourSat/BackOffice/docs/REMAINING-TASKS.md
cat /home/masoud/Apps/project/FourSat/BackOffice/docs/EXCLUDED-FILES.md
```
---
## ✅ همه مشکلات حل شد!
### تکمیل شده:
- ✅ PaginationState namespace - حل شد
- ✅ BulkEdit Module - فعال و کار می‌کند
- ✅ Product Image Management - کامل (Proto + UI + Backend)
- ✅ GalleryDialog - فعال
- ✅ CreateDialog/UpdateDialog - فعال با Image Upload
- ✅ ProductsService methods - uncommented و فعال
- ✅ CQRS Handlers - پیاده‌سازی شده
- ✅ CMS Integration - متصل به ProductGalleries
- ✅ Image Optimization - 1200x1200 + 300x300
---
## خطاهای قدیمی (همه حل شدند)
### 1. PaginationState Namespace
**فایل**: `ProductsAutoComplete.razor.cs`
**خطا**: `PaginationState` پیدا نمیشه
**فیکس**: تغییر using به `BackOffice.BFF.Products.Protobuf.Protos.Products`
### 2. Int32Value/Int64Value Binding
**فایل**: `WithdrawalReports.razor`
**خطا**: `@bind-Value` روی `Int32Value` کار نمی‌کنه
**فیکس**: استفاده از conversion یا wrapper
### 3. GalleryDialog Reference
**فایل**: `ProductsMainPage.razor.cs`
**خطا**: `GalleryDialog` exclude شده ولی متد `OpenGalleryDialog` هنوز هست
**فیکس**: comment کردن متد
### 4. DiscountShopWidget Reference
**فایل**: `SystemOverview.razor`
**خطا**: component exclude شده ولی استفاده میشه
**فیکس**: حذف یا comment کردن component از صفحه
### 5. ClubMembers Bool Binding
**فایل**: `ClubMembers.razor`
**خطا**: `bool?` به `MudSwitch T="bool"` bind نمیشه
**فیکس**: تغییر نوع متغیر یا استفاده از converter
---
## فایل‌های کلیدی
| فایل | هدف |
|------|-----|
| `BackOffice.csproj` | لیست exclude ها و references |
| `ConfigureService.cs` | DI registrations |
| `_Imports.razor` | global using و inject ها |
| `BackOffice/docs/BUILD-FIX-STATUS.md` | وضعیت کامل خطاها |
| `BackOffice/docs/EXCLUDED-FILES.md` | فایل‌های exclude شده |
| `BackOffice/docs/PROTO-DEPENDENCIES.md` | وابستگی‌های proto |
---
## نکات مهم
1. **هیچ فایلی حذف نشده** - فقط از build exclude شدند
2. **Products.Protobuf** از ProjectReference استفاده می‌کند (نه NuGet)
3. **MudBlazor 8.14.0** نیاز به `T` parameter دارد
4. **Snackbar** در `_Imports.razor` inject شده
5. **.NET 9** target framework هست
---
## چک‌لیست تکمیل شده
- [x] فیکس PaginationState namespace
- [x] فعال‌سازی BulkEdit Module
- [x] اضافه کردن Proto Messages برای Image Upload
- [x] فعال‌سازی GalleryDialog
- [x] فعال‌سازی CreateDialog/UpdateDialog
- [x] Uncomment کردن ProductsService methods
- [x] بررسی CQRS Handlers
- [x] اتصال به CMS ProductGalleries
- [x] ✅ Build موفق - BackOffice UI
- [x] ✅ Build موفق - BackOffice.BFF
- [x] تست و تایید نهایی
---
## ⚠️ قبل از شروع کار - بخوان!
### Proto Package Management (خیلی مهم!)
**هر تغییر در Proto = این 3 مرحله اجباری:**
1. ✏️ Version++ در `.csproj`
2. 📦 `dotnet pack -c Release`
3. 🔄 Update version در پروژه‌های وابسته
**این قانون برای همه سرویس‌ها است:**
- CMS Proto → BFF Protos → UI
- هر لایه → لایه بالاتر
**فراموش کردن = Bug های عجیب و غریب!**
---
## 🎯 System Status: PRODUCTION READY ✅
**BackOffice System**:
- UI: 100% Complete
- Backend: 100% Complete
- Build: 0 Errors
- Modules: 9 Active
- Pages: 38+
- Proto Projects: 14
**آماده برای استفاده در Production** 🚀
-170
View File
@@ -1,170 +0,0 @@
# فایل‌های Exclude شده از Build
> آخرین بروزرسانی: December 6, 2025
>
> این فایل‌ها از build خارج شدند ولی **حذف نشدند**
## ✅ فایل‌های برگردانده شده (Enabled)
این فایل‌ها قبلاً exclude بودند و حالا **فعال** شدند:
### DiscountShop Module
-`Pages/DiscountShop/**` - تمام صفحات فروشگاه تخفیفی
-`Services/DiscountProduct/**` - سرویس محصولات تخفیفی
-`Services/DiscountCategory/**` - سرویس دسته‌بندی‌ها
-`Services/DiscountOrder/**` - سرویس سفارشات
### Tag Module
-`Pages/Tag/**` - صفحات مدیریت تگ
-`Services/Tag/**` - سرویس تگ
### PublicMessages Module
-`Pages/PublicMessages/**` - مدیریت پیام‌های عمومی
-`Services/PublicMessage/**` - سرویس پیام‌ها
### Payment Module
-`Pages/Payment/ManualPayments.razor*` - پرداخت‌های دستی
-`Pages/Payment/Components/ManualPaymentDialog.razor*` - دیالوگ پرداخت
-`Pages/Payment/Transactions.razor*` - صفحه تراکنش‌ها
### Dashboard
-`Pages/Dashboard/DiscountShopWidget.razor*` - ویجت آمار فروشگاه
### DragDrop Pages
-`Pages/Category/CategoryProductsDragDropPage.razor*` - مدیریت محصولات دسته
-`Pages/Products/ProductCategoriesDragDropPage.razor*` - مدیریت دسته‌های محصول
### BulkEdit Module
-`Pages/Products/BulkEdit.razor*` - ویرایش گروهی محصولات (ENABLED)
### BulkEdit Module
-`Pages/Products/BulkEdit.razor*` - ویرایش گروهی محصولات (ENABLED)
### Product Image Management
-`Pages/Products/Components/GalleryDialog.razor*` - گالری تصاویر (ENABLED)
-`Pages/Products/Components/CreateDialog.razor*` - ایجاد محصول با تصویر (ENABLED)
-`Pages/Products/Components/UpdateDialog.razor*` - ویرایش محصول با تصویر (ENABLED)
---
## ❌ فایل‌های هنوز Exclude
**هیچ فایلی Exclude نیست!**
تمامی فایل‌ها فعال شدند. Proto Messages و RPCهای لازم برای Image Upload اضافه شدند.
### ✅ وضعیت نهایی:
همه چیز کامل و آماده است:
-`GetProductGalleryAsync` - دریافت لیست تصاویر محصول (READY)
-`AddProductImageAsync` - آپلود تصویر جدید (READY)
-`RemoveProductImageAsync` - حذف تصویر (READY)
**Backend Implementation**: ✅ COMPLETED
- ProductsService.cs: Methods uncommented
- CQRS Handlers: Fully implemented
- CMS Integration: Connected
- Image Processing: Optimized with SixLabors.ImageSharp
---
## آمار
- **✅ فایل‌های Enabled**: ~38+ صفحه و ~15 سرویس
- **❌ فایل‌های Excluded**: 0 فایل ✅
---
## آمار
- **✅ فایل‌های Enabled**: ~30+ صفحه و ~15 سرویس
- **❌ فایل‌های Excluded**: 4 فایل
- **Proto Projects ساخته شده**: 14
- **Build Errors**: 0
---
## Exclude های فعلی در csproj
```xml
<ItemGroup>
<!-- BulkEdit - needs refactoring -->
<Compile Remove="Pages\Products\BulkEdit.razor" />
<Content Remove="Pages\Products\BulkEdit.razor" />
<Compile Remove="Pages\Products\BulkEdit.razor.cs" />
<!-- GalleryDialog - needs AddProductImageAsync/RemoveProductImageAsync -->
<Compile Remove="Pages\Products\Components\GalleryDialog.razor" />
<Content Remove="Pages\Products\Components\GalleryDialog.razor" />
<Compile Remove="Pages\Products\Components\GalleryDialog.razor.cs" />
<!-- CreateDialog/UpdateDialog - needs ImageFileModel -->
<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" />
</ItemGroup>
```
---
## نکات مهم
### برای فعال‌سازی فایل‌های Exclude:
1. **GalleryDialog, CreateDialog, UpdateDialog**:
- نیاز به پیاده‌سازی Image Upload API در Backend
- افزودن `ImageFileModel` message به proto
- افزودن RPC methods برای upload/remove
2. **BulkEdit**:
- حذف dependency به `CMSMicroservice.Protobuf`
- استفاده از `BackOffice.BFF.Products.Protobuf`
- افزودن `BulkUpdateProducts` RPC به backend
### فایل‌های کامل شده که دیگر exclude نیستند:
- ✅ تمام ماژول DiscountShop
- ✅ تمام ماژول Tag
- ✅ تمام ماژول PublicMessages
- ✅ تمام ماژول ManualPayments
- ✅ DiscountShopWidget
- ✅ Transactions
- ✅ DragDrop Pages
---
<!-- BulkEdit -->
<Compile Remove="Pages\Products\BulkEdit.razor" />
<Content Remove="Pages\Products\BulkEdit.razor" />
<Compile Remove="Pages\Products\BulkEdit.razor.cs" />
<!-- UserOrder Components -->
<Compile Remove="Pages\UserOrder\Components\CancelOrderDialog.razor" />
<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" />
<!-- Transactions -->
<Compile Remove="Pages\Payment\Transactions.razor" />
<Content Remove="Pages\Payment\Transactions.razor" />
<Compile Remove="Pages\Payment\Transactions.razor.cs" />
<!-- Product Dialogs -->
<Compile Remove="Pages\Products\Components\CreateProductDialog.razor" />
<Content Remove="Pages\Products\Components\CreateProductDialog.razor" />
<Compile Remove="Pages\Products\Components\CreateProductDialog.razor.cs" />
<Compile Remove="Pages\Products\Components\UpdateProductDialog.razor" />
<Content Remove="Pages\Products\Components\UpdateProductDialog.razor" />
<Compile Remove="Pages\Products\Components\UpdateProductDialog.razor.cs" />
<Compile Remove="Pages\Products\Components\GalleryDialog.razor" />
<Content Remove="Pages\Products\Components\GalleryDialog.razor" />
<Compile Remove="Pages\Products\Components\GalleryDialog.razor.cs" />
</ItemGroup>
```
+28
View File
@@ -0,0 +1,28 @@
# ⚠️ توجه: مستندات اصلی منتقل شده
مستندات اصلی پروژه در فولدر زیر قرار دارند:
```
/home/masoud/Apps/project/FourSat/totalDoc/
```
## 🗂️ ساختار اصلی مستندات:
- **00-INDEX.md** - فهرست جامع مستندات
- **QUICK-REFERENCE.md** - مرجع سریع
- **FINAL-STATUS.md** - وضعیت نهایی پروژه
- **CHANGELOG-2025-12-XX.md** - لاگ تغییرات روزانه
- **01-BUSINESS/** - منطق تجاری
- **02-ARCHITECTURE/** - معماری سیستم
- **03-BACKEND/** - مستندات Backend (CMS, BFF)
- **04-FRONTEND/** - مستندات Frontend (BackOffice, FrontOffice)
- **05-TASKS/** - کارهای جاری
- **06-DEPLOYMENT/** - راهنمای استقرار
## 📝 این پوشه:
فایل‌های این پوشه (`BackOffice/docs/`) برای مرجع محلی نگه داشته شده‌اند ولی **مستندات اصلی و به‌روز** در `totalDoc` قرار دارند.
---
**تاریخ**: ۳۰ آذر ۱۴۰۴ (20 December 2025)
-177
View File
@@ -1,177 +0,0 @@
# BackOffice Proto Dependencies
> این فایل وابستگی‌های proto بین BackOffice UI و BackOffice.BFF را مستند می‌کند
## Proto های موجود در BackOffice.BFF
| Proto Project | وضعیت | نوع Reference در UI |
|---------------|-------|---------------------|
| Category.Protobuf | ✅ موجود | NuGet |
| ClubMembership.Protobuf | ✅ موجود | ProjectReference |
| Commission.Protobuf | ✅ موجود | ProjectReference |
| Common.Protobuf | ✅ موجود | ProjectReference |
| Configuration.Protobuf | ✅ موجود | ProjectReference |
| Health.Protobuf | ✅ موجود | ProjectReference |
| ManualPayment.Protobuf | ✅ موجود | ProjectReference |
| NetworkMembership.Protobuf | ✅ موجود | ProjectReference |
| Otp.Protobuf | ✅ موجود | NuGet |
| Package.Protobuf | ✅ موجود | NuGet |
| Products.Protobuf | ✅ موجود | **ProjectReference** (تغییر داده شد) |
| PublicMessage.Protobuf | ✅ موجود | ProjectReference |
| Role.Protobuf | ✅ موجود | NuGet |
| User.Protobuf | ✅ موجود | NuGet |
| UserAddress.Protobuf | ✅ موجود | NuGet |
| UserOrder.Protobuf | ✅ موجود | NuGet |
| UserRole.Protobuf | ✅ موجود | NuGet |
## Proto های مورد نیاز (وجود ندارند)
| Proto Project | صفحات وابسته | سرویس‌های وابسته |
|---------------|-------------|------------------|
| DiscountProduct.Protobuf | `Pages/DiscountShop/*` | `Services/DiscountProduct/*` |
| DiscountCategory.Protobuf | `Pages/DiscountShop/*` | `Services/DiscountCategory/*` |
| DiscountOrder.Protobuf | `Pages/DiscountShop/*` | `Services/DiscountOrder/*` |
| Tag.Protobuf | `Pages/Tag/*` | `Services/Tag/*` |
| ProductTag.Protobuf | `Pages/Products/BulkEdit` | - |
## متدهای Proto مورد نیاز (وجود ندارند)
### UserOrder.Protobuf
```protobuf
// متدهای جدید مورد نیاز
rpc CancelOrder(CancelOrderRequest) returns (CancelOrderResponse);
rpc ApplyDiscountToOrder(ApplyDiscountToOrderRequest) returns (ApplyDiscountToOrderResponse);
rpc UpdateOrderStatus(UpdateOrderStatusRequest) returns (UpdateOrderStatusResponse);
// فیلدهای جدید در GetUserOrderResponse
message GetUserOrderResponse {
// ... existing fields ...
int64 vat_amount = X;
int32 vat_percentage = X;
int64 vat_base_amount = X;
int64 vat_total_amount = X;
}
// PaymentStatus enum needs None value
enum PaymentStatus {
None = 0;
Success = 1;
Reject = 2;
Pending = 3;
}
```
### Products.Protobuf
```protobuf
// متدهای جدید مورد نیاز
rpc AddProductImage(AddProductImageRequest) returns (AddProductImageResponse);
rpc RemoveProductImage(RemoveProductImageRequest) returns (google.protobuf.Empty);
// فیلدهای جدید در CreateNewProductsRequest
message CreateNewProductsRequest {
// ... existing fields ...
bytes image_file = X;
bytes thumbnail_file = X;
}
// یا بهتر:
message ImageFileModel {
bytes content = 1;
string file_name = 2;
string content_type = 3;
}
```
### ManualPayment.Protobuf
نیاز به بررسی - ممکن است متدهایی کم باشد
### PublicMessage.Protobuf
نیاز به بررسی - ممکن است متدهایی کم باشد
---
## نحوه ساخت Proto Project جدید
```bash
# 1. ساخت پوشه
mkdir -p BackOffice.BFF/src/Protobufs/BackOffice.BFF.Tag.Protobuf/Protos
# 2. ساخت csproj
cat > BackOffice.BFF/src/Protobufs/BackOffice.BFF.Tag.Protobuf/BackOffice.BFF.Tag.Protobuf.csproj << 'EOF'
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Protobuf Include="Protos\*.proto" GrpcServices="Client" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.29.3" />
<PackageReference Include="Grpc.Net.Client" Version="2.71.0" />
<PackageReference Include="Grpc.Tools" Version="2.69.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
EOF
# 3. ساخت proto file
cat > BackOffice.BFF/src/Protobufs/BackOffice.BFF.Tag.Protobuf/Protos/tag.proto << 'EOF'
syntax = "proto3";
package tag;
option csharp_namespace = "BackOffice.BFF.Tag.Protobuf.Protos.Tag";
// ... define services and messages
EOF
# 4. اضافه کردن به solution
dotnet sln BackOffice.BFF/src/BackOffice.BFF.sln add BackOffice.BFF/src/Protobufs/BackOffice.BFF.Tag.Protobuf/BackOffice.BFF.Tag.Protobuf.csproj
```
---
## تغییرات اعمال شده در Products.Protobuf
### Namespace Change
```protobuf
// FROM:
option csharp_namespace = "CMSMicroservice.Protobuf.Protos.Products";
// TO:
option csharp_namespace = "BackOffice.BFF.Products.Protobuf.Protos.Products";
```
### اضافه شدن public_messages.proto
فایل از `ClubMembership.Protobuf` کپی شد با namespace:
```protobuf
option csharp_namespace = "BackOffice.BFF.Products.Protobuf.Protos";
```
### RPC های جدید
```protobuf
rpc GetProductGallery(GetProductGalleryRequest) returns (GetProductGalleryResponse);
rpc GetCategories(GetCategoriesRequest) returns (GetCategoriesResponse);
rpc UpdateProductCategories(UpdateProductCategoriesRequest) returns (google.protobuf.Empty);
rpc GetProductsForCategory(GetProductsForCategoryRequest) returns (GetProductsForCategoryResponse);
rpc UpdateCategoryProducts(UpdateCategoryProductsRequest) returns (google.protobuf.Empty);
```
### Message های جدید
- CategoryItem
- CategoryProductItem
- GetProductGalleryRequest/Response
- ProductGalleryImage
- GetCategoriesRequest/Response
- UpdateProductCategoriesRequest
- GetProductsForCategoryRequest/Response
- UpdateCategoryProductsRequest
+34
View File
@@ -0,0 +1,34 @@
# BackOffice Documentation - README
> آخرین بروزرسانی: **December 20, 2025**
## فایل‌های این پوشه
| فایل | شرح |
|------|-----|
| `STATUS.md` | وضعیت کلی پروژه و Build Status |
| `CHANGELOG.md` | تاریخچه تغییرات به ترتیب تاریخ |
| `TECHNICAL-NOTES.md` | نکات فنی، Mapster، MudBlazor، Proto |
| `development-plan.md` | برنامه توسعه (قدیمی - برای مرجع) |
---
## وضعیت فعلی
```
✅ Build Status: SUCCESS (0 Errors)
✅ Proto Projects: 24 فعال
✅ صفحات فعال: 40+
✅ Excluded Files: 0
```
## دستورات سریع
```bash
# Build همه
cd /home/masoud/Apps/project/FourSat/BackOffice/src
dotnet build BackOffice.sln
# فقط UI
dotnet build BackOffice/BackOffice.csproj
```
+74 -3
View File
@@ -1,16 +1,87 @@
# کارهای باقیمانده - BackOffice
> آخرین بروزرسانی: December 6, 2025
> آخرین بروزرسانی: December 20, 2025
## وضعیت کلی
**Build Status**: ✅ SUCCESS (0 Errors)
**Enabled Modules**: 9 ماژول کامل
**Enabled Modules**: 10+ ماژول کامل
**Remaining Tasks**: فقط Backend Implementation
---
## ✅ کارهای انجام شده امروز
## ✅ کارهای انجام شده - Session December 20, 2025
### 1. صفحه `/network/balances` - ✅ FIXED
**مشکل**: ValidationException هنگام لود صفحه
**راه‌حل**: اضافه کردن Mapster mapping برای `GetUserWeeklyBalancesRequest``GetUserWeeklyBalancesQuery`
**فایل تغییر یافته**:
- `BackOffice.BFF/WebApi/Common/Mappings/CommissionProfile.cs`
```csharp
config.NewConfig<GetUserWeeklyBalancesRequest, GetUserWeeklyBalancesQuery>()
.Map(dest => dest.PaginationState, src => src.PaginationState);
```
### 2. صفحه `/club/members` - ✅ FIXED
**مشکل**: داده‌ها لود نمی‌شدند (Mapster mapping نداشت)
**راه‌حل**: ایجاد ClubMembershipProfile در CMS و BFF
**فایل‌های جدید**:
- `CMS/WebApi/Common/Mappings/ClubMembershipProfile.cs` (NEW)
- `BackOffice.BFF/WebApi/Common/Mappings/ClubMembershipProfile.cs` (REWRITTEN)
**Mappings اضافه شده**:
- `GetAllClubMembershipsRequest``GetAllClubMembershipsQuery`
- `GetAllClubMembershipsResponseDto``GetAllClubMembershipsResponse`
### 3. صفحه `/club/statistics` - ✅ FIXED
**مشکل**: خطای `Status(StatusCode="Unimplemented")`
**راه‌حل**: پیاده‌سازی متد gRPC در BFF و اضافه کردن mappings
**فایل‌های تغییر یافته**:
- `BackOffice.BFF/WebApi/Services/ClubMembershipService.cs` - اضافه شدن `GetClubStatistics` override
- `CMS/WebApi/Common/Mappings/ClubMembershipProfile.cs` - اضافه شدن mappings
- `BackOffice.BFF/WebApi/Common/Mappings/ClubMembershipProfile.cs` - اضافه شدن mappings
**Mappings اضافه شده**:
- `GetClubStatisticsRequest``GetClubStatisticsQuery`
- `GetClubStatisticsResponseDto``GetClubStatisticsResponse`
- PackageDistribution و MonthlyTrend mappings
### 4. صفحه Products - ✅ ALL FEATURES ENABLED
**مشکل**: همه قابلیت‌ها disabled بودند و "در حال توسعه" نشان می‌دادند
**راه‌حل**: Uncomment کردن کدهای دیالوگ‌ها
**فایل تغییر یافته**:
- `BackOffice/Pages/Products/ProductsMainPage.razor.cs`
**قابلیت‌های فعال شده**:
-`CreateNew()` - ایجاد محصول جدید
-`Update()` - ویرایش محصول
-`OpenGallery()` - گالری تصاویر
-`OpenTagAssignment()` - اختصاص تگ
### 5. فیلد "تعداد موجودی" در Products - ✅ ADDED
**مشکل**: فیلد RemainingCount در فرم‌ها و لیست نبود
**راه‌حل**: اضافه کردن فیلد به همه لایه‌ها
**فایل‌های تغییر یافته**:
- `BackOffice/Pages/Products/Components/CreateDialog.razor` - اضافه شدن فیلد موجودی
- `BackOffice/Pages/Products/Components/UpdateDialog.razor` - اضافه شدن فیلد موجودی
- `BackOffice/Pages/Products/ProductsMainPage.razor` - اضافه شدن ستون موجودی با رنگ‌بندی
- `BackOffice.BFF.Application/ProductsCQ/Commands/CreateNewProducts/CreateNewProductsCommand.cs` - اضافه شدن `RemainingCount`
- `BackOffice.BFF.Application/ProductsCQ/Commands/UpdateProducts/UpdateProductsCommand.cs` - اضافه شدن `RemainingCount`
**نمایش موجودی در لیست**:
- 🔴 **ناموجود** - اگر موجودی `0` یا کمتر (Chip قرمز)
- 🟡 **عدد** - اگر موجودی کمتر از `10` (Chip زرد - هشدار)
- 🟢 **عدد** - اگر موجودی `10` یا بیشتر (Chip سبز)
---
## ✅ کارهای انجام شده قبلی
### 1. BulkEdit Module - COMPLETED ✅
- ✅ حذف dependency به CMSMicroservice
+311
View File
@@ -0,0 +1,311 @@
# Session Log - December 20, 2025
## خلاصه Session
این session شامل رفع چندین باگ در صفحات BackOffice و فعال‌سازی قابلیت‌های Products بود.
---
## 1. فیکس صفحه `/network/balances`
### مشکل
```
ValidationException هنگام لود صفحه بالانس‌های هفتگی
```
### علت
Mapster mapping برای تبدیل `GetUserWeeklyBalancesRequest` به `GetUserWeeklyBalancesQuery` وجود نداشت.
### راه‌حل
اضافه کردن mapping در `CommissionProfile.cs`:
```csharp
// File: BackOffice.BFF/src/BackOffice.BFF.WebApi/Common/Mappings/CommissionProfile.cs
config.NewConfig<GetUserWeeklyBalancesRequest, GetUserWeeklyBalancesQuery>()
.Map(dest => dest.PaginationState, src => src.PaginationState);
```
---
## 2. فیکس صفحه `/club/members`
### مشکل
```
صفحه لود می‌شد ولی هیچ داده‌ای نمایش نمی‌داد
```
### علت
Mapster mappings در CMS و BFF برای `GetAllClubMemberships` وجود نداشتند.
### راه‌حل
ایجاد `ClubMembershipProfile.cs` در هر دو لایه:
**CMS/src/CMSMicroservice.WebApi/Common/Mappings/ClubMembershipProfile.cs** (NEW):
```csharp
public class ClubMembershipProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
// GetAllClubMemberships mappings
config.NewConfig<GetAllClubMembershipsRequest, GetAllClubMembershipsQuery>()
.Map(dest => dest.PaginationState, src => src.PaginationState)
.Map(dest => dest.Filter, src => src.Filter);
config.NewConfig<GetAllClubMembershipsResponseDto, GetAllClubMembershipsResponse>()
.MapWith(src => new GetAllClubMembershipsResponse
{
MetaData = src.MetaData != null ? new CMSMicroservice.Protobuf.Common.MetaData
{
PageIndex = src.MetaData.PageIndex,
TotalPages = src.MetaData.TotalPages,
TotalCount = src.MetaData.TotalCount
} : null,
Models = { src.Models?.Select(...) ?? Enumerable.Empty<...>() }
});
}
}
```
**BackOffice.BFF/src/BackOffice.BFF.WebApi/Common/Mappings/ClubMembershipProfile.cs** (REWRITTEN):
- Mapping از BFF Proto به Query
- Mapping از CMS Response به BFF Proto Response
- استفاده از alias imports برای disambiguation
---
## 3. فیکس صفحه `/club/statistics`
### مشکل
```
Status(StatusCode="Unimplemented", Detail="Method cms.ClubMembershipContract/GetClubStatistics is unimplemented")
```
### علت
متد `GetClubStatistics` در BFF Service override نشده بود.
### راه‌حل
**1. اضافه کردن override در ClubMembershipService.cs:**
```csharp
public override async Task<GetClubStatisticsResponse> GetClubStatistics(
GetClubStatisticsRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetClubStatisticsRequest, GetClubStatisticsQuery, GetClubStatisticsResponse>(request, context);
}
```
**2. اضافه کردن mappings در CMS ClubMembershipProfile:**
```csharp
config.NewConfig<GetClubStatisticsRequest, GetClubStatisticsQuery>();
config.NewConfig<GetClubStatisticsResponseDto, GetClubStatisticsResponse>()
.MapWith(src => new GetClubStatisticsResponse
{
TotalMembers = src.TotalMembers,
ActiveMembers = src.ActiveMembers,
// ... سایر فیلدها
PackageDistribution = { src.PackageDistribution?.Select(...) },
MonthlyTrend = { src.MonthlyTrend?.Select(...) }
});
```
**3. اضافه کردن mappings در BFF ClubMembershipProfile:**
- Mapping از BFF Proto به Query
- Mapping از CMS Response DTO به BFF Proto Response
---
## 4. فعال‌سازی قابلیت‌های Products
### مشکل
```
همه دکمه‌های صفحه محصولات "در حال توسعه" نشان می‌دادند
```
### علت
کدهای دیالوگ‌ها comment شده بودند با TODO markers.
### راه‌حل
Uncomment کردن کدها در `ProductsMainPage.razor.cs`:
**فایل: BackOffice/src/BackOffice/Pages/Products/ProductsMainPage.razor.cs**
```csharp
// ✅ CreateNew() - فعال شد
public async Task CreateNew()
{
var dialog = await DialogService.ShowAsync<CreateDialog>("افزودن محصول",
new DialogParameters<CreateDialog> { { x => x.Model, new CreateNewProductsRequest() } },
new DialogOptions { CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.Small });
// ...
}
// ✅ Update() - فعال شد
public async Task Update(DataModel model)
{
var parameters = new DialogParameters<UpdateDialog> { { x => x.Model, model.Adapt<UpdateProductsRequest>() } };
var dialog = await DialogService.ShowAsync<UpdateDialog>("ویرایش محصول", parameters, ...);
// ...
}
// ✅ OpenGallery() - فعال شد
public async Task OpenGallery(DataModel model)
{
var parameters = new DialogParameters<GalleryDialog>
{
{ x => x.ProductId, model.Id },
{ x => x.ProductTitle, model.Title }
};
await DialogService.ShowAsync<GalleryDialog>("گالری تصاویر", parameters, ...);
}
// ✅ OpenTagAssignment() - فعال شد
public async Task OpenTagAssignment(DataModel model)
{
var parameters = new DialogParameters<AssignTagsDialog>
{
{ x => x.ProductId, model.Id },
{ x => x.ProductTitle, model.Title }
};
await DialogService.ShowAsync<AssignTagsDialog>("مدیریت تگ‌های محصول", parameters, ...);
}
```
**Using statement uncomment شد:**
```csharp
using BackOffice.Pages.Tag.Components; // برای AssignTagsDialog
```
---
## 5. اضافه کردن فیلد "تعداد موجودی" به Products
### نیاز
نمایش و ویرایش تعداد موجودی محصول در فرم‌ها و لیست
### تغییرات
**1. فرم‌های دیالوگ (CreateDialog.razor & UpdateDialog.razor):**
```razor
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudItem xs="6">
<MudNumericField T="int" HideSpinButtons="true" @bind-Value="Model.Discount"
Disabled="_isLoading" Label="تخفیف (%)" Variant="Variant.Outlined" Margin="Margin.Dense" />
</MudItem>
<MudItem xs="6">
<MudNumericField T="int" HideSpinButtons="true" @bind-Value="Model.RemainingCount"
Disabled="_isLoading" Label="تعداد موجودی" Variant="Variant.Outlined" Margin="Margin.Dense" />
</MudItem>
</MudStack>
```
**2. ستون جدید در لیست (ProductsMainPage.razor):**
```razor
<TemplateColumn Title="موجودی" CellStyle="text-wrap: nowrap;">
<CellTemplate>
@if (context.Item.RemainingCount <= 0)
{
<MudChip T="string" Color="Color.Error" Size="Size.Small">ناموجود</MudChip>
}
else if (context.Item.RemainingCount < 10)
{
<MudChip T="string" Color="Color.Warning" Size="Size.Small">@context.Item.RemainingCount</MudChip>
}
else
{
<MudChip T="string" Color="Color.Success" Size="Size.Small">@context.Item.RemainingCount</MudChip>
}
</CellTemplate>
</TemplateColumn>
```
**3. اضافه کردن فیلد به BFF Commands (فیکس مهم!):**
مشکل: فیلد `RemainingCount` در BFF Application Commands نبود و باعث می‌شد مقدار ارسال/دریافت نشه.
**CreateNewProductsCommand.cs:**
```csharp
public int Discount { get; init; }
public int Rate { get; init; }
public int RemainingCount { get; init; } // ← اضافه شد
public ImageFileModel ImageFile { get; init; }
```
**UpdateProductsCommand.cs:**
```csharp
public int Discount { get; init; }
public int Rate { get; init; }
public int RemainingCount { get; init; } // ← اضافه شد
public string ImagePath { get; init; }
```
---
## لیست کامل فایل‌های تغییر یافته
### BackOffice.BFF
| فایل | نوع تغییر | توضیح |
|------|----------|-------|
| `WebApi/Common/Mappings/CommissionProfile.cs` | MODIFIED | اضافه شدن mapping برای GetUserWeeklyBalances |
| `WebApi/Common/Mappings/ClubMembershipProfile.cs` | REWRITTEN | Mappings کامل برای ClubMembership |
| `WebApi/Services/ClubMembershipService.cs` | MODIFIED | اضافه شدن GetClubStatistics override |
| `Application/ProductsCQ/Commands/CreateNewProducts/CreateNewProductsCommand.cs` | MODIFIED | اضافه شدن RemainingCount |
| `Application/ProductsCQ/Commands/UpdateProducts/UpdateProductsCommand.cs` | MODIFIED | اضافه شدن RemainingCount |
### CMS
| فایل | نوع تغییر | توضیح |
|------|----------|-------|
| `WebApi/Common/Mappings/ClubMembershipProfile.cs` | NEW | Mappings برای ClubMembership |
### BackOffice UI
| فایل | نوع تغییر | توضیح |
|------|----------|-------|
| `Pages/Products/ProductsMainPage.razor.cs` | MODIFIED | فعال‌سازی CreateNew, Update, OpenGallery, OpenTagAssignment |
| `Pages/Products/ProductsMainPage.razor` | MODIFIED | اضافه شدن ستون موجودی |
| `Pages/Products/Components/CreateDialog.razor` | MODIFIED | اضافه شدن فیلد موجودی |
| `Pages/Products/Components/UpdateDialog.razor` | MODIFIED | اضافه شدن فیلد موجودی |
---
## نکات فنی مهم
### 1. Mapster با Proto Types
برای proto types که immutable هستند، باید از `MapWith` استفاده کرد:
```csharp
config.NewConfig<SourceDto, ProtoResponse>()
.MapWith(src => new ProtoResponse
{
Field1 = src.Field1,
RepeatedField = { src.List?.Select(...) ?? Enumerable.Empty<...>() }
});
```
### 2. Alias Imports برای Proto Disambiguation
وقتی دو proto با اسم یکسان داریم:
```csharp
using BffProtos = BackOffice.BFF.ClubMembership.Protobuf.Protos.ClubMembership;
using CmsProtos = CMSMicroservice.Protobuf.Protos.ClubMembership;
```
### 3. Null-Safe MetaData Mapping
```csharp
MetaData = src.MetaData != null ? new MetaData
{
PageIndex = src.MetaData.PageIndex,
TotalPages = src.MetaData.TotalPages,
TotalCount = src.MetaData.TotalCount
} : null
```
---
## Build Status پایان Session
```
BackOffice.BFF: ✅ Build succeeded (0 errors)
CMS: ✅ Build succeeded (0 errors)
BackOffice UI: ✅ Build succeeded (0 errors)
```
+135
View File
@@ -0,0 +1,135 @@
# BackOffice Project Status
> آخرین بروزرسانی: **December 20, 2025**
---
## 🎯 وضعیت کلی
| Component | Build Status | Errors |
|-----------|--------------|--------|
| BackOffice UI | ✅ SUCCESS | 0 |
| BackOffice.BFF | ✅ SUCCESS | 0 |
| CMS Microservice | ✅ SUCCESS | 0 |
**System Status**: 🟢 **PRODUCTION READY**
---
## 📦 Proto Projects (24 پروژه فعال)
### Core Protos:
- ✅ Common.Protobuf
- ✅ Health.Protobuf
- ✅ Configuration.Protobuf
### User Management:
- ✅ User.Protobuf
- ✅ UserRole.Protobuf
- ✅ Role.Protobuf
- ✅ UserAddress.Protobuf
- ✅ UserWallet.Protobuf
- ✅ Otp.Protobuf
### Products & Shop:
- ✅ Products.Protobuf
- ✅ Category.Protobuf
- ✅ Tag.Protobuf
- ✅ ProductTag.Protobuf
- ✅ Package.Protobuf
### Discount Shop:
- ✅ DiscountProduct.Protobuf
- ✅ DiscountCategory.Protobuf
- ✅ DiscountOrder.Protobuf
- ✅ DiscountShoppingCart.Protobuf
### Network & Commission:
- ✅ NetworkMembership.Protobuf
- ✅ ClubMembership.Protobuf
- ✅ Commission.Protobuf
### Orders & Payments:
- ✅ UserOrder.Protobuf
- ✅ ManualPayment.Protobuf
### Messaging:
- ✅ PublicMessage.Protobuf
---
## 🗂️ ماژول‌های فعال
### 1. Products Module ✅
- صفحه اصلی محصولات با فیلتر و صفحه‌بندی
- ایجاد محصول جدید با آپلود تصویر
- ویرایش محصول
- گالری تصاویر محصول
- مدیریت تگ‌های محصول
- ویرایش گروهی (قیمت، موجودی، وضعیت)
- **ستون موجودی** با رنگ‌بندی هوشمند (🔴🟡🟢)
- DragDrop دسته‌بندی محصولات
### 2. Discount Shop Module ✅
- مدیریت محصولات تخفیفی
- مدیریت دسته‌بندی‌ها
- مدیریت سفارشات
- گزارش فروش
### 3. Commission Module ✅
- داشبورد استخر هفتگی
- لیست پرداخت‌ها
- لیست برداشت‌ها
- بالانس‌های هفتگی کاربران
### 4. Network Module ✅
- لیست اعضای شبکه
- نمای درختی شبکه
- آمار شبکه
### 5. Club Module ✅
- لیست اعضای باشگاه
- آمار باشگاه
- مدیریت ویژگی‌های باشگاه
### 6. Tag Module ✅
- مدیریت تگ‌ها (CRUD)
- اختصاص تگ به محصولات
### 7. Public Messages Module ✅
- مدیریت پیام‌های عمومی
- قالب‌های پیام
### 8. Manual Payments Module ✅
- ثبت پرداخت دستی
- تایید/رد پرداخت
### 9. System Management ✅
- تنظیمات سیستم
- لاگ تغییرات
- Health Check
### 10. Dashboard ✅
- ویجت آمار فروشگاه تخفیفی (7 روز اخیر)
---
## 📊 آمار
| Metric | Value |
|--------|-------|
| Build Errors | 0 |
| Proto Projects | 24 |
| Active Pages | 40+ |
| Active Components | 60+ |
| Excluded Files | 0 |
| Test Coverage | N/A |
---
## 🔧 Environment
- **Framework**: Blazor WebAssembly .NET 9.0
- **UI Library**: MudBlazor 8.14.0
- **gRPC**: Grpc.Net.Client 2.70.0
- **Mapping**: Mapster 7.4.0+
+230
View File
@@ -0,0 +1,230 @@
# BackOffice Technical Notes
> نکات فنی برای توسعه‌دهندگان
---
## 1. Mapster Mapping Patterns
### 1.1 Proto Types (Immutable)
برای proto types که immutable هستند، باید از `MapWith` استفاده کرد:
```csharp
config.NewConfig<SourceDto, ProtoResponse>()
.MapWith(src => new ProtoResponse
{
Field1 = src.Field1,
Field2 = src.Field2 ?? string.Empty,
RepeatedField = { src.List?.Select(x => new Item { ... }) ?? Enumerable.Empty<Item>() }
});
```
### 1.2 Null-Safe MetaData
```csharp
MetaData = src.MetaData != null ? new MetaData
{
PageIndex = src.MetaData.PageIndex,
TotalPages = src.MetaData.TotalPages,
TotalCount = src.MetaData.TotalCount
} : null
```
### 1.3 Alias Imports برای Disambiguation
وقتی دو proto با نام یکسان داریم:
```csharp
using BffProtos = BackOffice.BFF.ClubMembership.Protobuf.Protos.ClubMembership;
using CmsProtos = CMSMicroservice.Protobuf.Protos.ClubMembership;
// استفاده:
config.NewConfig<BffProtos.GetRequest, CmsProtos.GetRequest>();
```
### 1.4 PaginationState Mapping
```csharp
config.NewConfig<BffRequest, AppQuery>()
.Map(dest => dest.PaginationState, src => src.PaginationState);
```
---
## 2. MudBlazor 8 Breaking Changes
### 2.1 Dialog Instance
```csharp
// ❌ قبلی
[CascadingParameter] MudDialogInstance MudDialog { get; set; }
// ✅ جدید
[CascadingParameter] IMudDialogInstance MudDialog { get; set; }
```
### 2.2 Generic Components
```razor
<!-- ❌ قبلی -->
<MudSwitch @bind-Value="isActive" />
<MudChip>Text</MudChip>
<!-- ✅ جدید -->
<MudSwitch T="bool" @bind-Value="isActive" />
<MudChip T="string">Text</MudChip>
```
### 2.3 Drag Events
```razor
<!-- ❌ قبلی -->
@ondragover="e => e.PreventDefault()"
<!-- ✅ جدید -->
@ondragover:preventDefault
```
### 2.4 File Upload
```csharp
// FilesChanged حالا IBrowserFile می‌گیرد
<MudFileUpload T="IBrowserFile" FilesChanged="OnFileSelected">
```
---
## 3. gRPC Patterns
### 3.1 Service Override in BFF
```csharp
public override async Task<GetResponse> GetData(GetRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetRequest, GetQuery, GetResponse>(request, context);
}
```
### 3.2 CQRS Handler
```csharp
public class GetQueryHandler : IRequestHandler<GetQuery, GetResponseDto>
{
private readonly IApplicationContractContext _context;
public async Task<GetResponseDto> Handle(GetQuery request, CancellationToken ct)
{
var cmsRequest = request.Adapt<CmsProtos.GetRequest>();
var response = await _context.Service.GetAsync(cmsRequest, cancellationToken: ct);
return response.Adapt<GetResponseDto>();
}
}
```
---
## 4. Proto Update Checklist
هر تغییری در Proto نیاز به این مراحل دارد:
### Step 1: Update Version
```xml
<!-- در .csproj -->
<Version>0.0.142</Version><Version>0.0.143</Version>
```
### Step 2: Pack
```bash
cd path/to/proto/project
dotnet pack -c Release
# Push به GitLab Registry خودکار انجام می‌شود
```
### Step 3: Update References
```xml
<PackageReference Include="Foursat.Proto" Version="0.0.143" />
```
### Step 4: Build & Test
```bash
dotnet build
dotnet test
```
---
## 5. Common Fixes
### 5.1 Snackbar Duplicate Injection
اگر در `_Imports.razor` inject شده، در component نیاز نیست:
```csharp
// ❌ حذف کن
[Inject] ISnackbar Snackbar { get; set; }
```
### 5.2 BasePageComponent Reload
```csharp
private MudDataGrid<Model>? _gridData;
private async Task OnFilterSubmit()
{
if (_gridData != null)
await _gridData.ReloadServerData();
}
```
### 5.3 Nullable Wrapper Types
```csharp
// Proto nullable types:
// google.protobuf.Int64Value → long?
// google.protobuf.BoolValue → bool?
// Set value:
request.UserId = userId; // نه new Int64Value { Value = userId }
```
---
## 6. Build Commands
```bash
# Full Solution Build
cd /home/masoud/Apps/project/FourSat/BackOffice/src
dotnet build BackOffice.sln
# Single Project
dotnet build BackOffice/BackOffice.csproj
# With Restore
dotnet build --restore
# Clean Build
dotnet clean && dotnet build
# Check Errors Only
dotnet build 2>&1 | grep -E "error CS"
```
---
## 7. Project References
### ProjectReference (Local Development):
```xml
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/X.Protobuf/X.Protobuf.csproj" />
```
### PackageReference (Production):
```xml
<PackageReference Include="Foursat.X.Protobuf" Version="0.0.143" />
```
---
## 8. File Organization
```
BackOffice/
├── docs/
│ ├── README.md # Index
│ ├── STATUS.md # Current Status
│ ├── CHANGELOG.md # History
│ ├── TECHNICAL-NOTES.md # This file
│ └── SESSION-*.md # Session logs
├── src/
│ └── BackOffice/
│ ├── Pages/ # Blazor pages
│ ├── Services/ # gRPC clients
│ └── Common/ # Shared components
```
+1 -1
View File
@@ -116,7 +116,7 @@
<PackageReference Include="Foursat.BackOffice.BFF.ClubMembership.Protobuf" Version="0.0.7"/>
<PackageReference Include="Foursat.BackOffice.BFF.Commission.Protobuf" Version="0.0.13"/>
<PackageReference Include="Foursat.BackOffice.BFF.Common.Protobuf" Version="0.0.3"/>
<PackageReference Include="Foursat.BackOffice.BFF.Configuration.Protobuf" Version="1.0.7"/>
<PackageReference Include="Foursat.BackOffice.BFF.Configuration.Protobuf" Version="1.0.20"/>
<PackageReference Include="Foursat.BackOffice.BFF.DiscountCategory.Protobuf" Version="0.0.3"/>
<PackageReference Include="Foursat.BackOffice.BFF.DiscountOrder.Protobuf" Version="0.0.3"/>
<PackageReference Include="Foursat.BackOffice.BFF.DiscountProduct.Protobuf" Version="0.0.3"/>
@@ -12,6 +12,7 @@ using Foursat.BackOffice.BFF.ClubMembership.Protos;
using Foursat.BackOffice.BFF.Configuration.Protos;
using Foursat.BackOffice.BFF.NetworkMembership.Protos;
using Foursat.BackOffice.BFF.Health.Protobuf;
using BackOffice.BFF.Configuration.Protobuf.Protos.AppVersion;
// TODO: Create these proto projects - temporarily disabled
// using BackOffice.BFF.DiscountProduct.Protobuf.Protos.DiscountProduct;
@@ -68,6 +69,7 @@ public static class ConfigureServices
// Application Services
services.AddScoped<BackOffice.Services.Authorization.IAuthorizationService, BackOffice.Services.Authorization.AuthorizationService>();
services.AddScoped<BackOffice.Services.AppVersion.IAppVersionService, BackOffice.Services.AppVersion.AppVersionService>();
// TODO: Re-enable when proto projects are created
// services.AddScoped<IDiscountProductService, DiscountProductService>();
// services.AddScoped<IDiscountCategoryService, DiscountCategoryService>();
@@ -116,6 +118,7 @@ public static class ConfigureServices
services.AddTransient(sp => new ClubMembershipContract.ClubMembershipContractClient(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 AppVersionContract.AppVersionContractClient(sp.GetRequiredService<CallInvoker>()));
// TODO: Re-enable when proto projects are created
// Discount Shop Services
@@ -139,8 +139,24 @@
</MudCardContent>
</MudCard>
<!-- Discount Shop Stats -->
<BackOffice.Pages.Dashboard.DiscountShopWidget />
<!-- Discount Shop Stats - TEMPORARILY DISABLED (needs DiscountOrder proto) -->
@* <BackOffice.Pages.Dashboard.DiscountShopWidget /> *@
<MudCard Class="mb-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.Discount" Class="mr-2" />
آمار فروشگاه تخفیفی
</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudAlert Severity="Severity.Info" Dense="true">
این بخش در حال توسعه است و به‌زودی فعال می‌شود.
</MudAlert>
</MudCardContent>
</MudCard>
<!-- Quick Actions -->
<MudCard>
@@ -215,8 +231,8 @@
</MudContainer>
@code {
[Inject] public CommissionContract.CommissionContractClient CommissionClient { get; set; }
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubClient { get; set; }
[Inject] public CommissionContract.CommissionContractClient CommissionClient { get; set; } = null!;
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubClient { get; set; } = null!;
private bool _loading = false;
private long _currentWeekDefinitionId = 0;
@@ -1,5 +1,6 @@
@page "/network/tree"
@attribute [Authorize]
@implements IAsyncDisposable
@inject IJSRuntime JS
@using Foursat.BackOffice.BFF.NetworkMembership.Protos
@@ -8,6 +9,7 @@
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-4">
<MudText Typo="Typo.h4" Class="mb-4">درخت شبکه</MudText>
@* Search & Filter Panel *@
<MudPaper Class="pa-4 mb-4">
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center" Class="mb-3">
<div style="max-width: 300px; min-width: 250px;">
@@ -24,50 +26,97 @@
</MudStack>
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center">
<div style="max-width: 200px; min-width: 150px;">
<MudSelect T="bool?" Label="وضعیت باشگاه" @bind-Value="_clubActiveFilter" Variant="Variant.Outlined" Clearable="true">
<MudSelect T="int" @bind-Value="_selectedDepth" Label="عمق نمایش" Variant="Variant.Outlined"
Dense="true" Margin="Margin.Dense" Style="width: 120px;">
<MudSelectItem Value="3">3 سطح</MudSelectItem>
<MudSelectItem Value="5">5 سطح</MudSelectItem>
<MudSelectItem Value="10">10 سطح</MudSelectItem>
<MudSelectItem Value="15">15 سطح</MudSelectItem>
<MudSelectItem Value="100">همه</MudSelectItem>
</MudSelect>
<MudSelect T="bool?" Label="وضعیت باشگاه" @bind-Value="_clubActiveFilter" Variant="Variant.Outlined"
Clearable="true" Dense="true" Style="width: 140px;">
<MudSelectItem T="bool?" Value="@(null)">همه</MudSelectItem>
<MudSelectItem T="bool?" Value="@(true)">فعال</MudSelectItem>
<MudSelectItem T="bool?" Value="@(false)">غیرفعال</MudSelectItem>
</MudSelect>
</div>
<div style="max-width: 200px; min-width: 150px;">
<WeekNumberPicker Label="هفته فعالسازی"
@bind-SelectedWeekDefinitionId="_activationWeekFilter" />
</div>
<MudSpacer />
</MudStack>
</MudPaper>
@* Chart Toolbar *@
@if (_treeData != null && _treeData.Nodes.Any())
{
<MudPaper Class="pa-2 mb-2">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
<MudButtonGroup Variant="Variant.Outlined" Size="Size.Small">
<MudIconButton Icon="@Icons.Material.Filled.UnfoldMore" OnClick="ExpandAll" Title="باز کردن همه" />
<MudIconButton Icon="@Icons.Material.Filled.UnfoldLess" OnClick="CollapseAll" Title="بستن همه" />
<MudIconButton Icon="@Icons.Material.Filled.CenterFocusStrong" OnClick="FitChart" Title="نمایش کامل" />
<MudIconButton Icon="@Icons.Material.Filled.Image" OnClick="ExportPng" Title="خروجی تصویر" />
</MudButtonGroup>
@if (_currentViewUserId.HasValue && _currentViewUserId != _searchUserId)
{
<MudButtonGroup Variant="Variant.Outlined" Size="Size.Small" Color="Color.Secondary">
<MudButton StartIcon="@Icons.Material.Filled.ArrowForward" OnClick="GoBack" Size="Size.Small">
بازگشت
</MudButton>
<MudButton StartIcon="@Icons.Material.Filled.Home" OnClick="GoToRoot" Size="Size.Small">
ریشه
</MudButton>
</MudButtonGroup>
}
<MudStack Row="true" Spacing="2">
<MudChip T="string" Color="Color.Info" Size="Size.Small">
کل اعضا: @_totalMembers
<MudChip T="string" Color="Color.Info" Size="Size.Small" Icon="@Icons.Material.Filled.People">
کل: @_totalMembers
</MudChip>
<MudChip T="string" Color="Color.Success" Size="Size.Small">
زیرمجموعه چپ: @_leftCount
چپ: @_leftCount
</MudChip>
<MudChip T="string" Color="Color.Warning" Size="Size.Small">
زیرمجموعه راست: @_rightCount
راست: @_rightCount
</MudChip>
</MudStack>
</MudStack>
</MudPaper>
}
@* Chart Container *@
@if (_isLoading)
{
<MudPaper Class="pa-8 d-flex justify-center">
<MudPaper Class="pa-8 d-flex flex-column align-center justify-center" Style="min-height: 500px;">
<MudProgressCircular Color="Color.Primary" Size="Size.Large" Indeterminate="true" />
<MudText Typo="Typo.body1" Class="mt-3">در حال بارگذاری درخت شبکه...</MudText>
</MudPaper>
}
else if (_hasError)
{
<MudPaper Class="pa-8 d-flex flex-column align-center justify-center" Style="min-height: 300px;">
<MudIcon Icon="@Icons.Material.Filled.Error" Color="Color.Error" Size="Size.Large" />
<MudText Typo="Typo.body1" Class="mt-2" Color="Color.Error">خطا در بارگذاری درخت</MudText>
<MudButton Variant="Variant.Text" Color="Color.Primary" OnClick="LoadTree" Class="mt-2">
تلاش مجدد
</MudButton>
</MudPaper>
}
else if (_treeData != null && _treeData.Nodes.Any())
{
<MudPaper Class="pa-4" Style="min-height: 800px;">
<div id="network-tree-container" style="width: 100%; height: 800px; overflow: auto;"></div>
<MudPaper Class="pa-0" Style="min-height: 600px; overflow: hidden;">
<div id="admin-org-chart-container" class="admin-org-chart-container"></div>
</MudPaper>
@* Data Grid *@
<MudPaper Class="pa-4 mt-4">
<MudText Typo="Typo.h6" Class="mb-3">جدول اعضای شبکه</MudText>
<MudDataGrid T="NetworkTreeNodeModel" Items="@_treeData.Nodes" Hover="true" Filterable="true" Dense="true">
<MudDataGrid T="NetworkTreeNodeModel" Items="@_treeData.Nodes" Hover="true" Filterable="true" Dense="true"
SortMode="SortMode.Multiple" RowsPerPage="10">
<Columns>
<PropertyColumn Property="x => x.UserId" Title="شناسه کاربر" />
<PropertyColumn Property="x => x.UserId" Title="شناسه" />
<PropertyColumn Property="x => x.UserName" Title="نام کاربر" />
<PropertyColumn Property="x => x.NetworkLeg" Title="موقعیت">
@@ -82,56 +131,17 @@
<PropertyColumn Property="x => x.NetworkLevel" Title="سطح" />
<PropertyColumn Property="x => x.IsClubActive" Title="وضعیت">
<PropertyColumn Property="x => x.IsClubActive" Title="باشگاه">
<CellTemplate>
<MudChip T="string"
Color="@(context.Item.IsClubActive ? Color.Success : Color.Error)"
Color="@(context.Item.IsClubActive ? Color.Success : Color.Default)"
Size="Size.Small">
@(context.Item.IsClubActive ? "فعال" : "غیرفعال")
</MudChip>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.JoinedAt" Title="تاریخ عضویت">
<CellTemplate>
@if (context.Item.JoinedAt != null)
{
@context.Item.JoinedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd")
}
else
{
<MudText Typo="Typo.body2" Color="Color.Default">-</MudText>
}
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.IsClubActive" Title="باشگاه">
<CellTemplate>
@if (context.Item.IsClubActive)
{
<MudChip T="string" Color="Color.Success" Size="Size.Small">فعال</MudChip>
}
else
{
<MudChip T="string" Color="Color.Default" Size="Size.Small">غیرفعال</MudChip>
}
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.ActivationWeekDefinitionId" Title="هفته فعالسازی">
<CellTemplate>
@if (context.Item.ActivationWeekDefinitionId != null)
{
<MudText Typo="Typo.body2">@context.Item.ActivationWeekDefinitionId</MudText>
}
else
{
<MudText Typo="Typo.body2" Color="Color.Default">-</MudText>
}
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.ClubActivatedAt" Title="تاریخ فعالسازی باشگاه">
<PropertyColumn Property="x => x.ClubActivatedAt" Title="تاریخ فعالسازی">
<CellTemplate>
@if (context.Item.ClubActivatedAt != null)
{
@@ -139,30 +149,52 @@
}
else
{
<MudText Typo="Typo.body2" Color="Color.Default">-</MudText>
<span style="color: #999;">-</span>
}
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.ActivationWeekDefinitionId" Title="هفته">
<CellTemplate>
@if (context.Item.ActivationWeekDefinitionId != null)
{
<MudChip T="string" Size="Size.Small" Color="Color.Info">
W@context.Item.ActivationWeekDefinitionId
</MudChip>
}
else
{
<span style="color: #999;">-</span>
}
</CellTemplate>
</PropertyColumn>
<TemplateColumn Title="عملیات" Sortable="false">
<CellTemplate>
<MudButton Size="Size.Small"
Variant="Variant.Text"
<MudButtonGroup Size="Size.Small" Variant="Variant.Text">
<MudIconButton Icon="@Icons.Material.Filled.AccountTree"
Color="Color.Primary"
OnClick="@(() => ViewUserDetails(context.Item.UserId))">
جزئیات
</MudButton>
Title="نمایش درخت این کاربر"
OnClick="@(() => ViewUserTree(context.Item.UserId))" />
<MudIconButton Icon="@Icons.Material.Filled.Info"
Color="Color.Info"
Title="جزئیات کاربر"
OnClick="@(() => ViewUserDetails(context.Item.UserId))" />
</MudButtonGroup>
</CellTemplate>
</TemplateColumn>
</Columns>
<PagerContent>
<MudDataGridPager T="NetworkTreeNodeModel" />
</PagerContent>
</MudDataGrid>
</MudPaper>
}
else
{
<MudPaper Class="pa-8">
<MudAlert Severity="Severity.Info">
برای نمایش درخت شبکه، شناسه کاربر را وارد کنید و دکمه "نمایش درخت" را بزنید.
<MudAlert Severity="Severity.Info" Icon="@Icons.Material.Filled.Info">
برای نمایش درخت شبکه، کاربر مورد نظر را جستجو کرده و دکمه "نمایش درخت" را بزنید.
</MudAlert>
</MudPaper>
}
@@ -173,14 +205,19 @@
[Inject] public NavigationManager NavigationManager { get; set; }
private long? _searchUserId;
private long? _currentViewUserId;
private GetNetworkTreeResponse _treeData;
private bool _isLoading;
private bool _hasError;
private int _totalMembers;
private int _leftCount;
private int _rightCount;
private DotNetObjectReference<NetworkTreeViewer> _dotNetRef;
private int _selectedDepth = 10;
private bool? _clubActiveFilter;
private long? _activationWeekFilter;
private DotNetObjectReference<NetworkTreeViewer> _dotNetRef;
private Stack<long> _navigationHistory = new();
private bool _chartNeedsInit = false;
protected override void OnInitialized()
{
@@ -189,9 +226,10 @@
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
if (_chartNeedsInit && _treeData != null && _treeData.Nodes.Any())
{
await JS.InvokeVoidAsync("NetworkTreeViewer.setDotNetReference", _dotNetRef);
_chartNeedsInit = false;
await RenderChart();
}
}
@@ -204,81 +242,186 @@
}
_isLoading = true;
StateHasChanged(); // Force render to show loading state
_hasError = false;
StateHasChanged();
try
{
var request = new GetNetworkTreeRequest
{
UserId = _searchUserId.Value,
MaxDepth = 20,
IsClubActive = _clubActiveFilter.HasValue ? _clubActiveFilter.Value : null,
ActivationWeekDefinitionId = _activationWeekFilter!=null ? _activationWeekFilter : null
MaxDepth = _selectedDepth,
IsClubActive = _clubActiveFilter,
ActivationWeekDefinitionId = _activationWeekFilter
};
_treeData = await NetworkContract.GetNetworkTreeAsync(request);
_currentViewUserId = _searchUserId;
_navigationHistory.Clear();
CalculateStats();
_isLoading = false;
StateHasChanged(); // Render the container first
await Task.Delay(100); // Wait for DOM to be ready
await RenderTree();
_chartNeedsInit = true;
StateHasChanged();
Snackbar.Add($"درخت بارگذاری شد - {_treeData.Nodes.Count} عضو", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری درخت: {ex.Message}", Severity.Error);
_hasError = true;
_isLoading = false;
Snackbar.Add($"خطا در بارگذاری درخت: {ex.Message}", Severity.Error);
StateHasChanged();
}
}
private async Task RenderTree()
private async Task LoadSubTree(long userId)
{
_isLoading = true;
StateHasChanged();
try
{
var request = new GetNetworkTreeRequest
{
UserId = userId,
MaxDepth = _selectedDepth,
IsClubActive = _clubActiveFilter,
ActivationWeekDefinitionId = _activationWeekFilter
};
_treeData = await NetworkContract.GetNetworkTreeAsync(request);
if (_currentViewUserId.HasValue && _currentViewUserId != userId)
{
_navigationHistory.Push(_currentViewUserId.Value);
}
_currentViewUserId = userId;
CalculateStats();
_isLoading = false;
_chartNeedsInit = true;
StateHasChanged();
}
catch (Exception ex)
{
_isLoading = false;
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
StateHasChanged();
}
}
private async Task RenderChart()
{
if (_treeData == null || !_treeData.Nodes.Any()) return;
try
{
await Task.Delay(50); // Wait for DOM
var jsNodes = _treeData.Nodes.Select(n => new
{
id = n.UserId.ToString(),
parentId = n.ParentId > 0 ? n.ParentId.ToString() : "",
userId = n.UserId,
userName = n.UserName,
parentId = n.ParentId,
userName = n.UserName ?? $"کاربر {n.UserId}",
networkLevel = n.NetworkLevel,
networkLeg = n.NetworkLeg,
isActive = n.IsClubActive,
isClubActive = n.IsClubActive,
isActivatedInTargetWeek = n.IsActivatedInTargetWeek,
activationWeekNumber = _activationWeekFilter ?? 0, // فیلتر UI
clubActivatedAt = n.ClubActivatedAt?.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd") ?? "",
userCreated = n.UserCreated?.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd") ?? ""
activationWeekDefinitionId = n.ActivationWeekDefinitionId,
clubActivatedAt = n.ClubActivatedAt?.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd") ?? ""
}).ToArray();
await JS.InvokeVoidAsync("NetworkTreeViewer.initialize", "network-tree-container", jsNodes);
var options = new { filterWeek = _activationWeekFilter };
await JS.InvokeVoidAsync("AdminOrgChart.init", "admin-org-chart-container", jsNodes, _dotNetRef, options);
}
catch (Exception ex)
{
Console.WriteLine($"Error rendering chart: {ex.Message}");
}
}
[JSInvokable]
public async Task OnNodeClicked(long userId)
{
_searchUserId = userId;
await LoadTree();
if (userId == _currentViewUserId) return;
await LoadSubTree(userId);
}
private void CalculateStats()
{
if (_treeData == null || !_treeData.Nodes.Any()) return;
if (_treeData == null || !_treeData.Nodes.Any())
{
_totalMembers = _leftCount = _rightCount = 0;
return;
}
_totalMembers = _treeData.Nodes.Count;
_leftCount = _treeData.Nodes.Count(n => n.NetworkLeg == 0);
_rightCount = _treeData.Nodes.Count(n => n.NetworkLeg == 1);
}
private async Task ExpandAll()
{
try { await JS.InvokeVoidAsync("AdminOrgChart.expandAll"); } catch { }
}
private async Task CollapseAll()
{
try { await JS.InvokeVoidAsync("AdminOrgChart.collapseAll"); } catch { }
}
private async Task FitChart()
{
try { await JS.InvokeVoidAsync("AdminOrgChart.fit"); } catch { }
}
private async Task ExportPng()
{
try { await JS.InvokeVoidAsync("AdminOrgChart.exportPng"); } catch { }
}
private async Task GoBack()
{
if (_navigationHistory.Count > 0)
{
var previousUserId = _navigationHistory.Pop();
_currentViewUserId = previousUserId;
await LoadSubTree(previousUserId);
}
}
private async Task GoToRoot()
{
if (_searchUserId.HasValue)
{
_navigationHistory.Clear();
await LoadSubTree(_searchUserId.Value);
}
}
private async Task ViewUserTree(long userId)
{
await LoadSubTree(userId);
}
private void ViewUserDetails(long userId)
{
NavigationManager.NavigateTo($"/network/user-info/{userId}");
}
public void Dispose()
public async ValueTask DisposeAsync()
{
try
{
await JS.InvokeVoidAsync("AdminOrgChart.dispose");
}
catch { }
_dotNetRef?.Dispose();
}
}
@@ -0,0 +1,364 @@
@page "/settings/app-versions"
@attribute [Authorize]
@using BackOffice.Services.AppVersion
@using BackOffice.Pages.Settings.Components
@inject IAppVersionService AppVersionService
@inject IDialogService DialogService
@inject ISnackbar Snackbar
<PageTitle>مدیریت نسخه اپلیکیشن‌ها</PageTitle>
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
<MudText Typo="Typo.h4" GutterBottom="true">
<MudIcon Icon="@Icons.Material.Filled.PhoneAndroid" Class="ml-2" />
مدیریت نسخه اپلیکیشن‌ها
</MudText>
<MudText Typo="Typo.body1" Color="Color.Secondary" Class="mb-4">
مدیریت نسخه‌های اپلیکیشن‌های موبایل و کنترل به‌روزرسانی اجباری
</MudText>
<MudPaper Class="pa-4">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-4">
<MudSwitch Value="_includeInactive"
Color="Color.Primary"
Label="نمایش نسخه‌های غیرفعال"
T="bool"
ValueChanged="@(async (bool val) => { _includeInactive = val; await LoadVersions(); })" />
<MudStack Row="true" Spacing="2">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="@OpenCreateDialog">
افزودن نسخه جدید
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Refresh"
OnClick="@LoadVersions">
بارگذاری مجدد
</MudButton>
</MudStack>
</MudStack>
@if (_loading)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mb-4" />
}
<MudDataGrid T="AppVersionDto"
Items="@_versions"
Loading="@_loading"
Hover="true"
Dense="true">
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<TemplateColumn Title="اپلیکیشن">
<CellTemplate>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@GetAppIcon(context.Item.AppName)"
Color="@GetAppColor(context.Item.AppName)"
Size="Size.Small" />
<MudText>@context.Item.AppNameDisplay</MudText>
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="نسخه فعلی">
<CellTemplate>
<MudChip T="string" Color="Color.Primary" Size="Size.Small" Variant="Variant.Outlined">
@context.Item.CurrentVersion
</MudChip>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="حداقل نسخه">
<CellTemplate>
@if (!string.IsNullOrEmpty(context.Item.MinRequiredVersion))
{
<MudChip T="string" Color="Color.Warning" Size="Size.Small" Variant="Variant.Outlined">
@context.Item.MinRequiredVersion
</MudChip>
}
else
{
<MudText Typo="Typo.body2" Color="Color.Default">-</MudText>
}
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="پاکسازی کش">
<CellTemplate>
@if (context.Item.RequiresFullCacheClear)
{
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Warning" Size="Size.Small" />
}
else
{
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" />
}
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="وضعیت">
<CellTemplate>
@if (context.Item.IsActive)
{
<MudChip T="string" Color="Color.Success" Size="Size.Small">فعال</MudChip>
}
else
{
<MudChip T="string" Color="Color.Default" Size="Size.Small">غیرفعال</MudChip>
}
</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.LastModified" Title="آخرین تغییر" Format="yyyy/MM/dd HH:mm" />
<TemplateColumn Title="عملیات" Sortable="false">
<CellTemplate>
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Color="Color.Primary"
Size="Size.Small"
OnClick="@(() => OpenEditDialog(context.Item))" />
<MudIconButton Icon="@Icons.Material.Filled.Visibility"
Color="Color.Info"
Size="Size.Small"
OnClick="@(() => OpenDetailsDialog(context.Item))" />
</CellTemplate>
</TemplateColumn>
</Columns>
</MudDataGrid>
</MudPaper>
@* Info Cards *@
<MudGrid Class="mt-4">
@foreach (var version in _versions.Where(v => v.IsActive))
{
<MudItem xs="12" md="6">
<MudCard Elevation="2">
<MudCardHeader>
<CardHeaderAvatar>
<MudAvatar Color="@GetAppColor(version.AppName)">
<MudIcon Icon="@GetAppIcon(version.AppName)" />
</MudAvatar>
</CardHeaderAvatar>
<CardHeaderContent>
<MudText Typo="Typo.h6">@version.AppNameDisplay</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">@version.AppName</MudText>
</CardHeaderContent>
<CardHeaderActions>
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Color="Color.Primary"
OnClick="@(() => OpenEditDialog(version))" />
</CardHeaderActions>
</MudCardHeader>
<MudCardContent>
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">نسخه فعلی:</MudText>
<MudChip T="string" Color="Color.Primary" Size="Size.Small">@version.CurrentVersion</MudChip>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">حداقل نسخه:</MudText>
@if (!string.IsNullOrEmpty(version.MinRequiredVersion))
{
<MudChip T="string" Color="Color.Warning" Size="Size.Small">@version.MinRequiredVersion</MudChip>
}
else
{
<MudText Typo="Typo.body2" Color="Color.Default">تنظیم نشده</MudText>
}
</MudStack>
@if (version.RequiresFullCacheClear)
{
<MudAlert Severity="Severity.Warning" Dense="true" Class="mt-2">
نیاز به پاکسازی کامل کش دارد
</MudAlert>
}
@if (!string.IsNullOrEmpty(version.UpdateMessage))
{
<MudDivider Class="my-2" />
<MudText Typo="Typo.caption" Color="Color.Secondary">پیام به‌روزرسانی:</MudText>
<MudText Typo="Typo.body2">@version.UpdateMessage</MudText>
}
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
}
</MudGrid>
</MudContainer>
@code {
private List<AppVersionDto> _versions = new();
private bool _loading = false;
private bool _includeInactive = false;
protected override async Task OnInitializedAsync()
{
await LoadVersions();
}
private async Task LoadVersions()
{
_loading = true;
try
{
_versions = await AppVersionService.GetAllAsync(_includeInactive);
Snackbar.Add("اطلاعات نسخه‌ها بارگذاری شد", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
private async Task OpenCreateDialog()
{
var parameters = new DialogParameters<AppVersionEditDialog>
{
{ x => x.Model, new UpdateAppVersionDto
{
AppName = "",
CurrentVersion = "1.0.0",
MinRequiredVersion = null,
RequiresFullCacheClear = false,
UpdateMessage = null,
ReleaseNotes = null
}
},
{ x => x.IsNew, true }
};
var options = new DialogOptions
{
CloseButton = true,
MaxWidth = MaxWidth.Medium,
FullWidth = true
};
var dialog = await DialogService.ShowAsync<AppVersionEditDialog>(
"افزودن نسخه جدید",
parameters,
options);
var result = await dialog.Result;
if (result is { Canceled: false, Data: UpdateAppVersionDto dto })
{
try
{
await AppVersionService.UpdateAsync(dto);
Snackbar.Add("نسخه جدید با موفقیت ایجاد شد", Severity.Success);
await LoadVersions();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در ایجاد: {ex.Message}", Severity.Error);
}
}
}
private async Task OpenEditDialog(AppVersionDto version)
{
var parameters = new DialogParameters<AppVersionEditDialog>
{
{ x => x.Model, new UpdateAppVersionDto
{
AppName = version.AppName,
CurrentVersion = version.CurrentVersion,
MinRequiredVersion = version.MinRequiredVersion,
RequiresFullCacheClear = version.RequiresFullCacheClear,
UpdateMessage = version.UpdateMessage,
ReleaseNotes = version.ReleaseNotes
}
}
};
var options = new DialogOptions
{
CloseButton = true,
MaxWidth = MaxWidth.Medium,
FullWidth = true
};
var dialog = await DialogService.ShowAsync<AppVersionEditDialog>(
$"ویرایش نسخه {version.AppNameDisplay}",
parameters,
options);
var result = await dialog.Result;
if (result is { Canceled: false, Data: UpdateAppVersionDto dto })
{
try
{
await AppVersionService.UpdateAsync(dto);
Snackbar.Add("نسخه با موفقیت به‌روزرسانی شد", Severity.Success);
await LoadVersions();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در به‌روزرسانی: {ex.Message}", Severity.Error);
}
}
}
private void OpenDetailsDialog(AppVersionDto version)
{
var parameters = new DialogParameters
{
{ "ContentText", BuildDetailsContent(version) },
{ "ButtonText", "بستن" },
{ "Color", Color.Primary }
};
DialogService.Show<MudMessageBox>($"جزئیات {version.AppNameDisplay}", parameters);
}
private string BuildDetailsContent(AppVersionDto version)
{
var lines = new List<string>
{
$"نام اپلیکیشن: {version.AppName}",
$"نسخه فعلی: {version.CurrentVersion}",
$"حداقل نسخه: {version.MinRequiredVersion ?? "تنظیم نشده"}",
$"نیاز به پاکسازی کش: {(version.RequiresFullCacheClear ? "بله" : "خیر")}",
$"وضعیت: {(version.IsActive ? "فعال" : "غیرفعال")}",
"",
"پیام به‌روزرسانی:",
version.UpdateMessage ?? "ندارد",
"",
"یادداشت‌های انتشار:",
version.ReleaseNotes ?? "ندارد",
"",
$"تاریخ ایجاد: {version.Created?.ToString("yyyy/MM/dd HH:mm") ?? "-"}",
$"آخرین تغییر: {version.LastModified?.ToString("yyyy/MM/dd HH:mm") ?? "-"}"
};
return string.Join(Environment.NewLine, lines);
}
private string GetAppIcon(string appName) => appName switch
{
"KaraBazarApp" => Icons.Material.Filled.ShoppingCart,
"KaraBazarAdminApp" => Icons.Material.Filled.AdminPanelSettings,
_ => Icons.Material.Filled.PhoneAndroid
};
private Color GetAppColor(string appName) => appName switch
{
"KaraBazarApp" => Color.Primary,
"KaraBazarAdminApp" => Color.Secondary,
_ => Color.Default
};
}
@@ -0,0 +1,100 @@
@using BackOffice.Services.AppVersion
@inject ISnackbar Snackbar
<MudDialog>
<DialogContent>
<MudForm @ref="_form" @bind-IsValid="_isValid">
<MudStack Spacing="3">
@if (IsNew)
{
<MudSelect @bind-Value="Model.AppName"
Label="نام اپلیکیشن"
Variant="Variant.Outlined"
Required="true"
RequiredError="انتخاب اپلیکیشن اجباری است">
<MudSelectItem Value="@("KaraBazarApp")">کارابازار</MudSelectItem>
<MudSelectItem Value="@("KaraBazarAdminApp")">ادمین کارابازار</MudSelectItem>
</MudSelect>
}
else
{
<MudTextField @bind-Value="Model.AppName"
Label="نام اپلیکیشن"
Variant="Variant.Outlined"
ReadOnly="true"
Disabled="true" />
}
<MudTextField @bind-Value="Model.CurrentVersion"
Label="نسخه فعلی"
Variant="Variant.Outlined"
Required="true"
RequiredError="نسخه فعلی اجباری است"
HelperText="مثال: 1.2.0" />
<MudTextField @bind-Value="Model.MinRequiredVersion"
Label="حداقل نسخه مورد نیاز"
Variant="Variant.Outlined"
HelperText="کاربران با نسخه‌های پایین‌تر مجبور به آپدیت می‌شوند" />
<MudSwitch @bind-Value="Model.RequiresFullCacheClear"
Color="Color.Warning"
Label="نیاز به پاکسازی کامل کش" />
<MudTextField @bind-Value="Model.UpdateMessage"
Label="پیام به‌روزرسانی"
Variant="Variant.Outlined"
Lines="2"
HelperText="پیامی که به کاربر نمایش داده می‌شود" />
<MudTextField @bind-Value="Model.ReleaseNotes"
Label="یادداشت‌های انتشار"
Variant="Variant.Outlined"
Lines="4"
HelperText="توضیحات تغییرات این نسخه" />
<MudDivider Class="my-2" />
<MudTextField @bind-Value="Model.UpdateReason"
Label="دلیل تغییر (برای لاگ)"
Variant="Variant.Outlined"
Required="true"
RequiredError="دلیل تغییر اجباری است"
Lines="2"
HelperText="این متن در لاگ ذخیره می‌شود" />
</MudStack>
</MudForm>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel" Color="Color.Default" Variant="Variant.Text">
انصراف
</MudButton>
<MudButton OnClick="Submit" Color="Color.Primary" Variant="Variant.Filled" Disabled="@(!_isValid)">
ذخیره
</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter]
private IMudDialogInstance MudDialog { get; set; } = null!;
[Parameter]
public UpdateAppVersionDto Model { get; set; } = new();
[Parameter]
public bool IsNew { get; set; } = false;
private MudForm? _form;
private bool _isValid;
private void Cancel() => MudDialog.Cancel();
private void Submit()
{
if (_isValid)
{
MudDialog.Close(DialogResult.Ok(Model));
}
}
}
@@ -0,0 +1,90 @@
using BackOffice.BFF.Configuration.Protobuf.Protos.AppVersion;
using Google.Protobuf.WellKnownTypes;
namespace BackOffice.Services.AppVersion;
public class AppVersionService : IAppVersionService
{
private readonly AppVersionContract.AppVersionContractClient _client;
public AppVersionService(AppVersionContract.AppVersionContractClient client)
{
_client = client;
}
public async Task<List<AppVersionDto>> GetAllAsync(bool includeInactive = false)
{
var request = new GetAllAppVersionsRequest
{
IncludeInactive = includeInactive
};
var response = await _client.GetAllAppVersionsAsync(request);
return response.Items.Select(item => new AppVersionDto
{
Id = item.Id,
AppName = item.AppName,
CurrentVersion = item.CurrentVersion,
MinRequiredVersion = item.MinRequiredVersion,
RequiresFullCacheClear = item.RequiresFullCacheClear,
UpdateMessage = item.UpdateMessage,
ReleaseNotes = item.ReleaseNotes,
IsActive = item.IsActive,
Created = item.Created?.ToDateTime(),
LastModified = item.LastModified?.ToDateTime()
}).ToList();
}
public async Task<AppVersionDto?> GetByNameAsync(string appName)
{
var request = new GetAppVersionRequest
{
AppName = appName
};
var response = await _client.GetAppVersionAsync(request);
if (!response.Found || response.Item == null)
return null;
var item = response.Item;
return new AppVersionDto
{
Id = item.Id,
AppName = item.AppName,
CurrentVersion = item.CurrentVersion,
MinRequiredVersion = item.MinRequiredVersion,
RequiresFullCacheClear = item.RequiresFullCacheClear,
UpdateMessage = item.UpdateMessage,
ReleaseNotes = item.ReleaseNotes,
IsActive = item.IsActive,
Created = item.Created?.ToDateTime(),
LastModified = item.LastModified?.ToDateTime()
};
}
public async Task UpdateAsync(UpdateAppVersionDto dto)
{
var request = new UpdateAppVersionRequest
{
AppName = dto.AppName,
CurrentVersion = dto.CurrentVersion,
RequiresFullCacheClear = dto.RequiresFullCacheClear
};
if (!string.IsNullOrWhiteSpace(dto.MinRequiredVersion))
request.MinRequiredVersion = dto.MinRequiredVersion;
if (!string.IsNullOrWhiteSpace(dto.UpdateMessage))
request.UpdateMessage = dto.UpdateMessage;
if (!string.IsNullOrWhiteSpace(dto.ReleaseNotes))
request.ReleaseNotes = dto.ReleaseNotes;
if (!string.IsNullOrWhiteSpace(dto.UpdateReason))
request.UpdateReason = dto.UpdateReason;
await _client.UpdateAppVersionAsync(request);
}
}
@@ -0,0 +1,41 @@
namespace BackOffice.Services.AppVersion;
public interface IAppVersionService
{
Task<List<AppVersionDto>> GetAllAsync(bool includeInactive = false);
Task<AppVersionDto?> GetByNameAsync(string appName);
Task UpdateAsync(UpdateAppVersionDto dto);
}
public class AppVersionDto
{
public long Id { get; set; }
public string AppName { get; set; } = string.Empty;
public string AppNameDisplay => GetDisplayName(AppName);
public string CurrentVersion { get; set; } = string.Empty;
public string MinRequiredVersion { get; set; } = string.Empty;
public bool RequiresFullCacheClear { get; set; }
public string UpdateMessage { get; set; } = string.Empty;
public string ReleaseNotes { get; set; } = string.Empty;
public bool IsActive { get; set; }
public DateTime? Created { get; set; }
public DateTime? LastModified { get; set; }
private static string GetDisplayName(string appName) => appName switch
{
"FoursatMarketApp" => "اپ مارکت فورست",
"FoursatClubApp" => "اپ باشگاه فورست",
_ => appName
};
}
public class UpdateAppVersionDto
{
public string AppName { get; set; } = string.Empty;
public string CurrentVersion { get; set; } = string.Empty;
public string? MinRequiredVersion { get; set; }
public bool RequiresFullCacheClear { get; set; }
public string? UpdateMessage { get; set; }
public string? ReleaseNotes { get; set; }
public string? UpdateReason { get; set; }
}
+11
View File
@@ -252,6 +252,15 @@
تنظیمات سیستم
</MudNavLink>
}
@if (CanViewSettings)
{
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/settings/app-versions"
Icon="@Icons.Material.Filled.PhoneAndroid">
نسخه اپلیکیشن‌ها
</MudNavLink>
}
</Authorized>
</AuthorizeView>
@@ -295,6 +304,7 @@
private bool CanViewSystemAlerts;
private bool CanViewSystemHealth;
private bool CanManageSystemConfiguration;
private bool CanViewSettings;
protected override async Task OnInitializedAsync()
{
@@ -323,6 +333,7 @@
CanViewSystemAlerts = await AuthorizationService.HasPermissionAsync("system.alerts.view");
CanViewSystemHealth = await AuthorizationService.HasPermissionAsync("system.health.view");
CanManageSystemConfiguration = await AuthorizationService.HasPermissionAsync("settings.manage_configuration");
CanViewSettings = await AuthorizationService.HasPermissionAsync("settings.view");
_initialized = true;
StateHasChanged();
+1 -3
View File
@@ -1,9 +1,7 @@
{
// "GwUrl": "https://bogw.kbs1.ir",
"GwUrl": "https://backoffice-bff.foursat.afrino.co",
// "GwUrl": "https://localhost:6468",
"GwUrl": "https://backoffice-bff.foursat.afrino.co",
"Authentication": {
//"Authority": "https://localhost:5001",
"Authority": "https://ids.afrino.co/",
"ClientId": "client_backoffice_spa"
}
@@ -0,0 +1,272 @@
/* Admin Org Chart Styles */
.admin-org-chart-container {
width: 100%;
height: 600px;
background: #fafafa;
border-radius: 4px;
}
/* Node Card */
.admin-node-card {
background: white;
border-radius: 8px;
padding: 8px 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 2px solid #e0e0e0;
min-width: 140px;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
}
.admin-node-card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
/* Position colors */
.admin-node-card.leg-left {
border-left: 4px solid #4caf50;
}
.admin-node-card.leg-right {
border-left: 4px solid #ff9800;
}
.admin-node-card.leg-root {
border-left: 4px solid #1976d2;
}
/* Club status */
.admin-node-card.club-active {
border-top: 2px solid #4caf50;
}
.admin-node-card.club-inactive {
border-top: 2px solid #e0e0e0;
opacity: 0.85;
}
/* Node header */
.admin-node-card .node-header {
display: flex;
align-items: center;
gap: 8px;
}
.admin-node-card .node-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: linear-gradient(135deg, #1976d2 0%, #42a5f5 100%);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 14px;
}
.admin-node-card.leg-left .node-avatar {
background: linear-gradient(135deg, #388e3c 0%, #66bb6a 100%);
}
.admin-node-card.leg-right .node-avatar {
background: linear-gradient(135deg, #f57c00 0%, #ffb74d 100%);
}
.admin-node-card .node-main-info {
flex: 1;
min-width: 0;
}
.admin-node-card .node-name {
font-weight: 600;
font-size: 12px;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100px;
}
.admin-node-card .node-meta {
display: flex;
align-items: center;
gap: 6px;
margin-top: 2px;
}
.admin-node-card .level-badge {
background: #e3f2fd;
color: #1976d2;
font-size: 10px;
padding: 1px 6px;
border-radius: 10px;
font-weight: 500;
}
.admin-node-card .leg-text-left {
color: #4caf50;
font-size: 10px;
font-weight: 500;
}
.admin-node-card .leg-text-right {
color: #ff9800;
font-size: 10px;
font-weight: 500;
}
/* Node footer */
.admin-node-card .node-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 6px;
padding-top: 6px;
border-top: 1px solid #f0f0f0;
font-size: 10px;
}
.admin-node-card .club-status {
font-weight: 500;
}
.admin-node-card .club-status.club-active {
color: #4caf50;
}
.admin-node-card .club-status.club-inactive {
color: #9e9e9e;
}
.admin-node-card .activation-date {
color: #757575;
font-size: 9px;
}
/* Week badge */
.admin-node-card .week-badge {
position: absolute;
top: -8px;
right: -8px;
padding: 2px 6px;
border-radius: 10px;
font-size: 9px;
font-weight: bold;
}
.admin-node-card .week-badge.week-match {
background: #4caf50;
color: white;
}
.admin-node-card .week-badge.week-other {
background: #ff5722;
color: white;
}
/* Week filter disabled state */
.admin-node-card.week-disabled {
opacity: 0.35;
filter: grayscale(80%);
transform: scale(0.95);
box-shadow: none;
border-color: #e0e0e0 !important;
}
.admin-node-card.week-disabled:hover {
opacity: 0.5;
transform: scale(0.97);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.admin-node-card.week-disabled .node-avatar {
background: #bdbdbd !important;
}
.admin-node-card.week-disabled .node-name {
color: #9e9e9e;
}
.admin-node-card.week-disabled .club-status {
color: #bdbdbd !important;
}
/* Target week highlight (sparkle badge) */
.admin-node-card .target-week-highlight {
position: absolute;
top: -12px;
left: -12px;
font-size: 18px;
animation: pulse-highlight 1.5s ease-in-out infinite;
}
@keyframes pulse-highlight {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.2); opacity: 0.8; }
}
/* Enhanced styling for target week nodes */
.admin-node-card:not(.week-disabled) {
/* Normal nodes when filter is active get subtle enhancement */
}
/* When week filter is active, make matching nodes stand out more */
.admin-node-card.leg-left:not(.week-disabled),
.admin-node-card.leg-right:not(.week-disabled) {
/* These will naturally stand out against disabled ones */
}
/* Expand button */
.admin-expand-btn {
width: 20px;
height: 20px;
border-radius: 50%;
background: #1976d2;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: bold;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.admin-expand-btn:hover {
background: #1565c0;
}
/* Messages */
.admin-org-chart-container .no-data-message,
.admin-org-chart-container .error-message {
padding: 40px;
text-align: center;
color: #666;
}
/* RTL support */
[dir="rtl"] .admin-node-card {
border-left: none;
border-right: 4px solid #e0e0e0;
}
[dir="rtl"] .admin-node-card.leg-left {
border-right-color: #4caf50;
}
[dir="rtl"] .admin-node-card.leg-right {
border-right-color: #ff9800;
}
[dir="rtl"] .admin-node-card.leg-root {
border-right-color: #1976d2;
}
[dir="rtl"] .admin-node-card .week-badge {
right: auto;
left: -8px;
}
+4
View File
@@ -14,6 +14,7 @@
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
<link href="_content/Tizzani.MudBlazor.HtmlEditor/MudHtmlEditor.css" rel="stylesheet" />
<link href="css/admin-org-chart.css" rel="stylesheet" />
<!-- If you add any scoped CSS files, uncomment the following to load them
<link href="BackOffice.styles.css" rel="stylesheet" /> -->
@@ -35,6 +36,9 @@
</div>
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
<script src="/js/d3.v7.min.js"></script>
<script src="/js/d3-flextree.min.js"></script>
<script src="/js/d3-org-chart3.js"></script>
<script src="/js/admin-org-chart.js"></script>
<script src="/js/main.js"></script>
<script src="/js/quill.js"></script>
<script src="/js/network-tree.js"></script>
@@ -0,0 +1,243 @@
/**
* d3-org-chart integration for BackOffice Admin
* Network Binary Tree visualization with admin features
*/
window.AdminOrgChart = {
chart: null,
dotNetHelper: null,
containerId: null,
/**
* Initialize the organization chart
* @param {string} containerId - The ID of the container element
* @param {object} data - The tree data in flat array format
* @param {object} dotNetHelper - Blazor .NET helper for callbacks
* @param {object} options - Chart options (filterWeek, etc.)
*/
init: function (containerId, data, dotNetHelper, options = {}) {
this.dotNetHelper = dotNetHelper;
this.containerId = containerId;
const container = document.getElementById(containerId);
if (!container) {
console.error('AdminOrgChart: Container not found:', containerId);
return;
}
// Clear previous chart
container.innerHTML = '';
if (!data || data.length === 0) {
container.innerHTML = '<div class="no-data-message" style="padding: 40px; text-align: center; color: #666;">داده‌ای برای نمایش وجود ندارد</div>';
return;
}
try {
const filterWeek = options.filterWeek || null;
this.chart = new d3.OrgChart()
.container('#' + containerId)
.data(data)
.nodeWidth((d) => 160)
.nodeHeight((d) => 80)
.childrenMargin((d) => 60)
.compactMarginBetween((d) => 20)
.compactMarginPair((d) => 20)
.neighbourMargin((a, b) => 20)
.siblingsMargin((d) => 20)
.buttonContent(({ node, state }) => {
const hasChildren = node.data._directSubordinates > 0;
const isExpanded = node.children;
return hasChildren ? `<div class="admin-expand-btn">
<span>${isExpanded ? '' : '+'}</span>
</div>` : '';
})
.linkUpdate(function (d, i, arr) {
d3.select(this)
.attr('stroke', (d) => d.data._highlighted || d.data._upToTheRootHighlighted ? '#1976d2' : '#bdbdbd')
.attr('stroke-width', (d) => d.data._highlighted || d.data._upToTheRootHighlighted ? 3 : 2);
})
.nodeContent(function (d, i, arr, state) {
const data = d.data;
const isRoot = !data.parentId || data.parentId === '';
// Position styling
const positionClass = data.networkLeg === 0 ? 'leg-left' :
data.networkLeg === 1 ? 'leg-right' : 'leg-root';
// Club status
const clubClass = data.isClubActive ? 'club-active' : 'club-inactive';
// Week filter: check if this node is activated in target week
const isTargetWeek = data.isActivatedInTargetWeek;
const weekFilterActive = filterWeek && filterWeek > 0;
const isDisabledByWeekFilter = weekFilterActive && !isTargetWeek;
const disabledClass = isDisabledByWeekFilter ? 'week-disabled' : '';
// Week activation status badge
let weekIndicator = '';
if (weekFilterActive && data.activationWeekDefinitionId) {
weekIndicator = `<div class="week-badge ${isTargetWeek ? 'week-match' : 'week-other'}">
W${data.activationWeekDefinitionId}
</div>`;
}
// Highlight badge for target week matches
let highlightBadge = '';
if (weekFilterActive && isTargetWeek) {
highlightBadge = '<div class="target-week-highlight">✨</div>';
}
// Avatar - first letter of name
const firstChar = data.userName ? data.userName.charAt(0).toUpperCase() : '?';
// Level badge
const levelBadge = `<span class="level-badge">L${data.networkLevel || 0}</span>`;
// Leg indicator
const legText = isRoot ? '' : (data.networkLeg === 0 ? 'چپ' : 'راست');
const legClass = data.networkLeg === 0 ? 'leg-text-left' : 'leg-text-right';
// Build tooltip text
const tooltipLines = [
`👤 ${data.userName || 'کاربر ' + data.userId}`,
`🆔 شناسه: ${data.userId}`,
`📊 سطح: ${data.networkLevel || 0}`,
`${data.networkLeg === 0 ? '⬅️' : '➡️'} موقعیت: ${data.networkLeg === 0 ? 'چپ' : 'راست'}`,
`${data.isClubActive ? '✅' : '❌'} باشگاه: ${data.isClubActive ? 'فعال' : 'غیرفعال'}`
];
if (data.clubActivatedAt) {
tooltipLines.push(`📅 فعالسازی: ${data.clubActivatedAt}`);
}
if (data.activationWeekDefinitionId) {
tooltipLines.push(`📆 هفته: ${data.activationWeekDefinitionId}`);
}
if (weekFilterActive) {
tooltipLines.push(isTargetWeek ? '🎯 فعال در هفته انتخابی' : '⚪ خارج از هفته انتخابی');
}
const tooltipText = tooltipLines.join('&#10;');
return `
<div class="admin-node-card ${positionClass} ${clubClass} ${disabledClass}"
data-user-id="${data.userId}"
title="${tooltipText}">
${weekIndicator}
${highlightBadge}
<div class="node-header">
<div class="node-avatar">${firstChar}</div>
<div class="node-main-info">
<div class="node-name">${data.userName || 'کاربر ' + data.userId}</div>
<div class="node-meta">
${levelBadge}
${legText ? `<span class="${legClass}">${legText}</span>` : ''}
</div>
</div>
</div>
<div class="node-footer">
<span class="club-status ${clubClass}">
${data.isClubActive ? '✓ فعال' : '✗ غیرفعال'}
</span>
${data.clubActivatedAt ? `<span class="activation-date">${data.clubActivatedAt}</span>` : ''}
</div>
</div>
`;
})
.onNodeClick((d) => {
if (this.dotNetHelper) {
const userId = parseInt(d.data.userId, 10);
this.dotNetHelper.invokeMethodAsync('OnNodeClicked', userId);
}
})
.render();
// Initial fit
setTimeout(() => {
if (this.chart) {
this.chart.fit();
}
}, 100);
} catch (error) {
console.error('AdminOrgChart: Error initializing chart:', error);
container.innerHTML = '<div class="error-message" style="padding: 40px; text-align: center; color: #f44336;">خطا در بارگذاری نمودار</div>';
}
},
/**
* Update the chart with new data
*/
update: function (data, options = {}) {
if (this.chart && this.containerId) {
// Re-init with new data
this.init(this.containerId, data, this.dotNetHelper, options);
}
},
/**
* Expand all nodes
*/
expandAll: function () {
if (this.chart) {
this.chart.expandAll().render();
}
},
/**
* Collapse all nodes
*/
collapseAll: function () {
if (this.chart) {
this.chart.collapseAll().render();
}
},
/**
* Center/Fit the chart
*/
fit: function () {
if (this.chart) {
this.chart.fit();
}
},
/**
* Zoom to specific node
*/
zoomToNode: function (nodeId) {
if (this.chart) {
this.chart.setCentered(nodeId.toString()).render();
}
},
/**
* Export chart as PNG
*/
exportPng: function () {
if (this.chart) {
this.chart.exportImg({ full: true });
}
},
/**
* Export chart as SVG
*/
exportSvg: function () {
if (this.chart) {
this.chart.exportSvg();
}
},
/**
* Dispose the chart
*/
dispose: function () {
if (this.chart) {
this.chart = null;
this.dotNetHelper = null;
this.containerId = null;
}
}
};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long