diff --git a/docs/DISCOUNT-PRODUCT-FORM-REFACTORING.md b/docs/DISCOUNT-PRODUCT-FORM-REFACTORING.md new file mode 100644 index 0000000..fc24687 --- /dev/null +++ b/docs/DISCOUNT-PRODUCT-FORM-REFACTORING.md @@ -0,0 +1,274 @@ +# Refactoring DiscountProduct Form - Session 2026-01-05 + +## هدف +یکسان‌سازی فرم ایجاد/ویرایش محصولات تخفیفی (DiscountProduct) با فرم محصولات عادی (Product) برای بهبود تجربه کاربری و افزودن قابلیت‌های حرفه‌ای. + +## تغییرات انجام شده + +### 1. Frontend - ProductFormDialog.razor + +#### قبل: +- استفاده از MudGrid +- TextField ساده برای مسیر تصویر (بدون قابلیت آپلود) +- TextField چند خطی برای توضیحات +- MudSelect ساده برای دسته‌بندی‌ها +- فاقد پیش‌نمایش تصویر + +#### بعد: +- **تغییر Layout**: از MudGrid به MudStack +- **آپلود تصویر**: + - MudFileUpload برای انتخاب تصویر اصلی و بندانگشتی + - پیش‌نمایش تصویر قبل از آپلود + - نمایش تصویر موجود در حالت ویرایش +- **ویرایشگر HTML**: + - استفاده از MudHtmlEditor برای FullInformation + - قابلیت فرمت‌دهی متن (bold, italic, لیست و...) +- **دسته‌بندی**: + - استفاده از MudSelect با MultiSelection + - لود خودکار DiscountCategory ها (نه Category های عادی) + - نمایش تعداد دسته‌بندی‌های انتخاب شده +- **فیلدهای اختصاصی حفظ شده**: + - MaxDiscountPercent + - IsActive + - SortOrder + +**فایل**: `/home/masoud/Apps/project/FourSat/BackOffice/src/BackOffice/Pages/DiscountShop/Components/ProductFormDialog.razor` + +### 2. Proto File - discountproduct.proto + +#### تغییرات: +```protobuf +message CreateDiscountProductRequest { + // ... فیلدهای قبلی + ImageFileModel image_file = 12; // جدید + ImageFileModel thumbnail_file = 13; // جدید +} + +message UpdateDiscountProductRequest { + // ... فیلدهای قبلی + ImageFileModel image_file = 12; // جدید + ImageFileModel thumbnail_file = 13; // جدید +} + +// تعریف مدل جدید +message ImageFileModel { + bytes file = 1; + string mime = 2; + string file_name = 3; +} +``` + +**فایل**: `/home/masoud/Apps/project/FourSat/BackOffice.BFF/src/Protobufs/BackOffice.BFF.DiscountProduct.Protobuf/Protos/discountproduct.proto` + +### 3. DTOs - Backend + +#### CreateDiscountProductDto و UpdateDiscountProductDto: +```csharp +// پراپرتی‌های جدید اضافه شده: +public byte[]? ImageFile { get; set; } +public string? ImageMime { get; set; } +public string? ImageFileName { get; set; } +public byte[]? ThumbnailFile { get; set; } +public string? ThumbnailMime { get; set; } +public string? ThumbnailFileName { get; set; } +``` + +**فایل‌ها**: +- `/home/masoud/Apps/project/FourSat/BackOffice/src/BackOffice/Services/DiscountProduct/IDiscountProductService.cs` + +### 4. Service Layer - DiscountProductService + +#### تغییرات CreateAsync و UpdateAsync: +```csharp +// افزودن لاجیک آپلود فایل +if (dto.ImageFile != null && !string.IsNullOrEmpty(dto.ImageMime)) { + request.ImageFile = new ImageFileModel { + File = ByteString.CopyFrom(dto.ImageFile), + Mime = dto.ImageMime, + FileName = dto.ImageFileName + }; +} +``` + +**فایل**: `/home/masoud/Apps/project/FourSat/BackOffice/src/BackOffice/Services/DiscountProduct/DiscountProductService.cs` + +### 5. Page Integration - DiscountProductsMainPage + +#### تغییرات در OpenCreateDialog و OpenEditDialog: +```csharp +var dto = new CreateDiscountProductDto { + // ... سایر فیلدها + InitialCount = 0, // تغییر: همیشه 0 - موجودی از طریق Inventory مدیریت می‌شود + ImageFile = formData.ImageFile, + ImageMime = formData.ImageMime, + ImageFileName = formData.ImageFileName, + ThumbnailFile = formData.ThumbnailFile, + ThumbnailMime = formData.ThumbnailMime, + ThumbnailFileName = formData.ThumbnailFileName +}; +``` + +**فایل**: `/home/masoud/Apps/project/FourSat/BackOffice/src/BackOffice/Pages/DiscountShop/DiscountProductsMainPage.razor` + +### 6. BFF - Validators + +#### اصلاح CreateDiscountProductCommandValidator: +```csharp +// قبل: الزامی بودن ShortInformation و FullInformation +RuleFor(x => x.ShortInformation) + .NotEmpty().WithMessage("اطلاعات کوتاه محصول الزامی است") + +// بعد: اختیاری شدن با چک طول +RuleFor(x => x.ShortInformation) + .MaximumLength(500) + .When(x => !string.IsNullOrEmpty(x.ShortInformation)); + +RuleFor(x => x.FullInformation) + .MaximumLength(10000) + .When(x => !string.IsNullOrEmpty(x.FullInformation)); +``` + +**فایل‌ها**: +- `/home/masoud/Apps/project/FourSat/BackOffice.BFF/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/CreateDiscountProduct/CreateDiscountProductCommandValidator.cs` +- `/home/masoud/Apps/project/FourSat/BackOffice.BFF/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/UpdateDiscountProduct/UpdateDiscountProductCommandValidator.cs` + +### 7. BFF - Command Handlers + +#### اصلاح CreateDiscountProductCommandHandler: +```csharp +// افزودن null coalescing برای جلوگیری از ArgumentNullException +var createRequest = new CreateDiscountProductRequest { + ShortInfomation = request.ShortInformation ?? string.Empty, + FullInformation = request.FullInformation ?? string.Empty, + // ... +}; + +// افزودن لاجیک آپلود فایل به File Storage +if (request.ImageFile != null && request.ImageFile.File.Length > 0) { + var imageFileInfo = await _context.FileInfos.CreateNewFileInfoAsync(...); + if (imageFileInfo != null) + createRequest.ImagePath = imageFileInfo.File; +} +``` + +**فایل‌ها**: +- `/home/masoud/Apps/project/FourSat/BackOffice.BFF/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/CreateDiscountProduct/CreateDiscountProductCommandHandler.cs` +- `/home/masoud/Apps/project/FourSat/BackOffice.BFF/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/UpdateDiscountProduct/UpdateDiscountProductCommandHandler.cs` + +### 8. CMS - Validators + +#### اصلاح CreateDiscountProductCommandValidator: +```csharp +// حذف الزامی بودن ImagePath و ThumbnailPath +// قبل: +RuleFor(v => v.ImagePath).NotEmpty() +RuleFor(v => v.ThumbnailPath).NotEmpty() + +// بعد: حذف شده - فایل‌ها از طریق BFF آپلود می‌شوند + +// ShortInformation و FullInformation اختیاری شدن +RuleFor(v => v.ShortInfomation) + .MaximumLength(500) + .When(v => !string.IsNullOrEmpty(v.ShortInfomation)); +``` + +**فایل‌ها**: +- `/home/masoud/Apps/project/FourSat/CMS/src/CMSMicroservice.Application/DiscountShopCQ/Commands/CreateDiscountProduct/CreateDiscountProductCommandValidator.cs` +- `/home/masoud/Apps/project/FourSat/CMS/src/CMSMicroservice.Application/DiscountShopCQ/Commands/UpdateDiscountProduct/UpdateDiscountProductCommandValidator.cs` + +### 9. Bug Fixes + +#### 9.1. Foreign Key Constraint Error +**مشکل**: استفاده از CategoryMultiSelectCombo که Category های عادی را لود می‌کرد، در حالی که DiscountProduct نیاز به DiscountCategory دارد. + +**راه‌حل**: +- برگشت به MudSelect ساده +- لود DiscountCategory از IDiscountCategoryService +- Flatten کردن tree برای نمایش در dropdown + +#### 9.2. GetDiscountProducts NullReferenceException +**مشکل**: `response.MetaData` یا `response.Models` می‌توانست null باشد. + +**راه‌حل**: +```csharp +Products = response.Models?.Select(p => new DiscountProductDto { + Created = p.Created?.ToDateTime() ?? DateTime.MinValue +}).ToList() ?? new List() +``` + +**فایل**: `/home/masoud/Apps/project/FourSat/BackOffice.BFF/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProducts/GetDiscountProductsQueryHandler.cs` + +#### 9.3. GetDiscountCategories Handler Bugs +**مشکل‌ها**: +1. `response.Categories` بدون null check +2. شرط‌های `string.IsNullOrWhiteSpace` معکوس بودند +3. `ParentCategoryId` چک نادرست +4. `node.Children` بدون null check + +**راه‌حل**: +```csharp +Categories = response.Categories?.Select(MapCategory).ToList() + ?? new List() + +// اصلاح شرط‌ها +Description = !string.IsNullOrWhiteSpace(node.Description) ? node.Description : string.Empty, +ParentCategoryId = node.ParentCategoryId != 0 ? node.ParentCategoryId : null, +ChildCategories = node.Children?.Select(MapCategory).ToList() + ?? new List() +``` + +**فایل**: `/home/masoud/Apps/project/FourSat/BackOffice.BFF/src/BackOffice.BFF.Application/DiscountCategoryCQ/Queries/GetDiscountCategories/GetDiscountCategoriesQueryHandler.cs` + +## Build و Deploy + +### Build Status +- ✅ BackOffice: 0 errors, 248 warnings (MudBlazor style warnings only) +- ✅ BackOffice.BFF: 0 errors +- ✅ CMS: 0 errors + +### دستور Build Proto +```bash +bash /home/masoud/Apps/project/FourSat/BackOffice/build-deps.sh +``` + +### فایل‌های نیاز به Deploy +1. **BackOffice.BFF** (stage) + - تغییرات: Validators, Command Handlers, Query Handlers + +2. **CMS** (stage) + - تغییرات: Validators + +3. **BackOffice** (local) + - در حال اجرا با `dotnet watch` + - تغییرات بلافاصله اعمال می‌شود + +## نتیجه نهایی + +### قابلیت‌های جدید: +✅ آپلود تصویر با drag & drop +✅ پیش‌نمایش تصویر قبل از ذخیره +✅ ویرایشگر HTML برای محتوای غنی +✅ انتخاب چندگانه دسته‌بندی با UI بهتر +✅ مدیریت صحیح DiscountCategory ها +✅ حذف الزامی بودن فیلدهای اختیاری +✅ رفع تمام خطاهای NullReference + +### تفاوت با قبل: +| قبل | بعد | +|-----|-----| +| TextField برای مسیر تصویر | آپلود فایل واقعی | +| TextField ساده | HTML Editor | +| CategoryMultiSelectCombo (اشتباه) | MudSelect با DiscountCategory | +| الزامی بودن توضیحات | اختیاری | +| خطای Foreign Key | حل شده | + +## نکات مهم + +1. **موجودی (InitialCount)**: همیشه 0 قرار می‌گیرد - موجودی باید از صفحه Inventory مدیریت شود +2. **دسته‌بندی**: حتماً DiscountCategory استفاده شود نه Category معمولی +3. **Validators**: فیلدهای اختیاری با `.When()` چک شوند +4. **Null Safety**: همیشه `??` و `?.` استفاده شود در LINQ queries + +## مراجع +- Proto Guide: `/home/masoud/Apps/project/FourSat/PROTO-PACKAGING-GUIDE.md` +- Build Script: `/home/masoud/Apps/project/FourSat/BackOffice/build-deps.sh` diff --git a/src/BackOffice/Pages/DiscountShop/Components/ProductFormDialog.razor b/src/BackOffice/Pages/DiscountShop/Components/ProductFormDialog.razor index 3603d68..04ff380 100644 --- a/src/BackOffice/Pages/DiscountShop/Components/ProductFormDialog.razor +++ b/src/BackOffice/Pages/DiscountShop/Components/ProductFormDialog.razor @@ -116,6 +116,7 @@ Variant="Variant.Outlined" Margin="Margin.Dense" MultiSelection="true" + MultiSelectionTextFunc="@(new Func, string>(GetMultiSelectionText))" Disabled="_loading" T="long"> @foreach (var category in _categories) @@ -193,9 +194,11 @@ { var tree = await CategoryService.GetCategoriesAsync(isActive: true); _categories = FlattenCategories(tree); + Console.WriteLine($"Loaded {_categories.Count} categories"); } catch (Exception ex) { + Console.WriteLine($"Error loading categories: {ex}"); Snackbar.Add($"خطا در بارگذاری دسته‌بندی‌ها: {ex.Message}", Severity.Error); } } @@ -226,6 +229,14 @@ return category.Title; } + private string GetMultiSelectionText(List selectedValues) + { + if (selectedValues == null || !selectedValues.Any()) + return "دسته‌بندی انتخاب نشده"; + + return $"{selectedValues.Count} دسته‌بندی انتخاب شده"; + } + private async Task OnMainImageSelected(IBrowserFile? file) { _mainImageFile = file;