Refactor code structure for improved readability and maintainability

This commit is contained in:
masoodafar-web
2026-02-16 00:59:16 +03:30
parent 956a9ff6d6
commit ad31c8be97
10 changed files with 3388 additions and 3 deletions
+359
View File
@@ -0,0 +1,359 @@
# 🏗️ BackOffice — مرجع معماری و الگوها
> **تاریخ:** ۱۴۰۴/۱۱/۲۴ (February 13, 2026)
> **پروژه:** BackOffice Admin Panel (Blazor WebAssembly)
---
## ۱. معماری کلی
```
┌─────────────────────────────────────────────────┐
│ BackOffice │
│ (Blazor WebAssembly) │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ MudBlazor│ │ Mapster │ │ DateTimeCvt │ │
│ │ v8 │ │ (mapping)│ │ (تاریخ شمسی) │ │
│ └──────────┘ └──────────┘ └──────────────┘ │
│ │ │ │ │
│ ┌─────────────────────────────────────────┐ │
│ │ Pages / Components / Shared │ │
│ │ BasePageComponent, Hub Pages, Dialogs │ │
│ └─────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ gRPC Clients │ │ HTTP REST Services│ │
│ │ (Protobuf) │ │ (DiscountShop) │ │
│ └──────┬───────┘ └────────┬─────────┘ │
└─────────┼──────────────────────┼─────────────────┘
│ │
▼ ▼
┌──────────────────────────────────────────┐
│ CMS Microservice │
│ (ASP.NET Core + gRPC) │
│ Domain → Application (CQRS) → Infra │
└──────────────────────────────────────────┘
```
---
## ۲. Technology Stack
| لایه | تکنولوژی | نسخه |
|------|----------|------|
| Frontend Framework | Blazor WebAssembly | .NET 9 |
| UI Library | MudBlazor | v8 |
| Backend Communication (عادی) | gRPC / Protobuf | — |
| Backend Communication (تخفیفی) | HTTP REST | — |
| Object Mapping | Mapster | — |
| تاریخ شمسی | DateTimeConverterCL | — |
| Client State | Blazored.LocalStorage | — |
| Auth | JWT Role-based | Administrator, Admin, Author |
| Permission | IAuthorizationService.HasPermissionAsync | 18 permission |
---
## ۳. ساختار پوشه‌ها
```
BackOffice/src/BackOffice/
├── Common/
│ ├── BaseComponents/ ← کامپوننت‌های پایه (BasePageComponent, DateRangePicker, Image)
│ ├── Utilities/ ← RouteConstance, Extensions, Helpers
│ └── ...
├── Pages/
│ ├── Category/ ← دسته‌بندی فروشگاه عادی
│ ├── Products/ ← محصولات فروشگاه عادی
│ ├── UserOrder/ ← سفارشات + گزارش فروش (Hub)
│ ├── DiscountShop/ ← فروشگاه تخفیفی (محصولات + دسته‌بندی + سفارشات)
│ │ └── Components/ ← دیالوگ‌ها و کامپوننت‌های اختصاصی
│ ├── Inventory/ ← انبارداری (4 صفحه)
│ ├── Package/ ← پکیج‌ها
│ ├── Commission/ ← کمیسیون (5 صفحه)
│ ├── Network/ ← شبکه (4 صفحه)
│ ├── Club/ ← باشگاه مشتریان (Hub: اعضا + آمار + فیچرها)
│ ├── Blog/ ← بلاگ (Hub: پست + دسته‌بندی + تگ)
│ ├── Content/ ← صفحات سایت
│ ├── Wallet/ ← کیف‌پول (تب‌ها: لیست + تاریخچه)
│ ├── Contract/ ← قراردادها
│ ├── SystemManagement/ ← سیستم (Hub: تنظیمات + Worker + Health)
│ └── ...
├── Services/
│ ├── DiscountProduct/ ← IDiscountProductService + implementation
│ ├── DiscountCategory/ ← IDiscountCategoryService + implementation
│ ├── DiscountOrder/ ← IDiscountOrderService + implementation
│ └── Authorization/ ← IAuthorizationService
├── Shared/
│ ├── MainLayout.razor ← لایوت اصلی (AppBar + NavMenu + MudContainer)
│ ├── NavMenu.razor ← منوی ناوبری
│ ├── GlobalSearch.razor ← جستجوی سراسری
│ └── AppBreadcrumb.razor ← Breadcrumb فارسی
└── wwwroot/
├── js/main.js ← jsSaveAsFile (Excel export)
└── appsettings.json ← تنظیمات endpoints
```
---
## ۴. الگوهای اصلی
### ۴.۱ BasePageComponent — پترن صفحات لیست
**هر صفحه لیست** از `BasePageComponent` استفاده می‌کند:
```
┌──────────────────────────────────────┐
│ BasePageComponent │
│ ┌────────────────────────────────┐ │
│ │ 📋 Filter Panel (collapsible) │ │
│ │ [فیلد ۱] [فیلد ۲] [فیلد ۳] │ │
│ │ [پاک کردن فیلتر] [جستجو] │ │
│ └────────────────────────────────┘ │
│ ┌────────────────────────────────┐ │
│ │ 📊 Content (DataGrid) │ │
│ │ ToolBar: [عنوان] [Excel] [+] │ │
│ │ Columns: ... │ │
│ │ Pager: 20/50/100 │ │
│ └────────────────────────────────┘ │
└──────────────────────────────────────┘
```
**فایل:** `Common/BaseComponents/BasePageComponent.razor`
**پراپرتی‌ها:**
- `RenderFragment Filters` — محتوای فیلتر
- `RenderFragment Content` — محتوای اصلی
- `EventCallback OnSubmitClick` — کلیک جستجو
- `EventCallback OnClearFilterClick` — کلیک پاک کردن
- `bool IsFiltered` — آیا فیلتر فعال است (نشان‌دهنده badge «فعال»)
---
### ۴.۲ Hub Pages — پترن ادغام صفحات
صفحات مرتبط در یک Hub با `MudTabs` ادغام می‌شوند:
| Hub | Route‌ها | تب‌ها |
|-----|---------|-------|
| `OrdersHub` | `/OrdersPage/`, `/OrdersSalesReportsPage/` | سفارشات + گزارش فروش |
| `DiscountShopHub` | `/discount-shop`, `/discount-orders`, `/discount-sales-reports` | سفارشات + گزارش فروش |
| `ClubHub` | `/club`, `/club/members`, `/club/statistics` | اعضا + آمار |
| `BlogHub` | `/blog`, `/blog/posts`, `/blog/categories`, `/tags` | پست + دسته‌بندی + تگ |
| `SystemHub` | `/system`, `/system/configuration`, `/system/worker-control`, `/system/health` | تنظیمات + Worker + Health |
---
### ۴.۳ Code-Behind — پترن جداسازی markup/logic
```
MyPage.razor → فقط HTML/Razor markup
MyPage.razor.cs → partial class + [Inject] + methods
```
**قوانین:**
1. فایل‌هایی که سرویس inject دارند **باید** code-behind داشته باشند (محدودیت Razor source generator)
2. سرویس‌های global (`_Imports.razor`) **نباید** دوباره `[Inject]` شوند
3. `namespace` باید با مسیر فایل match کند
**سرویس‌های Global (از `_Imports.razor`):**
| سرویس | نام متغیر | توضیح |
|--------|-----------|-------|
| `IDialogService` | `DialogService` | دیالوگ MudBlazor |
| `ISnackbar` | `Snackbar` | نوتیفیکیشن MudBlazor |
| `IJSRuntime` | `jsRuntime` | ⚠️ حرف کوچک `j` |
| `NavigationManager` | `Navigation` | ناوبری |
| `ILocalStorageService` | `LocalStorageService` | ذخیره محلی |
| `AuthenticationStateProvider` | `AuthenticationStateProvider` | احراز هویت |
---
### ۴.۴ Excel Export — پترن خروجی CSV
```csharp
private async Task ExportToExcel()
{
var sb = new StringBuilder();
sb.AppendLine("ستون ۱,ستون ۲,ستون ۳"); // هدر فارسی
foreach (var item in items)
{
sb.AppendLine($"{EscapeCsv(item.Col1)},{item.Col2},{item.Col3}");
}
var bytes = Encoding.UTF8.GetPreamble() // UTF-8 BOM
.Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
var base64 = Convert.ToBase64String(bytes);
await jsRuntime.InvokeVoidAsync("jsSaveAsFile", "filename.csv", base64);
}
private string EscapeCsv(string? value)
{
if (string.IsNullOrEmpty(value)) return "";
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
return $"\"{value.Replace("\"", "\"\"")}\"";
return value;
}
```
**صفحات دارای Excel:** Products, UserOrders, ClubMembers, WithdrawalRequests, WeeklyReports, StockMovements, Users, DiscountOrders, ManualPayments, Inventory, DiscountProducts
---
### ۴.۵ Server-Side DataGrid — پترن بارگذاری صفحه‌ای
```razor
<MudDataGrid T="MyDto"
ServerData="LoadServerData"
Height="calc(100vh - 240px)"
FixedHeader="true"
Hover="true" Dense="true">
```
```csharp
private async Task<GridData<MyDto>> LoadServerData(GridState<MyDto> state)
{
var filter = new MyFilter
{
PageNumber = state.Page + 1, // MudDataGrid is 0-based
PageSize = state.PageSize
};
var (items, totalCount, _) = await MyService.GetAsync(filter);
return new GridData<MyDto> { Items = items, TotalItems = totalCount };
}
```
---
### ۴.۶ Permission System
NavMenu از `IAuthorizationService.HasPermissionAsync()` برای نمایش/مخفی کردن آیتم‌ها استفاده می‌کند:
| Permission | صفحه(ها) |
|-----------|----------|
| `dashboard.view` | داشبورد |
| `packages.manage` | پکیج‌ها |
| `products.manage` | محصولات + دسته‌بندی + ویرایش دسته‌جمعی |
| `orders.view` | سفارشات |
| `inventory.manage` | انبارداری (4 صفحه) |
| `discountshop.manage` | فروشگاه تخفیفی |
| `users.view` | کاربران |
| `roles.manage` | نقش‌ها |
| `manualpayments.create` | پرداخت دستی |
| `blog.manage` | بلاگ |
| `sitepages.manage` | صفحات سایت |
| `publicmessages.view` | پیام‌های عمومی |
| `settings.manage_configuration` | تنظیمات سیستم |
---
## ۵. مسیرهای (Routing)
### مسیرهای ثابت (`RouteConstance.cs`)
```
/ → Dashboard
/PackagePage/ → Packages
/ProductsPage/ → Products
/CategoryPage/ → Categories
/OrdersPage/ → Orders Hub
/OrdersSalesReportsPage/ → Orders Sales Reports
/InventoryPage/ → Inventory
/InventoryLowStockPage/ → Low Stock
/InventoryWarehousesPage/ → Warehouses
/InventoryMovementsPage/ → Stock Movements
/UserPage/ → Users
/RolePage/ → Roles
/ProductsBulkEditPage/ → Bulk Edit
/ProductCategoriesPage/ → Product-Category DragDrop
/CategoryProductsPage/ → Category-Product DragDrop
```
### مسیرهای hardcode (فروشگاه تخفیفی + سایر)
```
/discount-products → Discount Products
/discount-categories → Discount Categories
/discount-shop → Discount Orders Hub
/discount-orders → Discount Orders
/discount-sales-reports → Discount Sales Reports
/commission/* → Commission pages
/network/* → Network pages
/club/* → Club pages
/blog/* → Blog pages
/wallets → Wallets
/contracts → Contracts
/payment/manual-payments → Manual Payments
/system/* → System pages
/settings → Settings
/content/pages → Content Pages
/public-messages → Public Messages
```
---
## ۶. ارتباط فروشگاه عادی vs تخفیفی
| جنبه | فروشگاه عادی | فروشگاه تخفیفی |
|------|-------------|---------------|
| **سرویس محصولات** | gRPC `ProductsContractClient` | HTTP `IDiscountProductService` |
| **سرویس دسته‌بندی** | gRPC `CategoryContractClient` | HTTP `IDiscountCategoryService` |
| **سرویس سفارشات** | gRPC `UserOrderContractClient` | HTTP `IDiscountOrderService` |
| **Entity بکند** | `Product` | `DiscountProduct` |
| **پرداخت** | فقط درگاه | ترکیبی (کیف تخفیفی + درگاه) |
| **فیلد اختصاصی** | — | `MaxDiscountPercent` |
| **UI Pattern** | BasePageComponent | BasePageComponent (یکسان) |
| **ستون‌ها** | یکسان | یکسان + ستون تخفیف |
---
## ۷. نقشه NavMenu
```
داشبورد
─────────────────────
کمیسیون و شبکه
├── کمیسیون (NavGroup)
│ ├── داشبورد کمیسیون
│ ├── گزارش‌های هفتگی
│ ├── پرداخت کاربران
│ ├── درخواست‌های برداشت [Badge]
│ └── گزارش برداشت‌ها
├── شبکه (NavGroup)
│ ├── درخت شبکه
│ ├── گزارش موجودی‌ها
│ └── آمار شبکه
└── باشگاه مشتریان (NavGroup)
├── اعضا و آمار
└── فیچرهای باشگاه
─────────────────────
فروشگاه [AuthorizeView: Administrator]
├── پکیج‌ها
├── فروشگاه عادی (NavGroup)
│ ├── محصولات
│ ├── دسته‌بندی‌ها
│ └── سفارشات و گزارش
├── انبارداری (NavGroup)
│ ├── موجودی انبار
│ ├── محصولات کم‌موجود
│ ├── مدیریت انبارها
│ └── تاریخچه تغییرات
└── فروشگاه تخفیفی (NavGroup)
├── محصولات
├── دسته‌بندی‌ها
└── سفارشات و گزارش
─────────────────────
مدیریت [AuthorizeView: Administrator]
├── کاربران
├── نقش‌ها
├── پرداخت دستی
├── کیف‌پول
└── قراردادها
─────────────────────
مدیریت محتوا
├── بلاگ
├── صفحات سایت
└── پیام‌های عمومی
─────────────────────
سیستم [AuthorizeView: Administrator]
├── مدیریت سیستم
└── نسخه اپلیکیشن‌ها
─────────────────────
تنظیمات
```
@@ -0,0 +1,195 @@
# 🏪 یکسان‌سازی فروشگاه عادی و تخفیفی — BackOffice
> **تاریخ:** ۱۴۰۴/۱۱/۲۴ (February 13, 2026)
> **وضعیت:** ✅ کامل
> **Build:** 0 Error ✅
---
## ۱. هدف
فروشگاه عادی و فروشگاه تخفیفی در پنل مدیریت باید از نظر **ظاهری و UX** کاملاً یکسان باشند.
قبل از این تغییرات، صفحات فروشگاه تخفیفی ظاهر و ساختار متفاوتی داشتند. هدف این فاز:
1. **NavMenu** — جداسازی دو فروشگاه در گروه‌بندی‌های مجزا
2. **دسته‌بندی‌ها** — ظاهر یکسان با فروشگاه عادی (ستون‌ها، درخت، اکشن‌ها)
3. **محصولات** — ظاهر یکسان (گالری، فیلترها، ستون‌های گرید، اکسپورت)
4. **سفارشات** — حذف گزارش‌های کوچک اضافی، فقط لیست خالص + رفع باگ لیست خالی
---
## ۲. خلاصه تغییرات
### ۲.۱ بازسازی NavMenu
| قبل | بعد |
|-----|-----|
| یک بخش «فروشگاه» با زیرگروه‌های محصولات + دسته‌بندی + سفارش + ویرایش دسته‌جمعی | دو گروه مجزا: «فروشگاه عادی» و «فروشگاه تخفیفی» |
| ویرایش دسته‌جمعی در منو | حذف شد از منو |
| انبارداری داخل فروشگاه | انبارداری گروه مجزا |
| پکیج‌ها داخل فروشگاه | پکیج‌ها آیتم مستقل |
**ساختار جدید:**
```
فروشگاه (بخش)
├── پکیج‌ها (مستقل)
├── فروشگاه عادی (NavGroup)
│ ├── محصولات → /ProductsPage/
│ ├── دسته‌بندی‌ها → /CategoryPage/
│ └── سفارشات و گزارش → /OrdersPage/
├── انبارداری (NavGroup مستقل)
│ ├── موجودی انبار
│ ├── محصولات کم‌موجود
│ ├── مدیریت انبارها
│ └── تاریخچه تغییرات
└── فروشگاه تخفیفی (NavGroup)
├── محصولات → /discount-products
├── دسته‌بندی‌ها → /discount-categories
└── سفارشات و گزارش → /discount-orders
```
**فایل:** `Shared/NavMenu.razor`
---
### ۲.۲ رفع لیست خالی سفارشات + حذف گزارش‌های کوچک
**مشکل ۱ — لیست خالی:**
- `PaymentDate.ToDateTime()` بدون null check باعث exception در WASM می‌شد
- Exception در Blazor WASM silent است و grid خالی نشان می‌دهد
- **رفع:** اضافه کردن `@if (context.Item.PaymentDate != null)` با fallback `"-"`
**مشکل ۲ — گزارش‌های اضافی:**
- کارت‌های آماری (تعداد سفارشات + مجموع مبلغ) و نمودار Bar وضعیت ارسال بالای گرید بودند
- این آمار اضافی بود چون تب جداگانه «گزارش فروش» وجود دارد
- **رفع:** حذف کامل `MudGrid` (کارت‌ها)، `MudChart` (نمودار)، فیلدهای `_stats`/`_statusChartLabels`/`_statusChartSeries`، متد `UpdateStats()`، کلاس `OrderStatsViewModel`
- عنوان تولبار از «سفارش‌های کاربر» به «لیست سفارشات» تغییر کرد
**فایل‌ها:**
- `Pages/UserOrder/UserOrderMainPage.razor`
- `Pages/UserOrder/UserOrderMainPage.razor.cs`
---
### ۲.۳ بازنویسی صفحه محصولات تخفیفی
**قبل:** markup سفارشی بدون `BasePageComponent`، ستون‌های ساده، بدون image preview
**بعد:** کاملاً مطابق با `ProductsMainPage` فروشگاه عادی
| ویژگی | قبل | بعد |
|-------|-----|-----|
| Wrapper | markup دستی | `BasePageComponent` |
| فیلترها | جستجو + دسته‌بندی | جستجو + دسته‌بندی + وضعیت + موجودی |
| ستون عنوان | متن ساده | تصویر inline (MudAvatar) + متن truncate + tooltip |
| ستون موجودی | عدد ساده | چیپ رنگی (قرمز/نارنجی/سبز) |
| ستون وضعیت | متن | چیپ Error/Success |
| خروجی Excel | ✅ (داشت) | ✅ (حفظ شد) |
| گالری تصاویر | ✅ (داشت) | ✅ (حفظ شد) |
| Server-side paging | ✅ | ✅ |
**فایل‌ها:**
- `Pages/DiscountShop/DiscountProductsMainPage.razor` — بازنویسی کامل
- `Pages/DiscountShop/DiscountProductsMainPage.razor.cs` — بازنویسی کامل (code-behind)
---
### ۲.۴ بازنویسی صفحه دسته‌بندی‌های تخفیفی
**قبل:** markup دستی بدون `BasePageComponent`، ستون‌های متفاوت
**بعد:** کاملاً مطابق با `CategoryMainPage` فروشگاه عادی
| ویژگی | قبل | بعد |
|-------|-----|-----|
| Wrapper | markup دستی | `BasePageComponent` |
| لایوت | درخت + گرید | درخت (3 col) + گرید (9 col) — بدون تغییر |
| ستون‌ها | شناسه، عنوان، توضیحات، وضعیت | شناسه، نام لاتین، عنوان، دسته‌بندی والد، تعداد محصولات، ترتیب، فعال؟ |
| ستون والد | نداشت | resolve نام والد از لیست |
| ستون محصولات | نداشت | چیپ Info |
| ستون ترتیب | نداشت | PropertyColumn |
| فیلتر | داخل page | داخل `BasePageComponent` |
| حذف با فرزند | disabled | disabled (حفظ شد) |
**فایل‌ها:**
- `Pages/DiscountShop/DiscountCategoriesMainPage.razor` — بازنویسی کامل
- `Pages/DiscountShop/DiscountCategoriesMainPage.razor.cs` — ایجاد (code-behind جدید)
---
## ۳. فایل‌های تغییر یافته
| فایل | نوع تغییر | توضیح |
|------|----------|-------|
| `Shared/NavMenu.razor` | ✏️ ویرایش | بازسازی ساختار فروشگاه |
| `Pages/UserOrder/UserOrderMainPage.razor` | ✏️ ویرایش | حذف آمار، رفع PaymentDate |
| `Pages/UserOrder/UserOrderMainPage.razor.cs` | ✏️ ویرایش | حذف فیلدها/متدهای آمار |
| `Pages/DiscountShop/DiscountProductsMainPage.razor` | 🔄 بازنویسی | BasePageComponent + ستون‌های جدید |
| `Pages/DiscountShop/DiscountProductsMainPage.razor.cs` | 🔄 بازنویسی | code-behind کامل |
| `Pages/DiscountShop/DiscountCategoriesMainPage.razor` | 🔄 بازنویسی | BasePageComponent + ستون‌های جدید |
| `Pages/DiscountShop/DiscountCategoriesMainPage.razor.cs` | 🆕 ایجاد | code-behind جدید (از @code درون‌خطی) |
---
## ۴. الگوی پیاده‌سازی — BasePageComponent
تمام صفحات لیست در BackOffice از `BasePageComponent` استفاده می‌کنند:
```razor
<BasePageComponent @ref="_basePage" OnClearFilterClick="OnFilterCleared" OnSubmitClick="OnFilterSubmit">
<Filters>
<!-- فیلدهای فیلتر در MudItem -->
</Filters>
<Content>
<!-- MudDataGrid اصلی -->
</Content>
</BasePageComponent>
```
**در code-behind:**
```csharp
private BasePageComponent _basePage = default!;
private async Task OnFilterSubmit()
{
_basePage.IsFiltered = true;
// اعمال فیلتر
}
private async Task OnFilterCleared()
{
_basePage.IsFiltered = false;
// ریست فیلترها
}
```
---
## ۵. الگوی Code-Behind
به دلیل محدودیت Razor source generator در پروژه، **همه فایل‌هایی که سرویس inject دارند باید code-behind داشته باشند**:
```
Page.razor → فقط markup (بدون @code)
Page.razor.cs → partial class با [Inject] و منطق
```
**نکته مهم:** سرویس‌های global از `_Imports.razor` نباید دوباره با `[Inject]` تعریف شوند:
-`[Inject] public IDialogService DialogService { get; set; }` — از قبل global
-`[Inject] public ISnackbar Snackbar { get; set; }` — از قبل global
-`[Inject] public IJSRuntime jsRuntime { get; set; }` — از قبل global (حرف کوچک!)
-`[Inject] public IDiscountProductService DiscountProductService { get; set; }` — باید inject شود
---
## ۶. مقایسه نهایی فروشگاه عادی و تخفیفی
| جنبه | فروشگاه عادی | فروشگاه تخفیفی | وضعیت |
|------|-------------|---------------|-------|
| ارتباط با بکند | gRPC/Protobuf | HTTP REST (IDiscountXxxService) | تفاوت ذاتی |
| BasePageComponent | ✅ | ✅ | 🟢 یکسان |
| فیلترهای محصول | جستجو+دسته‌بندی+وضعیت | جستجو+دسته‌بندی+وضعیت+موجودی | 🟢 یکسان+ |
| ستون‌های محصول | تصویر+عنوان، قیمت، موجودی (چیپ)، وضعیت (چیپ) | تصویر+عنوان، قیمت، تخفیف، موجودی (چیپ)، وضعیت (چیپ) | 🟢 یکسان+ |
| گالری تصاویر | ✅ GalleryDialog | ✅ ProductImageGallery | 🟢 هر دو دارند |
| خروجی Excel | ✅ | ✅ | 🟢 یکسان |
| درخت دسته‌بندی | ✅ | ✅ | 🟢 یکسان |
| ستون‌های دسته‌بندی | شناسه+نام+عنوان+والد+محصولات+ترتیب+فعال | شناسه+نام+عنوان+والد+محصولات+ترتیب+فعال | 🟢 یکسان |
| سفارشات Hub | MudTabs (سفارشات + گزارش فروش) | MudTabs (سفارشات + گزارش فروش) | 🟢 یکسان |
+118
View File
@@ -0,0 +1,118 @@
# فاز ۱ — موجودیت‌های بکند CMS ✅ تکمیل شد
> **تاریخ تکمیل:** ۱۴۰۴/۰۴/۲۱ (2026-02-11)
> **وضعیت:** ✅ تکمیل — بیلد موفق + Migration ساخته شد
---
## خلاصه کارهای انجام شده
### 1.1 موجودیت‌های دامین (8 فایل)
| فایل | مسیر | توضیح |
|------|------|-------|
| `BlogPostStatus.cs` | `Domain/Enums/` | enum: Draft=0, Published=1, Scheduled=2, Archived=3 |
| `BlogPost.cs` | `Domain/Entities/Blog/` | پست بلاگ — عنوان، اسلاگ، خلاصه، محتوای HTML، تصویر، وضعیت، شمارنده بازدید |
| `BlogCategory.cs` | `Domain/Entities/Blog/` | دسته‌بندی بلاگ — عنوان، اسلاگ، آیکون، ترتیب |
| `BlogPostCategory.cs` | `Domain/Entities/Blog/` | جدول واسط پست-دسته‌بندی (Many-to-Many) |
| `BlogPostTag.cs` | `Domain/Entities/Blog/` | جدول واسط پست-تگ (از Tag موجود استفاده شد) |
| `BlogPostImage.cs` | `Domain/Entities/Blog/` | گالری تصاویر پست — مسیر، عنوان جایگزین، ترتیب |
| `SitePage.cs` | `Domain/Entities/Content/` | صفحات سایت (درباره ما، تماس با ما) — با کلید یکتا |
| `SitePageSection.cs` | `Domain/Entities/Content/` | بخش‌های هر صفحه — محتوای HTML، آیکون، تصویر، داده اضافی JSON |
### 1.2 تنظیمات Entity Framework (7 فایل)
| فایل | مسیر | ایندکس‌ها |
|------|------|----------|
| `BlogPostConfiguration.cs` | `Configurations/Blog/` | Slug (unique), Status, PublishedAt, IsFeatured, AuthorUserId, Status+PublishedAt |
| `BlogCategoryConfiguration.cs` | `Configurations/Blog/` | Slug (unique), IsActive |
| `BlogPostCategoryConfiguration.cs` | `Configurations/Blog/` | FK: BlogPostId + BlogCategoryId |
| `BlogPostTagConfiguration.cs` | `Configurations/Blog/` | FK: BlogPostId + TagId |
| `BlogPostImageConfiguration.cs` | `Configurations/Blog/` | FK: BlogPostId |
| `SitePageConfiguration.cs` | `Configurations/Content/` | PageKey (unique) |
| `SitePageSectionConfiguration.cs` | `Configurations/Content/` | SitePageId + SectionKey (compound) |
### 1.3 DbContext (2 فایل ویرایش شده)
- `IApplicationDbContext.cs` — افزودن 7 DbSet
- `ApplicationDbContext.cs` — افزودن 7 DbSet property
### 1.4 تعاریف Proto (4 فایل + csproj)
| فایل | RPCها | csharp_namespace |
|------|-------|-----------------|
| `blogpost.proto` | 11 RPC (CRUD + Publish/Archive/ViewCount + BySlug + Published/Featured) | `BlogPost` |
| `blogcategory.proto` | 6 RPC (CRUD + GetAll + GetActive) | `BlogCategory` |
| `blogpostimage.proto` | 4 RPC (Add/Delete/Get/Reorder) | `BlogPostImage` |
| `sitepage.proto` | 8 RPC (Get/GetByKey/Update/GetAll + Section CRUD + Reorder) | `SitePage` |
### 1.5 لایه CQRS Application (≈50 فایل)
#### BlogPost Commands (6 گروه، 14 فایل)
- `CreateBlogPost` — Command + Handler + Validator (با اعتبارسنجی اسلاگ regex)
- `UpdateBlogPost` — Command + Handler + Validator (الگوی delete-recreate برای دسته‌بندی/تگ)
- `DeleteBlogPost` — Command + Handler (soft-delete)
- `PublishBlogPost` — Command + Result + Handler (تنظیم Status و PublishedAt)
- `ArchiveBlogPost` — Command + Result + Handler
- `IncrementViewCount` — Command + Handler
#### BlogPost Queries (5 گروه، 10 فایل)
- `GetBlogPost` — Query + DTO + Handler (با Include chain)
- `GetBlogPostBySlug` — Query + Handler (بازاستفاده از BlogPostDto)
- `GetAllBlogPosts` — Query + ResponseDto + Handler (فیلتر + مرتب‌سازی + صفحه‌بندی)
- `GetPublishedBlogPosts` — Query + Handler (مشتری‌محور، فقط Published)
- `GetFeaturedBlogPosts` — Query + Handler (برای لندینگ پیج)
#### BlogCategory CQRS (11 فایل)
- Commands: Create + Update + Delete (با Validator)
- Queries: GetBlogCategory + GetAllBlogCategories + GetActiveBlogCategories
#### BlogPostImage CQRS (8 فایل)
- Commands: Add + Delete + Reorder (با ImageSortItem)
- Queries: GetBlogPostImages
#### SitePage CQRS (14 فایل)
- Commands: UpdateSitePage + CreateSection + UpdateSection + DeleteSection + ReorderSections
- Queries: GetSitePage + GetSitePageByKey + GetAllSitePages
### 1.6 سرویس‌های gRPC WebApi (4 فایل)
| سرویس | الگو | توضیح |
|-------|------|-------|
| `BlogPostService.cs` | ترکیبی (دستی + dispatcher) | مپینگ دستی برای لیست‌ها و RepeatedField |
| `BlogCategoryService.cs` | ترکیبی | dispatcher برای CRUD ساده، دستی برای لیست‌ها |
| `BlogPostImageService.cs` | ترکیبی | dispatcher + مپینگ دستی Reorder |
| `SitePageService.cs` | ترکیبی | dispatcher + مپینگ دستی Sections |
### 1.7 Mapping Profiles (2 فایل)
- `BlogPostProfile.cs` — مپینگ PublishBlogPostResult و ArchiveBlogPostResult
- `BlogCategoryProfile.cs` — مپینگ long → CreateBlogCategoryResponse
### 1.8 EF Migration
- `20260210232742_AddBlogAndContentEntities.cs` — ایجاد 7 جدول جدید
- **Build:** ✅ موفق (0 Error, warnings مربوط به کد قدیمی)
---
## آمار فاز ۱
| متریک | تعداد |
|-------|-------|
| فایل‌های جدید | ~65 |
| فایل‌های ویرایش شده | ~4 |
| موجودیت‌های دامین | 7 (+1 enum) |
| تنظیمات EF | 7 |
| تعاریف Proto | 4 |
| RPCهای gRPC | 29 |
| Commands CQRS | 16 |
| Queries CQRS | 12 |
| سرویس‌های WebApi | 4 |
| جداول دیتابیس جدید | 7 |
---
## فاز بعدی
**فاز ۲ — پنل مدیریت بلاگ (BackOffice)** — صفحات Blazor WASM برای مدیریت پست‌ها، دسته‌بندی‌ها، تصاویر و صفحات سایت.
+104
View File
@@ -0,0 +1,104 @@
# فاز ۳: صفحات محتوای پویا (Dynamic Content Pages) ✅
## 📋 خلاصه
تبدیل صفحات **درباره ما** و **تماس با ما** از محتوای هاردکد (hardcoded) به محتوای پویا که از CMS (سرویس SitePage) بارگذاری می‌شود، با پشتیبانی fallback به محتوای پیش‌فرض.
---
## 🏗️ معماری
```
FrontOffice (Blazor Server)
├── About.razor/cs ─── SitePageService ──► gRPC ──► CMS SitePageContract
└── Contact.razor/cs ─── SitePageService ──► gRPC ──► CMS SitePageContract
```
### الگوی Fallback:
```
OnInitializedAsync() → SitePageService.GetByKeyAsync("about")
├── ✅ Data received → Render dynamic content
└── ❌ Error/null → Render hardcoded fallback content
```
---
## 📁 فایل‌های ایجاد/تغییر یافته
### فایل‌های جدید:
| فایل | توضیحات |
|------|---------|
| `FrontOffice/src/FrontOffice.Main/Utilities/SitePageService.cs` | سرویس SitePage + DTOs (SitePageDto, SitePageSectionDto) |
| `dbbkup/SeedSitePages.sql` | اسکریپت Seed Data برای درج محتوای اولیه صفحات |
### فایل‌های تغییر یافته:
| فایل | تغییرات |
|------|---------|
| `FrontOffice/src/FrontOffice.Main/ConfigureServices.cs` | اضافه شدن SitePageService + SitePageContractClient به DI |
| `FrontOffice/src/FrontOffice.Main/Pages/About.razor` | تبدیل به محتوای پویا با fallback |
| `FrontOffice/src/FrontOffice.Main/Pages/About.razor.cs` | اضافه شدن OnInitializedAsync + بارگذاری sections |
| `FrontOffice/src/FrontOffice.Main/Pages/Contact.razor` | تبدیل hero/info/social به پویا، فرم بدون تغییر |
| `FrontOffice/src/FrontOffice.Main/Pages/Contact.razor.cs` | اضافه شدن OnInitializedAsync + ExtraData DTOs |
---
## 🔧 جزئیات فنی
### SitePageService
```csharp
public class SitePageService
{
Task<SitePageDto?> GetByKeyAsync(string pageKey) // "about" | "contact"
}
```
### SitePageDto Helpers
```csharp
GetSection(string sectionKey) // e.g. "vision", "mission", "contact-info"
GetSections(string prefix) // e.g. "value-" → value-1, value-2, ...
```
### SitePageSectionDto.GetExtraData<T>()
JSON deserializer برای فیلد ExtraData — استفاده شده در Contact:
- `ContactInfoData`: address, phone, email, hours
- `SocialMediaData`: telegram, instagram, linkedin, whatsapp
---
## 📄 SectionKey Mapping
### صفحه درباره ما (PageKey: `about`)
| SectionKey | کاربرد | فیلدهای اصلی |
|------------|--------|--------------|
| `vision` | کارت چشم‌انداز | Title, HtmlContent, IconName |
| `mission` | کارت مأموریت | Title, HtmlContent, IconName |
| `value-1` ... `value-6` | کارت‌های ارزش‌ها | Title, HtmlContent, IconName |
| `team-1` ... `team-3` | کارت‌های اعضای تیم | Title(نام), Subtitle(سمت), HtmlContent(توضیحات), ImagePath(آواتار) |
### صفحه تماس با ما (PageKey: `contact`)
| SectionKey | کاربرد | فیلدهای اصلی |
|------------|--------|--------------|
| `contact-info` | اطلاعات تماس | ExtraData → `{address, phone, email, hours}` |
| `social-media` | شبکه‌های اجتماعی | ExtraData → `{telegram, instagram, linkedin, whatsapp}` |
---
## 🗃️ Seed Data
فایل `dbbkup/SeedSitePages.sql` شامل:
- **2 صفحه**: about, contact
- **13 سکشن**: 2 (vision/mission) + 6 (values) + 3 (team) + 2 (contact-info/social-media)
- تمام محتوای فعلی hardcoded به عنوان داده اولیه درج شده
---
## ✅ بیلد
```
FrontOffice.Main: 0 Error(s), Build succeeded
```
---
## 📌 نکات مهم
1. **فرم تماس** (Contact Form) بدون تغییر باقی ماند — منطق سمت کلاینت است نه محتوای CMS
2. **Fallback**: اگر CMS در دسترس نباشد، محتوای hardcoded نمایش داده می‌شود
3. **Loading State**: صفحه About دارای حالت loading با spinner
4. آیکون‌ها در CMS به صورت string ذخیره می‌شوند (مثل `@Icons.Material.Filled.Security`)
File diff suppressed because it is too large Load Diff