# 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`