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
+39 -3
View File
@@ -1,10 +1,32 @@
# 📚 FourSat Documentation Index
> آخرین بروزرسانی: February 10, 2026
> آخرین بروزرسانی: February 17, 2026
> ۲۲۰ فایل → ۳۰ فایل (تجمیع ۳ فازی + cleanup نهایی)
---
## 🔍 راهنمای سریع — کدام مستند را باید ببینم؟
| می‌خواهم بدانم... | مستند |
|-------------------|-------|
| **کل تغییرات BackOffice چه بوده؟** | [`BackOffice/docs/BACKOFFICE-CHANGELOG.md`](../BackOffice/docs/BACKOFFICE-CHANGELOG.md) |
| **ساختار و معماری BackOffice چیست؟** | [`ui-modernization/BACKOFFICE-ARCHITECTURE.md`](ui-modernization/BACKOFFICE-ARCHITECTURE.md) |
| **یکسان‌سازی فروشگاه‌ها چه بوده؟** | [`ui-modernization/BACKOFFICE-STORE-UNIFICATION.md`](ui-modernization/BACKOFFICE-STORE-UNIFICATION.md) |
| **وضعیت فروشگاه تخفیفی؟** | [`business/DISCOUNT-STORE-STATUS.md`](business/DISCOUNT-STORE-STATUS.md) |
| **بیزینس فروشگاه تخفیفی چگونه کار می‌کند؟** | [`business/discount-shop-business.md`](business/discount-shop-business.md) |
| **سیستم کمیسیون چگونه کار می‌کند؟** | [`business/club-commission-system-complete.md`](business/club-commission-system-complete.md) |
| **چگونه deploy کنم؟** | [`deployment/OFFLINE-DEPLOYMENT-GUIDE.md`](deployment/OFFLINE-DEPLOYMENT-GUIDE.md) |
| **وضعیت CI/CD چیست؟** | [`deployment/CICD-PIPELINE-GUIDE.md`](deployment/CICD-PIPELINE-GUIDE.md) |
| **مشخصات سرور و زیرساخت؟** | [`deployment/INFRASTRUCTURE-GUIDE.md`](deployment/INFRASTRUCTURE-GUIDE.md) |
| **مهاجرت BFF→CMS چگونه بوده؟** | [`migration/BACKOFFICE-BFF-MIGRATION.md`](migration/BACKOFFICE-BFF-MIGRATION.md) |
| **مهاجرت FrontOffice→CMS؟** | [`migration/FRONTOFFICE-TO-CMS-MIGRATION.md`](migration/FRONTOFFICE-TO-CMS-MIGRATION.md) |
| **نقشه نوسازی UI فرانت؟** | [`ui-modernization/UI-MODERNIZATION-PLAN.md`](ui-modernization/UI-MODERNIZATION-PLAN.md) |
| **معماری مدیریت فایل و تصاویر؟** | [`cms/FILE-MANAGEMENT-ARCHITECTURE.md`](cms/FILE-MANAGEMENT-ARCHITECTURE.md) |
| **فیکس فلوی ثبت‌نام FrontOffice؟** | [`cms/REGISTRATION-FLOW-FIXES.md`](cms/REGISTRATION-FLOW-FIXES.md) |
| **Audit report کامل BackOffice؟** | [`BackOffice/docs/BACKOFFICE-AUDIT.md`](../BackOffice/docs/BACKOFFICE-AUDIT.md) |
---
## 📂 business/ — مستندات بیزنسی (۷ فایل)
| فایل | توضیح |
@@ -17,7 +39,7 @@
| [discount-shop-business.md](business/discount-shop-business.md) | فروشگاه تخفیفی: پرداخت ترکیبی، درصد تخفیف، entity design |
| [manual-payment-system.md](business/manual-payment-system.md) | پرداخت دستی: کارت به کارت، تأیید ادمین، آپلود FMS |
## 📂 cms/ — مستندات فنی CMS (۱۳ فایل)
## 📂 cms/ — مستندات فنی CMS (۱۵ فایل)
| فایل | توضیح |
|------|-------|
@@ -35,6 +57,8 @@
| [email-sms-configuration.md](cms/email-sms-configuration.md) | تنظیمات SMS/Email: Kavenegar templates، Gmail |
| [PRODUCT-BUNDLE-FEATURE.md](cms/PRODUCT-BUNDLE-FEATURE.md) | 🟡 فیچر آینده: طراحی Product Bundle |
| [FRONTOFFICE-RELEASE-NOTES-v1.5.0.md](cms/FRONTOFFICE-RELEASE-NOTES-v1.5.0.md) | 🆕 یادداشت انتشار FrontOffice v1.5.0 (فارسی): هفته‌نما، گزارش هفتگی، امتیاز انتقالی |
| [FILE-MANAGEMENT-ARCHITECTURE.md](cms/FILE-MANAGEMENT-ARCHITECTURE.md) | 🆕 معماری جامع مدیریت فایل: IFileManager, LocalFileManager, ImagePathResolverInterceptor, UploadsController (HTTP سرو عمومی + FMS Fallback), ذخیره دیسکی |
| [REGISTRATION-FLOW-FIXES.md](cms/REGISTRATION-FLOW-FIXES.md) | 🆕 فیکس فلوی ثبت‌نام: ایجاد کاربر جدید در VerifyOtpToken، رفع lookup موبایل AcceptContract، رفع sync IsCompleteRegister |
## 📂 deployment/ — مستندات استقرار (۴ فایل)
@@ -46,6 +70,14 @@
| [SERVER-MIRRORS-CONFIG.md](deployment/SERVER-MIRRORS-CONFIG.md) | تنظیمات mirror: K3s registries.yaml، containerd |
| [INGRESS-NGINX-WARNING.md](deployment/INGRESS-NGINX-WARNING.md) | ⚠️ هشدار K3s: مشکل hostNetwork در ingress-nginx |
## 📂 ui-modernization/ — مستندات نوسازی UI (۳ فایل)
| فایل | توضیح |
|------|-------|
| [UI-MODERNIZATION-PLAN.md](ui-modernization/UI-MODERNIZATION-PLAN.md) | 🆕 طرح جامع نوسازی UI فرانت‌آفیس: سیستم بلاگ، صفحات دینامیک، لندینگ، Mobile-First — ۷ فاز، ~۱۲۴ فایل جدید |
| [BACKOFFICE-ARCHITECTURE.md](ui-modernization/BACKOFFICE-ARCHITECTURE.md) | 🆕 مرجع معماری BackOffice: ساختار پوشه‌ها، الگوهای BasePageComponent/Hub/CodeBehind/ExcelExport، مسیرها، permission‌ها، نقشه NavMenu |
| [BACKOFFICE-STORE-UNIFICATION.md](ui-modernization/BACKOFFICE-STORE-UNIFICATION.md) | 🆕 یکسان‌سازی فروشگاه عادی و تخفیفی: NavMenu restructure، حذف آمار سفارشات، رفع PaymentDate، بازنویسی ۴ صفحه |
## 📂 migration/ — مستندات مهاجرت BFF→CMS (۶ فایل)
| فایل | توضیح |
@@ -69,4 +101,8 @@
| فاز ۳ (ساختاردهی + merge) | **28** | ۱۳ |
| cleanup نهایی (+2 فایل جدید) | **30** | — |
| session CI/CD + Admin fix (+2) | **32** | — |
| **نهایی** | **32 + INDEX** | **۱۹۱ فایل حذف/ادغام** |
| session UI Modernization plan (+1) | **33** | — |
| session Store Unification (+2 docs) | **35** | — |
| session File Mgmt + Content (+1 doc) | **36** | — |
| session Registration Flow Fix (+1 doc) | **37** | — |
| **نهایی** | **37 + INDEX** | **۱۹۱ فایل حذف/ادغام** |
+521
View File
@@ -0,0 +1,521 @@
# یکسان‌سازی فروشگاه عادی و فروشگاه تخفیفی (BackOffice)
**تاریخ:** ۱۳۹۴/۱۱/۲۴ (2026-02-13)
**وضعیت:** ✅ فاز ۱ تا ۶ — تکمیل شده (CMS + BackOffice Build Succeeded — 0 Error)
---
## هدف
هر دو فروشگاه (عادی و تخفیفی) از نظر **UI/UX، ساختار صفحات، اکشن‌ها و قابلیت‌ها** عین‌به‌عین یکسان باشند.
**تنها تفاوت مجاز:** منطق پرداخت — فروشگاه تخفیفی از کیف‌پول تخفیفی + درگاه، فروشگاه عادی فقط از کیف‌پول عادی.
### معیار یکسان‌سازی
- **مبنا:** فروشگاه عادی (Products, Category, UserOrder)
- **استثنا:** مزایای بدیهی فروشگاه تخفیفی به فروشگاه عادی هم اضافه شد
- **Bulk Operations:** بیخیال شد (طبق درخواست کاربر)
---
## تفاوت‌های زیرساختی (تغییر نکرده — بی‌تأثیر روی UX)
| موضوع | فروشگاه عادی | فروشگاه تخفیفی |
|---|---|---|
| ارتباط با سرور | gRPC مستقیم (`ProductsContractClient`) | سرویس اینترفیس (`IDiscountProductService`) که داخلاً gRPC صدا می‌زنه |
| مدل داده | Protobuf models | C# DTOs |
> **نکته:** هر دو در نهایت از همان gRPC backend استفاده می‌کنند. تفاوت فقط در لایه abstraction است و تأثیری روی UX ندارد.
---
## تغییرات انجام‌شده
### ۱. صفحه محصولات (`DiscountProductsMainPage`)
| تغییر | قبل | بعد |
|---|---|---|
| نمایش تصویر | `MudAvatar` | کامپوننت `Image` (مطابق فروشگاه عادی) |
| برش عنوان | `Substring(0, 20) + "…"` | `Truncate(20, true)` (extension method مشترک) |
| گالری تصاویر | `ProductFormDialog` (کلاینت‌ساید) | `GalleryDialog` سرور-محور (مطابق فروشگاه عادی) |
| پیش‌نمایش تصویر | HTML inline در `ShowMessageBox` | `ImagePreviewDialog` (مطابق فروشگاه عادی) |
| عنوان تولبار | «مدیریت محصولات تخفیفی» | «مدیریت محصولات» |
**اکشن‌های جدید اضافه‌شده:**
- ✅ دکمه «مدیریت دسته‌بندی (درگ و دراپ)» → ناوبری به `ProductCategoriesDragDropPage`
**فایل‌های تغییر یافته:**
- `Pages/DiscountShop/DiscountProductsMainPage.razor`
- `Pages/DiscountShop/DiscountProductsMainPage.razor.cs`
---
### ۲. صفحه دسته‌بندی‌ها (`DiscountCategoriesMainPage`)
**اکشن‌های جدید اضافه‌شده:**
- ✅ دکمه «مدیریت محصولات این دسته (درگ و دراپ)» → ناوبری به `CategoryProductsDragDropPage`
**فایل‌های تغییر یافته:**
- `Pages/DiscountShop/DiscountCategoriesMainPage.razor`
- `Pages/DiscountShop/DiscountCategoriesMainPage.razor.cs`
---
### ۳. صفحه سفارشات (`DiscountOrdersMainPage`) — بازنویسی کامل
| تغییر | قبل | بعد |
|---|---|---|
| الگوی فیلتر | فیلترهای inline با دکمه جستجو | `BasePageComponent` با OnSubmit/OnClear (مطابق فروشگاه عادی) |
| لایه‌بندی | `MudPaper` تو در تو | `BasePageComponent > Filters + Content` |
| اکشن‌ها | فقط مشاهده جزئیات + تغییر وضعیت | جزئیات + تغییر وضعیت + حذف (مطابق فروشگاه عادی) |
| آیکون‌های اکشن | `Visibility` + `Edit` | `Info` + `LocalShipping` + `DeleteOutline` (مطابق فروشگاه عادی) |
| ساختار تولبار | بدون تولبار | تولبار با عنوان + دکمه Excel (مطابق فروشگاه عادی) |
**ستون‌ها (حفظ شده — خاص فروشگاه تخفیفی):**
- شماره سفارش، تاریخ ثبت، مبلغ کل، **تخفیف کیف‌پول**، **پرداخت درگاه**، تعداد آیتم، وضعیت، پرداخت
**فایل‌های تغییر یافته:**
- `Pages/DiscountShop/DiscountOrdersMainPage.razor` ← بازنویسی کامل
- `Pages/DiscountShop/DiscountOrdersMainPage.razor.cs` ← بازنویسی کامل
---
### ۴. گزارش فروش (`SalesReports`)
| تغییر | قبل | بعد |
|---|---|---|
| نمودارها | روند فروش + محصولات پرفروش | روند فروش + محصولات پرفروش + **وضعیت سفارش‌ها** |
| عنوان | «گزارش فروش فروشگاه تخفیفی» | «گزارش فروش فروشگاه» |
| توضیح | «آمار فروش، تخفیف و وضعیت سفارش‌های فروشگاه تخفیفی...» | «آمار فروش و وضعیت سفارش‌ها بر اساس بازه تاریخ و وضعیت سفارش» |
**نمودار جدید:**
- ✅ «وضعیت سفارش‌ها» — نمودار میله‌ای تعداد سفارش بر اساس وضعیت (مطابق فروشگاه عادی)
**فایل‌های تغییر یافته:**
- `Pages/DiscountShop/SalesReports.razor`
---
### ۵. هاب سفارشات (`DiscountShopHub`)
| تغییر | قبل | بعد |
|---|---|---|
| عنوان | «فروشگاه تخفیفی» | «سفارشات فروشگاه تخفیفی» |
| آیکون تب سفارشات | `ShoppingCart` | `ReceiptLong` (مطابق `OrdersHub` فروشگاه عادی) |
**فایل‌های تغییر یافته:**
- `Pages/DiscountShop/DiscountShopHub.razor`
---
### ۶. تغییرات فروشگاه عادی (مزایای تخفیفی ← عادی)
#### CategoryMainPage
- ✅ ستون **«ترتیب»** (`SortOrder`) اضافه شد (از تخفیفی)
-**غیرفعال‌سازی حذف** دسته‌بندی دارای زیردسته (از تخفیفی)
**فایل‌های تغییر یافته:**
- `Pages/Category/CategoryMainPage.razor`
- `Pages/Category/CategoryMainPage.razor.cs`
---
## تغییرات فاز ۲ — ارتقاء به Admin RPC و فیلدهای پیشرفته
### ۷. سرویس سفارشات تخفیفی — سوئیچ به Admin RPC
| تغییر | قبل | بعد |
|---|---|---|
| RPC مورد استفاده | `GetUserOrders` (user-scoped) | `GetAllDiscountOrders` (admin-scoped) |
| DTO | `OrderSummaryDto` (محدود) | `AdminOrderDto` (کامل با user_full_name, user_mobile, shipping_address, payment_date, vat_amount...) |
| فیلترها | فقط userId, paymentCompleted, deliveryStatus | userId, paymentStatus, deliveryStatus, userMobile, trackingCode, fromDate, toDate, minAmount, maxAmount |
| گزارش فروش | محاسبه کلاینت‌ساید + N+1 (۵۰ فراخوان gRPC جداگانه!) | `GetDiscountSalesReport` RPC سرور-ساید با fallback |
**فایل‌های تغییر یافته:**
- `Services/DiscountOrder/IDiscountOrderService.cs` ← فیلترهای جدید + DTOهای گزارش فروش
- `Services/DiscountOrder/DiscountOrderService.cs` ← سوئیچ به `GetAllDiscountOrdersAsync` + `GetDiscountSalesReportAsync`
### ۸. ستون‌ها و فیلترهای ادمین در سفارشات تخفیفی
**ستون‌های جدید اضافه‌شده:**
-**نام کاربر** (لینک به پروفایل — مطابق فروشگاه عادی)
-**موبایل کاربر**
-**وضعیت پرداخت** (Pending/Completed/Failed/Refunded — مطابق فروشگاه عادی)
-**تاریخ پرداخت**
-**آدرس** (truncated با tooltip — مطابق فروشگاه عادی)
-**وضعیت ارسال** (جدا از وضعیت پرداخت — مطابق فروشگاه عادی)
**فیلترهای جدید اضافه‌شده:**
- ✅ شناسه سفارش
- ✅ جستجوی کاربر (UserAutoComplete)
- ✅ موبایل کاربر
- ✅ کد رهگیری
- ✅ از تاریخ / تا تاریخ
- ✅ وضعیت پرداخت (Pending/Completed/Failed/Refunded)
- ✅ وضعیت ارسال
**فایل‌های تغییر یافته:**
- `Pages/DiscountShop/DiscountOrdersMainPage.razor` ← ستون‌ها + فیلترها
- `Pages/DiscountShop/DiscountOrdersMainPage.razor.cs` ← فیلدها + متدهای وضعیت پرداخت
### ۹. گزارش فروش تخفیفی — حذف مشکل N+1
| تغییر | قبل | بعد |
|---|---|---|
| محصولات پرفروش | ۵۰ فراخوان gRPC جداگانه (`GetByIdAsync` × 50) | یک فراخوان `GetDiscountSalesReport` |
| خلاصه آماری | محاسبه کلاینت‌ساید | سرور-ساید (دقیق‌تر + سریع‌تر) |
| نمودار روند | GroupBy کلاینت‌ساید | `SalesPeriodDto` از سرور |
| جدول سفارش‌ها | فقط شماره سفارش | شناسه + نام کاربر (لینک) |
| خروجی Excel/PDF | فقط OrderNumber | شناسه + نام کاربر + موبایل |
**فایل‌های تغییر یافته:**
- `Pages/DiscountShop/SalesReports.razor`
### ۱۰. دسته‌بندی فروشگاه عادی — فیلد ImagePath
- ✅ فیلد `ImagePath` به دیالوگ ایجاد/ویرایش دسته‌بندی اضافه شد (مطابق فروشگاه تخفیفی)
**فایل‌های تغییر یافته:**
- `Pages/Category/Components/CreateOrUpdateCategoryDialog.razor`
- `Pages/Category/Components/CreateOrUpdateCategoryDialog.razor.cs`
### ۱۱. گزارش فروش عادی — نمودار محصولات پرفروش + خروجی PDF
- ✅ نمودار «محصولات پرفروش (بر اساس مبلغ)» از FactorDetails سفارشات
- ✅ دکمه خروجی PDF (نسخه متنی) — مطابق فروشگاه تخفیفی
**فایل‌های تغییر یافته:**
- `Pages/UserOrder/OrderSalesReports.razor`
- `Pages/UserOrder/OrderSalesReports.razor.cs`
---
## تغییرات فاز ۳ — تگ‌های محصول و نهایی‌سازی
### ۱۲. تگ‌های محصول در فروشگاه تخفیفی
- ✅ دکمه «تگ‌های محصول» (`Label` icon) به ستون عملیات صفحه محصولات تخفیفی اضافه شد
- ✅ از همان `AssignTagsDialog` فروشگاه عادی استفاده شد (کامپوننت مشترک)
- ✅ سرویس `ProductTagContract` مشترک بین هر دو فروشگاه — بدون نیاز به API جدید
> **نکته:** `ProductTagContract` یک سرویس ژنریک `product_id ↔ tag_id` است و محدود به فروشگاه خاصی نیست.
**فایل‌های تغییر یافته:**
- `Pages/DiscountShop/DiscountProductsMainPage.razor` ← دکمه تگ
- `Pages/DiscountShop/DiscountProductsMainPage.razor.cs` ← متد `OpenTagAssignment` + using
---
## تغییرات فاز ۴ — یکسان‌سازی دیالوگ‌ها و جزئیات تکمیلی
### ۱۳. دیالوگ جزئیات سفارش تخفیفی — Timeline + ویرایش Inline
| تغییر | قبل | بعد |
|---|---|---|
| Timeline وضعیت | ❌ فاقد | ✅ ۵ مرحله (ثبت → پرداخت → آماده‌سازی → ارسال → تحویل/مرجوعی) |
| ویرایش وضعیت | ❌ فقط خواندنی | ✅ درون‌خطی (وضعیت + کد رهگیری + یادداشت ادمین) |
| دکمه ذخیره | ❌ فقط «بستن» | ✅ «ثبت تغییرات» + اسپینر بارگذاری |
| هشدارهای شرطی | ❌ فاقد | ✅ هشدار لغو/مرجوعی + اطلاع‌رسانی ارسال |
| Code-behind | `@code` درون‌خطی | فایل جداگانه `.razor.cs` |
**فایل‌های تغییر یافته:**
- `Pages/DiscountShop/Components/OrderDetailsDialog.razor` ← Timeline + ویرایش inline
- `Pages/DiscountShop/Components/OrderDetailsDialog.razor.cs` ← فایل جدید — timeline + save logic
### ۱۴. دیالوگ تغییر وضعیت فروشگاه عادی — ارتقاء
| تغییر | قبل | بعد |
|---|---|---|
| فرم | `MudStack` ساده | `MudForm` با validation |
| فیلدها | فقط وضعیت | ✅ وضعیت + **کد رهگیری** + **توضیحات ارسال** |
| هشدارهای شرطی | ❌ فاقد | ✅ هشدار مرجوعی + اطلاع‌رسانی ارسال |
| دکمه | رنگ ثابت `Primary` | ✅ رنگ داینامیک بر اساس وضعیت + اسپینر |
| عنوان | بدون آیکون | ✅ آیکون `Edit` + عنوان |
| RPC | فقط `UpdateOrderStatusAsync` | ✅ `UpdateOrderStatusAsync` + `UpdateUserOrderAsync` (کد رهگیری + توضیحات) |
**فایل‌های تغییر یافته:**
- `Pages/UserOrder/Components/ChangeOrderStatusDialog.razor`
- `Pages/UserOrder/Components/ChangeOrderStatusDialog.razor.cs`
### ۱۵. گزارش فروش تخفیفی — Empty chart guard + ترتیب نمودارها
| تغییر | قبل | بعد |
|---|---|---|
| نمودار روند فروش | بدون بررسی خالی بودن | ✅ نمایش «داده کافی برای نمایش نمودار وجود ندارد» |
| ترتیب نمودارها | روند → پرفروش → وضعیت | ✅ روند → **وضعیت** → پرفروش (مطابق فروشگاه عادی) |
**فایل‌های تغییر یافته:**
- `Pages/DiscountShop/SalesReports.razor`
### ۱۶. گزارش فروش عادی — لینک نام کاربر
- ✅ نام کاربر در جدول سفارش‌ها از متن ساده به `MudLink` تبدیل شد (لینک به پروفایل کاربر)
**فایل‌های تغییر یافته:**
- `Pages/UserOrder/OrderSalesReports.razor`
### ۱۷. دیالوگ دسته‌بندی فروشگاه عادی — MudForm + UX
| تغییر | قبل | بعد |
|---|---|---|
| اعتبارسنجی | ❌ بدون validation | ✅ `MudForm` با `Required` + `RequiredError` |
| فیلد فعال | `MudCheckBox` | ✅ `MudSwitch` رنگی (مطابق فروشگاه تخفیفی) |
| Helper text | ❌ فاقد | ✅ «عدد کمتر = اولویت بالاتر» + «برای دسته اصلی خالی بگذارید» |
| دکمه ذخیره | همیشه «ثبت» | ✅ «ذخیره تغییرات» / «ایجاد دسته‌بندی» (داینامیک) |
| اسپینر بارگذاری | ❌ فاقد | ✅ `MudProgressCircular` + غیرفعال‌سازی دکمه |
| عنوان | بدون آیکون | ✅ آیکون `Edit`/`Add` + عنوان داینامیک |
| غیرفعال‌سازی دکمه | ❌ فاقد | ✅ غیرفعال تا validation سبز نشود |
**فایل‌های تغییر یافته:**
- `Pages/Category/CreateOrUpdateCategoryDialog.razor`
- `Pages/Category/CreateOrUpdateCategoryDialog.razor.cs`
### ۱۸. سفارشات تخفیفی — لغو سفارش واقعی
| تغییر | قبل | بعد |
|---|---|---|
| دکمه «حذف» | آیکون `DeleteOutline` — فقط snackbar (بدون API) | ✅ آیکون `Cancel` — فراخوان `UpdateStatusAsync` با `Cancelled` |
| متن تأییدیه | «آیا از حذف مطمئن هستید؟» | «آیا از لغو سفارش مطمئن هستید؟ وضعیت به لغو شده تغییر می‌کند» |
| عملکرد | ❌ هیچ | ✅ واقعی — `UpdateStatusAsync(Cancelled)` |
**فایل‌های تغییر یافته:**
- `Pages/DiscountShop/DiscountOrdersMainPage.razor` ← آیکون + tooltip
- `Pages/DiscountShop/DiscountOrdersMainPage.razor.cs` ← فراخوان API
---
## تغییرات فاز ۵ — فرمت‌بندی قیمت، زیردسته و برچسب فیلدها
### ۵.۱ فرمت‌بندی قیمت در لیست محصولات عادی
**مشکل:** ستون قیمت عدد خام بدون separator و بدون واحد نشان می‌داد.
**راه‌حل:** تبدیل `PropertyColumn` به `TemplateColumn` با `Price.ToString("N0") ریال` — مطابق فروشگاه تخفیفی.
**فایل تغییر یافته:**
- `Pages/Products/ProductsMainPage.razor` ← TemplateColumn + فرمت N0 + ریال
### ۵.۲ دکمه «افزودن زیردسته» در دسته‌بندی‌های عادی
**مشکل:** فروشگاه تخفیفی دکمه «افزودن زیردسته» در ستون عملیات داشت ولی فروشگاه عادی نداشت.
**راه‌حل:** افزودن `MudIconButton` با آیکون `CreateNewFolder` + رنگ `Color.Primary` + متد `CreateSubcategory(parent)` که دیالوگ را با `ParentId = parent.Id` باز می‌کند.
**فایل‌های تغییر یافته:**
- `Pages/Category/CategoryMainPage.razor` ← دکمه جدید در ستون عملیات
- `Pages/Category/CategoryMainPage.razor.cs` ← متد `CreateSubcategory`
### ۵.۳ برچسب واحد ریال در دیالوگ‌های محصول
**مشکل:** فیلد قیمت در دیالوگ‌های ایجاد/ویرایش محصول عادی `Label="قیمت"` داشت — بدون واحد.
**راه‌حل:** تغییر به `Label="قیمت (ریال)"` — مطابق فروشگاه تخفیفی.
**فایل‌های تغییر یافته:**
- `Pages/Products/Components/CreateDialog.razor` ← قیمت (ریال)
- `Pages/Products/Components/UpdateDialog.razor` ← قیمت (ریال)
---
## تغییرات فاز ۶ — تغییرات بکند (Proto + Handler + gRPC Service)
این فاز تمام موارد "محدودیت API" که در فازهای قبلی شناسایی شده بودند را حل می‌کند.
### ۶.۱ ستون وضعیت فعال/غیرفعال محصولات (`is_active`)
**مشکل:** پروتوباف `GetAllProductsByFilterResponseModel` فیلد `is_active` نداشت. محصولات عادی ستون وضعیت نداشتند.
**راه‌حل (end-to-end):**
- **Proto:** افزودن `bool is_active = 15` به `GetAllProductsByFilterResponseModel` + `google.protobuf.BoolValue is_active = 15` به `GetAllProductsByFilterFilter`
- **DTO:** افزودن `bool IsActive` به `CustomerProductModel`
- **Query:** افزودن `bool? IsActive` به `GetCustomerProductsByFilterQuery`
- **Handler:** فیلتر `IsDeleted != IsActive` + مپ `IsActive = !p.IsDeleted`
- **gRPC Service:** مپ `IsActive` در فیلتر و ریسپانس `ProductsService`
- **Frontend:** ستون `TemplateColumn` با `MudChip` رنگی در `ProductsMainPage.razor`
**فایل‌های تغییر یافته:**
- `CMS/.../Protos/products.proto`
- `CMS/.../GetCustomerProductsByFilterResponseDto.cs`
- `CMS/.../GetCustomerProductsByFilterQuery.cs`
- `CMS/.../GetCustomerProductsByFilterQueryHandler.cs`
- `CMS/.../Services/ProductsService.cs`
- `BackOffice/.../Pages/Products/ProductsMainPage.razor`
### ۶.۲ ستون تعداد محصولات دسته‌بندی (`product_count`)
**مشکل:** پروتوباف `GetAllCategoryByFilterResponseModel` فیلد `product_count` نداشت.
**راه‌حل (end-to-end):**
- **Proto:** افزودن `int32 product_count = 9` به `GetAllCategoryByFilterResponseModel`
- **DTO:** افزودن `int ProductCount` به `GetAllCategoryByFilterResponseModel` (C#)
- **Handler:** تغییر از `ProjectToType<>()` (Mapster auto-map) به manual `Select()` با `ProductCount = x.ProductCategories.Count`
- **gRPC Service:** auto-map Mapster (نام یکسان)
- **Frontend:** ستون `PropertyColumn` جدید در `CategoryMainPage.razor`
**فایل‌های تغییر یافته:**
- `CMS/.../Protos/category.proto`
- `CMS/.../GetAllCategoryByFilterResponseDto.cs`
- `CMS/.../GetAllCategoryByFilterQueryHandler.cs`
- `BackOffice/.../Pages/Category/CategoryMainPage.razor`
### ۶.۳ فیلتر بازه تاریخ سفارشات (`from_date` / `to_date`)
**مشکل:** پروتوباف `GetAllUserOrderByFilterFilter` فقط یک `payment_date` داشت. امکان فیلتر بازه تاریخ وجود نداشت.
**راه‌حل (end-to-end):**
- **Proto:** افزودن `google.protobuf.Timestamp from_date = 11` و `google.protobuf.Timestamp to_date = 12` به فیلتر
- **gRPC Service:** مپ `FromDate` از `from_date` (با fallback به `payment_date``ToDate` از `to_date`
- **Handler:** بدون تغییر — از قبل `FromDate`/`ToDate` را ساپورت می‌کرد
- **Frontend:** افزودن `MudDatePicker` دوم (تا تاریخ)، مپ به `Filter.FromDate`/`Filter.ToDate`
**فایل‌های تغییر یافته:**
- `CMS/.../Protos/userorder.proto`
- `CMS/.../Services/UserOrderService.cs`
- `BackOffice/.../Pages/UserOrder/UserOrderMainPage.razor`
- `BackOffice/.../Pages/UserOrder/UserOrderMainPage.razor.cs`
### ۶.۴ تصویر محصول در آیتم‌های سفارش تخفیفی (`image_path`)
**مشکل:** `OrderItemDto` در proto و C# فیلد `image_path` نداشت. آیتم‌های سفارش بدون تصویر بودند.
**راه‌حل (end-to-end):**
- **Proto:** افزودن `string image_path = 9` و `string thumbnail_path = 10` به `OrderItemDto` در `discountorder.proto`
- **Backend DTO:** افزودن `ImagePath`/`ThumbnailPath` به `OrderItemDto` (C# Application layer)
- **Handler:** مپ `ImagePath = od.Product.ImagePath` در `GetOrderByIdQueryHandler` (Product nav property از قبل Include شده بود)
- **Frontend DTO:** افزودن فیلدها به `IDiscountOrderService.OrderItemDto`
- **Frontend Service:** مپ در `DiscountOrderService`
- **Frontend Dialog:** `MudImage` + `MudStack` برای نمایش تصویر کنار نام محصول
**فایل‌های تغییر یافته:**
- `CMS/.../Protos/discountorder.proto`
- `CMS/.../GetOrderByIdQuery.cs` (OrderItemDto)
- `CMS/.../GetOrderByIdQueryHandler.cs`
- `BackOffice/.../Services/DiscountOrder/IDiscountOrderService.cs`
- `BackOffice/.../Services/DiscountOrder/DiscountOrderService.cs`
- `BackOffice/.../Pages/DiscountShop/Components/OrderDetailsDialog.razor`
---
## وضعیت مقایسه‌ای نهایی
### محصولات
| قابلیت | عادی | تخفیفی |
|---|:---:|:---:|
| لیست با MudDataGrid + server-side | ✅ | ✅ |
| فیلتر با BasePageComponent | ✅ | ✅ |
| نمایش تصویر (Image component) | ✅ | ✅ |
| برش عنوان (Truncate) | ✅ | ✅ |
| ایجاد/ویرایش/حذف | ✅ | ✅ |
| گالری تصاویر (GalleryDialog سرور-محور) | ✅ | ✅ |
| پیش‌نمایش تصویر (ImagePreviewDialog) | ✅ | ✅ |
| مدیریت دسته‌بندی (درگ و دراپ) | ✅ | ✅ |
| خروجی Excel | ✅ | ✅ |
| تگ‌های محصول (AssignTagsDialog مشترک) | ✅ | ✅ |
| فرمت قیمت (N0 + ریال) | ✅ | ✅ |
| ستون وضعیت فعال/غیرفعال | ✅ | ✅ |
| Bulk Edit/Delete/Toggle | ✅ | ❌ (طبق درخواست — بیخیال) |
### دسته‌بندی‌ها
| قابلیت | عادی | تخفیفی |
|---|:---:|:---:|
| درخت سایدبار | ✅ | ✅ |
| گرید با MudDataGrid | ✅ | ✅ |
| ستون‌ها: شناسه، نام لاتین، عنوان، والد، ترتیب، فعال | ✅ | ✅ |
| ستون تعداد محصولات | ✅ | ✅ |
| ایجاد/ویرایش/حذف | ✅ | ✅ |
| غیرفعال‌سازی حذف دارای زیردسته | ✅ | ✅ |
| افزودن زیردسته | ✅ | ✅ |
| درگ‌اندراپ محصولات دسته | ✅ | ✅ |
### سفارشات
| قابلیت | عادی | تخفیفی |
|---|:---:|:---:|
| BasePageComponent با فیلتر | ✅ | ✅ |
| MudDataGrid + تولبار | ✅ | ✅ |
| فیلتر شناسه / کاربر / تاریخ / وضعیت | ✅ | ✅ |
| فیلتر بازه تاریخ (از تاریخ + تا تاریخ) | ✅ | ✅ |
| ستون نام کاربر (لینک به پروفایل) | ✅ | ✅ |
| ستون وضعیت پرداخت (Chip رنگی) | ✅ | ✅ |
| ستون وضعیت ارسال (Chip رنگی) | ✅ | ✅ |
| ستون تاریخ پرداخت | ✅ | ✅ |
| ستون آدرس (truncated + tooltip) | ✅ | ✅ |
| جزئیات سفارش (Timeline + ویرایش inline) | ✅ | ✅ |
| تصویر محصول در آیتم‌های سفارش | ✅ | ✅ |
| تغییر وضعیت (MudForm + کد رهگیری + هشدار) | ✅ | ✅ |
| لغو سفارش | ✅ | ✅ (via UpdateStatus) |
| خروجی Excel | ✅ | ✅ |
| ستون‌های مالی تخفیف (DiscountBalanceUsed/GatewayAmount) | ❌ (مربوط نیست) | ✅ |
| اعمال تخفیف | ✅ | ❌ (API ندارد) |
### گزارش فروش
| قابلیت | عادی | تخفیفی |
|---|:---:|:---:|
| فیلتر تاریخ + وضعیت | ✅ | ✅ |
| کارت‌های خلاصه | ✅ | ✅ |
| نمودار روند فروش | ✅ | ✅ |
| نمودار وضعیت سفارش‌ها | ✅ | ✅ |
| نمودار محصولات پرفروش | ✅ | ✅ |
| خروجی Excel | ✅ | ✅ |
| خروجی PDF | ✅ | ✅ |
---
## موارد باقی‌مانده
این موارد نیاز به تغییرات بیشتر دارند:
| مورد | جزئیات | وضعیت |
|---|---|---|
| **اعمال تخفیف سفارش تخفیفی** | `discountorder.proto` فاقد `ApplyDiscountToOrder` RPC | نیاز به RPC جدید + لاجیک سرور |
| **لغو سفارش با بازپرداخت** | لغو وضعیت ✅ ولی refund/بازگشت موجودی نیاز به RPC اختصاصی | نیاز به `CancelOrder` RPC |
> **نکته:** موارد قبلی (is_active، product_count، from_date/to_date، image_path) در **فاز ۶** حل شدند.
---
## ساختار فایل‌های تغییریافته
```
CMS/src/
├── CMSMicroservice.Protobuf/Protos/
│ ├── products.proto ← is_active (filter + response field 15)
│ ├── category.proto ← product_count (response field 9)
│ ├── userorder.proto ← from_date/to_date (filter fields 11,12)
│ └── discountorder.proto ← image_path/thumbnail_path (OrderItemDto fields 9,10)
├── CMSMicroservice.Application/
│ ├── ProductsCQ/Queries/GetCustomerProductsByFilter/
│ │ ├── GetCustomerProductsByFilterQuery.cs ← IsActive filter
│ │ ├── GetCustomerProductsByFilterQueryHandler.cs ← IsActive filter + mapping
│ │ └── GetCustomerProductsByFilterResponseDto.cs ← IsActive field
│ ├── CategoryCQ/Queries/GetAllCategoryByFilter/
│ │ ├── GetAllCategoryByFilterQueryHandler.cs ← manual Select + ProductCount
│ │ └── GetAllCategoryByFilterResponseDto.cs ← ProductCount field
│ └── DiscountShopCQ/Queries/GetOrderById/
│ ├── GetOrderByIdQuery.cs ← ImagePath/ThumbnailPath
│ └── GetOrderByIdQueryHandler.cs ← Product image mapping
└── CMSMicroservice.WebApi/Services/
├── ProductsService.cs ← IsActive filter + response mapping
└── UserOrderService.cs ← FromDate/ToDate mapping
BackOffice/src/BackOffice/
├── Services/DiscountOrder/
│ ├── IDiscountOrderService.cs ← OrderItemDto + ImagePath/ThumbnailPath
│ └── DiscountOrderService.cs ← image mapping
├── Pages/
│ ├── Category/
│ │ ├── CategoryMainPage.razor ← ستون ProductCount + SortOrder + زیردسته
│ │ ├── CategoryMainPage.razor.cs ← HasChildren + CreateSubcategory
│ │ ├── CreateOrUpdateCategoryDialog.razor ← MudForm + ImagePath
│ │ └── CreateOrUpdateCategoryDialog.razor.cs ← validation + loading
│ ├── DiscountShop/
│ │ ├── DiscountShopHub.razor
│ │ ├── DiscountProductsMainPage.razor/.cs
│ │ ├── DiscountCategoriesMainPage.razor/.cs
│ │ ├── DiscountOrdersMainPage.razor/.cs
│ │ ├── SalesReports.razor
│ │ └── Components/
│ │ ├── OrderDetailsDialog.razor ← تصویر محصول + Timeline
│ │ └── OrderDetailsDialog.razor.cs
│ ├── UserOrder/
│ │ ├── UserOrderMainPage.razor ← فیلتر بازه تاریخ (از + تا)
│ │ ├── UserOrderMainPage.razor.cs ← FromDate/ToDate mapping
│ │ ├── OrderSalesReports.razor/.cs
│ │ └── Components/ChangeOrderStatusDialog.razor/.cs
│ └── Products/
│ ├── ProductsMainPage.razor ← ستون IsActive + فرمت قیمت
│ └── Components/
│ ├── CreateDialog.razor ← قیمت (ریال)
│ └── UpdateDialog.razor ← قیمت (ریال)
```
+196
View File
@@ -0,0 +1,196 @@
# فروشگاه تخفیفی — وضعیت پیاده‌سازی و تسک‌ها
> **تاریخ:** ۱۴۰۴/۱۱/۲۲ (2026-02-11)
> **آخرین بروزرسانی:** ۱۴۰۴/۱۱/۲۳
> **وضعیت کلی:** بکند کامل ✅ | بک‌آفیس کامل ✅ | فرانت‌آفیس کامل ✅
---
## ۱. خلاصه بیزینس
فروشگاه تخفیفی یک فروشگاه **مجزا** از فروشگاه معمولی است که:
- محصولات خاص خود را دارد (`DiscountProduct` — نه `Product`)
- پرداخت **ترکیبی** (Hybrid) دارد:
- بخشی از **موجودی کیف پول تخفیفی** (`DiscountBalance`) کسر می‌شود
- مابقی از **درگاه پرداخت** (IPG) پرداخت می‌شود
- هر محصول یک `MaxDiscountPercent` دارد (مثلاً ۳۰٪) — حداکثر درصدی که از کیف تخفیفی قابل پرداخت است
- مالیات فقط روی مبلغ درگاه محاسبه می‌شود
---
## ۲. وضعیت لایه‌ها
### ✅ Domain Entities — کامل (۷ entity)
| Entity | مسیر | توضیح |
|--------|------|-------|
| `DiscountProduct` | `CMS/.../Entities/DiscountStore/` | محصول (Title, Price, MaxDiscountPercent, RemainingCount, ...) |
| `DiscountProductCategory` | ↑ | دسته‌بندی درختی |
| `DiscountProductCategoryMapping` | ↑ | M:N محصول ↔ دسته‌بندی |
| `DiscountProductImage` | ↑ | گالری تصاویر |
| `DiscountShoppingCart` | ↑ | سبد خرید (UserId, ProductId, Count) |
| `DiscountOrder` | ↑ | سفارش (TotalAmount, DiscountBalanceUsed, GatewayAmountPaid, VAT) |
| `DiscountOrderDetail` | ↑ | جزئیات سفارش (UnitPrice, DiscountPercent, DiscountAmount, FinalPrice) |
### ✅ EF Configurations — کامل (۷ فایل + ۶ migration)
### ✅ Application (CQRS) — کامل (~۵۰ فایل)
- DiscountProductCQ: Create, Update, Delete, GetById, GetProducts + Image CRUD
- DiscountCategoryCQ: Create, Update, Delete, GetCategories
- DiscountOrderCQ: PlaceOrder, CompleteOrderPayment, UpdateOrderStatus, GetById, GetUserOrders, GetAll, SalesReport
- DiscountShoppingCartCQ: AddToCart, RemoveFromCart, UpdateCount, GetUserCart, ClearCart
- WalletCQ: ChargeDiscountWallet, VerifyDiscountWalletCharge
### ✅ Proto Definitions — کامل (۴ فایل)
| Proto | Namespace | RPCs |
|-------|-----------|------|
| `discountproduct.proto` | `CMSMicroservice.Protobuf.Protos.DiscountProduct` | DiscountProductContract (10 RPCs) |
| `discountcategory.proto` | `CMSMicroservice.Protobuf.Protos.DiscountCategory` | DiscountCategoryContract (4 RPCs) |
| `discountshoppingcart.proto` | `CMSMicroservice.Protobuf.Protos.DiscountShoppingCart` | DiscountShoppingCartContract (5 RPCs) |
| `discountorder.proto` | `CMSMicroservice.Protobuf.Protos.DiscountOrder` | DiscountOrderContract (7 RPCs) |
### ✅ gRPC Services (CMS WebApi) — کامل (۴ سرویس + mapping)
### ✅ BackOffice (Admin Panel) — کامل
- ۴ صفحه: محصولات، دسته‌بندی‌ها، سفارشات، گزارش فروش
- ۵ کامپوننت: فرم محصول، فرم دسته‌بندی، گالری، جزئیات سفارش، تغییر وضعیت
- ۶ سرویس: DiscountProduct, DiscountCategory, DiscountOrder (+ interfaces)
- NavMenu: بخش "فروشگاه تخفیفی" با ۳ لینک (محصولات، دسته‌بندی‌ها، سفارشات و گزارش)
- **یکسان‌سازی UI (بهمن ۱۴۰۴):** تمام صفحات فروشگاه تخفیفی بازنویسی شدند تا از `BasePageComponent` استفاده کنند و ظاهری یکسان با فروشگاه عادی داشته باشند → [جزئیات](../ui-modernization/BACKOFFICE-STORE-UNIFICATION.md)
### ✅ FrontOffice (مشتری) — پیاده‌سازی شده!
**فایل‌های اضافه/ویرایش شده:**
| فایل | نوع | توضیح |
|------|------|-------|
| `Utilities/RouteConstants.cs` | ویرایش | اضافه شدن بخش `DiscountStore` (6 مسیر) |
| `ConfigureServices.cs` | ویرایش | ثبت 3 سرویس + 4 gRPC client |
| `Utilities/DiscountProductService.cs` | جدید | سرویس محصولات تخفیفی (GetProducts, GetById, GetCategories) |
| `Utilities/DiscountCartService.cs` | جدید | سرویس سبد خرید تخفیفی (Add, Remove, Update, Clear) |
| `Utilities/DiscountOrderService.cs` | جدید | سرویس سفارش تخفیفی (PlaceOrder, CompletePayment, GetOrders) |
| `Pages/DiscountStore/Products.razor(.cs)` | جدید | لیست محصولات (جستجو + فیلتر دسته‌بندی + صفحه‌بندی) |
| `Pages/DiscountStore/ProductDetail.razor(.cs)` | جدید | جزئیات محصول + گالری + افزودن به سبد |
| `Pages/DiscountStore/Cart.razor(.cs)` | جدید | سبد خرید (Desktop: Table / Mobile: Cards) |
| `Pages/DiscountStore/Checkout.razor(.cs)` | جدید | پرداخت ترکیبی (آدرس + اسلایدر تخفیف + درگاه) |
| `Pages/DiscountStore/Orders.razor(.cs)` | جدید | لیست سفارشات (پرداخت/ارسال) |
| `Pages/DiscountStore/OrderDetail.razor(.cs)` | جدید | جزئیات سفارش + خلاصه مالی |
| `Pages/Profile/Index.razor.cs` | ویرایش | تایل "فروشگاه تخفیفی" در داشبورد |
| `Shared/MainLayout.razor` | ویرایش | لینک ناوبری دسکتاپ + drawer موبایل |
| `wwwroot/css/site.css` | ویرایش | ریجن CSS اختصاصی Discount Store |
---
## ۳. تسک‌های FrontOffice (ترتیب اجرا)
### تسک ۱: Routes — اضافه کردن مسیرها
```
فایل: RouteConstants.cs
اضافه: public static class DiscountStore {
Products = "/discount-store"
ProductDetail = "/discount-store/product/"
Cart = "/discount-store/cart"
Checkout = "/discount-store/checkout"
Orders = "/discount-store/orders"
OrderDetail = "/discount-store/order/"
}
```
### تسک ۲: gRPC Clients — ثبت DI
```
فایل: ConfigureServices.cs
اضافه:
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
using CMSMicroservice.Protobuf.Protos.DiscountCategory;
using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
services.AddScoped(CreateAuthenticatedClient<DiscountProductContract.DiscountProductContractClient>);
services.AddScoped(CreateAuthenticatedClient<DiscountCategoryContract.DiscountCategoryContractClient>);
services.AddScoped(CreateAuthenticatedClient<DiscountShoppingCartContract.DiscountShoppingCartContractClient>);
services.AddScoped(CreateAuthenticatedClient<DiscountOrderContract.DiscountOrderContractClient>);
```
### تسک ۳: Services — سرویس‌های FrontOffice
```
فایل‌های جدید در Utilities/:
DiscountProductService.cs — GetProducts (فیلتر + صفحه‌بندی), GetById, GetCategories
DiscountCartService.cs — Add, Remove, Update, GetCart, Clear + event OnChange
DiscountOrderService.cs — PlaceOrder, CompletePayment, GetUserOrders, GetOrderById
```
### تسک ۴: صفحات Blazor
```
فایل‌های جدید در Pages/DiscountStore/:
Products.razor + .cs — لیست محصولات (فیلتر دسته‌بندی + جستجو + صفحه‌بندی)
ProductDetail.razor + .cs — جزئیات محصول + گالری + افزودن به سبد
Cart.razor + .cs — سبد خرید (نمایش تخفیف هر آیتم)
Checkout.razor + .cs — پرداخت (انتخاب آدرس + تعیین مبلغ از تخفیفی + درگاه)
Orders.razor + .cs — لیست سفارشات
OrderDetail.razor + .cs — جزئیات سفارش + وضعیت ارسال
```
### تسک ۵: Dashboard Tile
```
فایل: Profile/Index.razor.cs
اضافه: تایل "فروشگاه تخفیفی" بعد از تایل "فروشگاه" موجود
```
### تسک ۶: Navigation
```
فایل: MainLayout.razor
اضافه: لینک "فروشگاه تخفیفی" در drawer موبایل + bottom nav (اختیاری)
```
### تسک ۷: CSS
```
فایل: site.css
اضافه: استایل‌های اختصاصی (checkout progress, discount badge, ...)
```
---
## ۴. فلوی پرداخت (مهم!)
```
کاربر سبد خرید دارد
صفحه Checkout:
├─ انتخاب آدرس تحویل
├─ نمایش خلاصه سبد:
│ هر محصول: قیمت × تعداد
│ تخفیف هر محصول: price × count × maxDiscountPercent / 100
│ جمع کل / جمع تخفیف / مبلغ درگاه
├─ موجودی تخفیفی کاربر: XXX تومان
├─ کاربر تعیین می‌کند چقدر از تخفیفی استفاده کند (≤ سقف مجاز)
└─ [پرداخت]
PlaceOrder RPC:
├─ بررسی موجودی + محاسبه
├─ ساخت سفارش (Pending)
├─ رزرو موجودی انبار
├─ اگر gateway_amount > 0 → payment_url برگردانده می‌شود
└─ اگر gateway_amount = 0 → سفارش مستقیم تکمیل
ریدایرکت به درگاه (اگر لازم باشد)
CompleteOrderPayment RPC (بعد از callback):
├─ success → کسر DiscountBalance + تأیید فروش + ثبت تراکنش
└─ failure → آزادسازی رزرو انبار + لغو سفارش
```
---
## ۵. تخمین زمان
| تسک | تخمین |
|-----|-------|
| Routes + DI + Services | ۱ ساعت |
| Products + ProductDetail | ۲ ساعت |
| Cart | ۱ ساعت |
| Checkout (پیچیده‌ترین بخش) | ۲ ساعت |
| Orders + OrderDetail | ۱ ساعت |
| Dashboard tile + Nav | ۰.۵ ساعت |
| CSS + Polish | ۰.۵ ساعت |
| **مجموع** | **~۸ ساعت** |
+251
View File
@@ -0,0 +1,251 @@
# 📁 معماری مدیریت فایل و تصاویر — CMS
> **تاریخ:** ۱۴۰۴/۱۱/۲۸ (February 17, 2026)
> **وضعیت:** ✅ عملیاتی
> **Build:** 0 Error (هر ۳ پروژه) ✅
---
## ۱. پیش‌زمینه
سیستم قبلی از **FMS (File Management Service)** در آدرس `https://dl.afrino.co` استفاده می‌کرد که غیرقابل دسترس/ناسازگار شده بود. در چندین فاز، معماری فایل‌ها به صورت کامل بازنویسی شد:
| فاز | شرح | وضعیت |
|-----|------|-------|
| ۱. حذف FMS | حذف کامل ۳ فایل مرده FMS | ✅ |
| ۲. حالت base64 | ذخیره data URI مستقیم در DB | ✅ (بازنشسته) |
| ۳. ذخیره دیسکی | فایل در دیسک + مسیر در DB + تبدیل به base64 هنگام serve | ✅ |
| ۴. **سرو HTTP عمومی** | **اندپوینت `/uploads/{path}` + Fallback FMS** | **✅ جدید** |
---
## ۲. معماری نهایی
```
BackOffice (Blazor WASM)
│ MudFileUpload → IBrowserFile → byte[] → gRPC ImageFileModel
CMS gRPC Service
│ proto ImageFileModel → Command.ImageFileBytes
MediatR Handler
│ IFileManager.UploadImageAsync(folder, bytes, mime, name)
LocalFileManager
├─ Main Image → Uploads/Images/{folder}/{guid}.jpg (1200×1200, JPEG Q75)
├─ Thumbnail → Uploads/Images/{folder}/{guid}_thumb.jpg (300×300, JPEG Q75)
│ Returns: { Main.Path, Thumbnail.Path } (relative paths stored in DB)
ImagePathResolverInterceptor (gRPC response)
│ Walks all response fields → reads file from disk → data:{mime};base64,{bytes}
BackOffice / FrontOffice ← receives base64 data URI directly in proto fields
```
---
## ۳. اجزای کلیدی
### ۳.۱ `IFileManager` — Interface
**مسیر:** `Application/Common/FileManager/IFileManager.cs`
```csharp
public interface IFileManager
{
Task<UploadResult> UploadAsync(string folder, byte[] file, string mime, string? fileName, CancellationToken ct);
Task<ImageUploadResult> UploadImageAsync(string folder, byte[] file, string mime, string? fileName, CancellationToken ct);
Task DeleteAsync(string path, CancellationToken ct);
string? ResolveImageUrl(string? path);
}
```
- **`UploadAsync`** — آپلود فایل خام
- **`UploadImageAsync`** — بهینه‌سازی + ساخت thumbnail خودکار (SixLabors.ImageSharp)
- **`ResolveImageUrl`** — تبدیل مسیر نسبی به data URI (base64)
### ۳.۲ `LocalFileManager` — پیاده‌سازی
**مسیر:** `Infrastructure/Services/LocalFileManager.cs`
| ویژگی | مقدار |
|-------|-------|
| ریشه آپلود | `FileStorage:UploadPath` یا `AppContext.BaseDirectory/Uploads` |
| فرمت تصویر اصلی | JPEG, Quality 75, حداکثر 1200×1200 |
| فرمت thumbnail | JPEG, Quality 75, حداکثر 300×300 |
| نام‌گذاری فایل | `{Guid}.jpg` + `{Guid}_thumb.jpg` |
| DI Registration | `services.AddSingleton<IFileManager, LocalFileManager>()` |
### ۳.۳ `ImagePathResolverInterceptor` — gRPC Interceptor
**مسیر:** `WebApi/Interceptors/ImagePathResolverInterceptor.cs`
اینترسپتور **خودکار** تمام فیلدهای تصویری را در response‌های gRPC پیدا کرده و مسیر نسبی را به data URI تبدیل می‌کند.
**فیلدهای شناسایی‌شده:**
- `image_path`, `thumbnail_path`, `image_thumbnail_path`
- `featured_image_path`, `featured_image_thumbnail_path`
- `hero_image_path`, `product_thumbnail_path`
- `avatar_path`, `avatar_url`
**قابلیت‌ها:**
- Walk بازگشتی پیام‌های proto
- پشتیبانی از `string` ساده و `Google.Protobuf.WellKnownTypes.StringValue`
- پشتیبانی از فیلدهای `repeated` (collection‌های تو در تو)
- اگر مقدار `data:` یا `http` باشد → رد می‌شود (تبدیل نمی‌شود)
### ۳.۴ `LoggingBehaviour` — پاکسازی لاگ
**مسیر:** `WebApi/Common/Behaviours/LoggingBehaviour.cs`
- فرمت لاگ: `JsonFormatter.Default.Format()` به جای `{@Request}`
- پاکسازی فیلدهای باینری با regex (`File`, `ImageFile`, `image_file`, `file`)
- محدودیت طول لاگ: حداکثر 2000 کاراکتر
### ۳.۵ `UploadsController` — سرو عمومی فایل‌ها (HTTP) 🆕
**مسیر:** `WebApi/Controllers/UploadsController.cs`
اندپوینت عمومی REST برای سرو مستقیم تصاویر بدون نیاز به base64. مناسب برای بارگذاری تصاویر در تگ `<img>` و کاهش پهنای باند.
| ویژگی | مقدار |
|-------|-------|
| مسیر | `GET /uploads/{**path}` |
| احراز هویت | `[AllowAnonymous]` — عمومی |
| کش مرورگر | `ResponseCache 86400` ثانیه (۲۴ ساعت) |
| Content-Type | تشخیص خودکار از پسوند فایل (`FileExtensionContentTypeProvider`) |
| Range Requests | ✅ فعال (`enableRangeProcessing: true`) |
| محافظت مسیر | جلوگیری از path traversal (`..`, `\`, `Path.GetFullPath` validation) |
**FMS Fallback:**
اگر فایل محلی وجود نداشته باشد و تنظیم `FMS:Address` پر باشد:
1. فایل از `{FMS:Address}/{relativePath}` دانلود می‌شود
2. Content-Type بررسی می‌شود (فقط `image/*` و `application/pdf` مجاز)
3. فایل روی دیسک محلی ذخیره و کش می‌شود
4. سپس فایل محلی سرو می‌شود
```
Client → GET /uploads/Images/BlogPosts/abc.jpg
├─ فایل محلی وجود دارد? → سرو مستقیم از دیسک
└─ فایل محلی وجود ندارد?
└─ FMS:Address تنظیم شده?
├─ بله → دانلود از dl.afrino.co → ذخیره محلی → سرو
└─ خیر → 404 Not Found
```
**وابستگی‌ها:**
- `IHttpClientFactory` با named client `"FMS"` (timeout: 30 ثانیه)
- ثبت در `Program.cs`: `builder.Services.AddHttpClient("FMS", ...)`
---
## ۴. Proto Messages — ImageFileModel
هر حوزه (DiscountProduct, BlogPost, SitePage) پیام مستقل `ImageFileModel` خود را دارد:
### DiscountProduct
```protobuf
message ImageFileModel {
bytes file = 1;
string mime = 2;
string file_name = 3;
}
```
**استفاده در:** `CreateDiscountProductRequest`, `UpdateDiscountProductRequest`
### BlogPost
```protobuf
message BlogImageFileModel {
bytes file = 1;
string mime = 2;
string file_name = 3;
}
```
**استفاده در:** `CreateBlogPostRequest`, `UpdateBlogPostRequest`
### SitePage
```protobuf
message SitePageImageFileModel {
bytes file = 1;
string mime = 2;
string file_name = 3;
}
```
**استفاده در:** `UpdateSitePageRequest`, `CreateSitePageSectionRequest`, `UpdateSitePageSectionRequest`
---
## ۵. جریان آپلود تصویر (مثال: BlogPost)
```
1. کاربر در BackOffice → MudFileUpload → انتخاب فایل
2. BlogPostEditDialog.OnImageSelected()
→ IBrowserFile.OpenReadStream() → byte[] + ContentType + FileName
→ پیش‌نمایش base64 در UI
3. Submit → BlogPostEditDto { ImageFile = bytes, ImageMime, ImageFileName }
4. BlogPostService.CreateAsync()
→ BlogImageFileModel { File = ByteString.CopyFrom(bytes), Mime, FileName }
→ gRPC CreateBlogPostRequest
5. CMS BlogPostService (gRPC) → CreateBlogPostCommand
{ ImageFileBytes = request.ImageFile.File.ToByteArray(), ... }
6. CreateBlogPostCommandHandler.Handle()
→ _fileManager.UploadImageAsync("Images/BlogPosts", bytes, mime, name)
→ post.FeaturedImagePath = result.Main.Path
→ post.FeaturedImageThumbnailPath = result.Thumbnail.Path
7. Response → ImagePathResolverInterceptor
→ featured_image_path → data:image/jpeg;base64,...
→ featured_image_thumbnail_path → data:image/jpeg;base64,...
8. BackOffice / FrontOffice → نمایش مستقیم base64 data URI
```
---
## ۶. فایل‌های حذف‌شده (کد مرده FMS)
| فایل | شرح |
|------|------|
| `Infrastructure/Services/FmsFileManager.cs` | پیاده‌سازی قدیمی FMS (HTTP upload) |
| `Application/Common/FileManager/FileManagementService.cs` | سرویس قدیمی مدیریت فایل |
| `Application/Common/FileManager/IFileManagementService.cs` | اینترفیس قدیمی |
---
## ۷. تنظیمات
### `appsettings.json` (CMS)
```json
{
"FileStorage": {
"UploadPath": "/app/Uploads"
}
}
```
### محدودیت حجم gRPC
```csharp
// Program.cs
services.AddGrpc(o => o.MaxReceiveMessageSize = 50 * 1024 * 1024); // 50MB
```
### FrontOffice — `UrlUtility.GetImageUrl()`
```csharp
public static string GetImageUrl(string? path)
{
if (string.IsNullOrWhiteSpace(path)) return string.Empty;
if (path.StartsWith("data:") || path.StartsWith("http")) return path;
return $"{DownloadUrl?.TrimEnd('/')}/{path.TrimStart('/')}";
}
```
+187
View File
@@ -0,0 +1,187 @@
# 🔐 فیکس فلوی ثبت‌نام / ورود FrontOffice
**تاریخ:** بهمن ۱۴۰۴ (February 2026)
---
## 📋 خلاصه
بررسی کامل فلوی ثبت‌نام و ورود FrontOffice از UI تا دیتابیس انجام شد. **۳ باگ بحرانی** شناسایی و رفع شده:
| # | شدت | مشکل | فایل |
|---|------|-------|------|
| 1 | 🔴 بحرانی | کاربران جدید ثبت‌نام نمی‌شوند | `UserCQ/VerifyOtpToken/VerifyOtpTokenCommandHandler.cs` |
| 2 | 🔴 بحرانی | امضای قرارداد همیشه شکست می‌خورد | `UserCQ/AcceptContract/AcceptContractCommandHandler.cs` |
| 3 | 🟡 متوسط | منوی کناری وضعیت نادرست نشان می‌دهد | `FrontOffice.Main/Utilities/AuthService.cs` |
---
## 🔴 باگ ۱ — کاربران جدید ثبت‌نام نمی‌شوند
### مشکل
FrontOffice از `UserContract.UserContractClient` (user.proto) استفاده می‌کند → `UserCQ/VerifyOtpTokenCommandHandler`. این handler وقتی کاربر یافت نمی‌شد فقط خطای **«کاربر یافت نشد»** برمی‌گرداند و کاربر جدید ایجاد **نمی‌کرد**.
لاجیک ایجاد کاربر (شامل: اعتبارسنجی کد معرف، درخت باینری، موقعیت شاخه) در `OtpTokenCQ/VerifyOtpTokenCommandHandler` بود — سرویسی که FrontOffice اصلاً از آن استفاده نمی‌کند.
### رفع
اضافه شدن لاجیک کامل ایجاد کاربر جدید به `UserCQ/VerifyOtpTokenCommandHandler`:
```
if (user == null)
{
// ۱. اعتبارسنجی کد معرف (ParentReferralCode) — الزامی
// ۲. بررسی وجود معرف و فعال بودن عضویت باشگاه
// ۳. بررسی ظرفیت (حداکثر ۲ زیرمجموعه مستقیم)
// ۴. تعیین شاخه (چپ اول، بعد راست)
// ۵. ایجاد User + UserRole + UserWallet
// ۶. رویدادهای دامنه: CreateNewUserEvent, CreateNewUserRoleEvent, CreateNewUserWalletEvent
// ۷. بارگذاری مجدد کاربر با روابط کامل
// ۸. تولید JWT token
}
```
### اعتبارسنجی‌ها
| مرحله | شرط | پیام خطا |
|-------|------|---------|
| کد معرف | خالی یا null | «کد معرف الزامی است» |
| معرف | وجود نداشته باشد | «معرف وجود ندارد» |
| عضویت باشگاه | غیرفعال باشد | «لینک دعوت معرف فعال نیست» |
| ظرفیت | بیش از ۱ فرزند | «ظرفیت معرف تکمیل است» |
| شاخه | هر دو پُر باشند | «ظرفیت معرف تکمیل است» |
### فایل
`CMS/src/CMSMicroservice.Application/UserCQ/Commands/VerifyOtpToken/VerifyOtpTokenCommandHandler.cs`
---
## 🔴 باگ ۲ — امضای قرارداد همیشه شکست می‌خورد
### مشکل
`AcceptContractCommandHandler` از `_currentUserService.Username` برای پیدا کردن OTP و کاربر بر اساس شماره موبایل استفاده می‌کرد:
```csharp
// ❌ قبل — Username = "{FirstName} {LastName}" نه شماره موبایل!
var otpToken = await _context.OtpTokens
.Where(x => x.Mobile == _currentUserService.Username ...)
var user = await _context.Users
.Where(x => x.Mobile == _currentUserService.Username ...)
```
**`CurrentUserService.Username`** مقدار `ClaimTypes.Name` را برمی‌گرداند که در JWT به صورت `"{FirstName} {LastName}"` ذخیره شده — **نه شماره موبایل!**
### رفع
اول کاربر بر اساس `UserId` (از `ClaimTypes.NameIdentifier`) پیدا شود، سپس از `user.Mobile` برای جستجوی OTP استفاده شود:
```csharp
// ✅ بعد — ابتدا کاربر از UserId پیدا شود
var userId = long.Parse(_currentUserService.UserId);
var user = await _context.Users
.Where(x => x.Id == userId)...
var otpToken = await _context.OtpTokens
.Where(x => x.Mobile == user.Mobile ...)
```
### فایل
`CMS/src/CMSMicroservice.Application/UserCQ/Commands/AcceptContract/AcceptContractCommandHandler.cs`
---
## 🟡 باگ ۳ — `IsCompleteRegister()` داده قدیمی می‌خواند
### مشکل
متد sync در `AuthService`:
```csharp
// ❌ قبل — GetAwaiter() بدون GetResult() عملیات async را اجرا نمی‌کند
InitUserAuthInfo().GetAwaiter();
```
`GetAwaiter()` فقط یک شیء awaiter برمی‌گرداند ولی عملیات را اجرا **نمی‌کند**. در نتیجه `_userAuthInfo` مقداردهی نمی‌شود و منوی کناری (`MainLayout.razor`) وضعیت نادرست نشان می‌دهد.
### رفع
```csharp
// ✅ بعد — عملیات async را همگام اجرا می‌کند
InitUserAuthInfo().GetAwaiter().GetResult();
```
### محل استفاده
`MainLayout.razor` خطوط ۱۰۶ و ۱۰۹:
```razor
Disabled="@(!AuthService.IsCompleteRegister())"
```
### فایل
`FrontOffice/src/FrontOffice.Main/Utilities/AuthService.cs`
---
## 🏗️ معماری فلوی ثبت‌نام (بعد از فیکس)
```
┌─────────────────────────────────────────────────────────────┐
│ FrontOffice (Blazor WASM) │
│ │
│ LoginPage → SendOtp → VerifyOtp(mobile, code, referral) │
│ ↓ gRPC-Web │
├─────────────────────────────────────────────────────────────┤
│ CMS Backend (gRPC) │
│ │
│ UserContract.VerifyOtpToken │
│ ↓ │
│ UserCQ/VerifyOtpTokenCommandHandler │
│ │ │
│ ├── OTP صحیح؟ → ❌ خطا │
│ │ │
│ ├── کاربر موجود؟ → ✅ تولید JWT Token │
│ │ │
│ └── کاربر جدید؟ │
│ ├── اعتبارسنجی کد معرف │
│ ├── بررسی ظرفیت درخت باینری │
│ ├── ایجاد User + UserRole + UserWallet │
│ ├── رویدادهای دامنه │
│ └── تولید JWT Token │
│ │
│ بعد از ثبت‌نام → RegisterWizard: │
│ Step 1: اطلاعات شخصی │
│ Step 2: امضای قرارداد (AcceptContract + OTP) │
│ Step 3: خرید پکیج │
├─────────────────────────────────────────────────────────────┤
│ Database │
│ │
│ Users ─── UserRoles ─── UserWallets │
│ └── NetworkParentId, LegPosition (Binary Tree) │
│ └── UserContracts, ClubMembership │
└─────────────────────────────────────────────────────────────┘
```
---
## 📝 فایل‌های تغییر یافته
| فایل | تغییر |
|------|-------|
| `CMS/.../UserCQ/Commands/VerifyOtpToken/VerifyOtpTokenCommandHandler.cs` | اضافه شدن لاجیک ایجاد کاربر جدید (86→165 خط) |
| `CMS/.../UserCQ/Commands/AcceptContract/AcceptContractCommandHandler.cs` | تغییر lookup از Username به UserId |
| `FrontOffice/.../Utilities/AuthService.cs` | اضافه شدن `.GetResult()` به `GetAwaiter()` |
---
## ✅ بیلد
```
CMS: 0 Error(s) ✅
FrontOffice: 0 Error(s) ✅
BackOffice: 0 Error(s) ✅
```
+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