feat: full BackOffice updates - discount shop, blog, content, UI improvements
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m20s
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m20s
- Blog management pages (posts, categories) - Content management pages (site pages, sections) - Contract management pages - Discount shop improvements (order details, product form, categories) - Wallet management pages - Commission withdrawal details/reject dialogs - Orders hub, sales reports - System hub, global search, breadcrumb - Club features page, club hub - UI/UX improvements across all pages - AppBreadcrumb, GlobalSearch components - NavMenu updates and styling
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,722 @@
|
||||
# 📝 BackOffice — Changelog
|
||||
|
||||
**آخرین بروزرسانی:** بهمن ۱۴۰۴ (February 2026)
|
||||
|
||||
---
|
||||
|
||||
## 🏷 فاز ۱۳ — بهبودهای UI بکآفیس (بهمن ۱۴۰۴)
|
||||
|
||||
> **هدف:** رفع مشکلات بصری، بهبود UX فرمها و ناوبری
|
||||
|
||||
### ۱۳.۱ — فیکس کامپوننت Image (پیشوند `/uploads/`) ✅
|
||||
|
||||
- **مشکل:** تصاویر آپلودشده (بلاگ، صفحات سایت، سکشنها) در بکآفیس نمایش داده نمیشدند — مسیر `GwUrl + Src` با مسیر `UploadsController` هماهنگ نبود
|
||||
- **رفع:** اضافه شدن `/uploads/` بین `FileBaseUrl` و مسیر نسبی تصویر
|
||||
- **فایل:** `BackOffice/Common/BaseComponents/Image.razor.cs`
|
||||
|
||||
### ۱۳.۲ — فیکس فیلتر هفته در گزارشهای هفتگی ✅
|
||||
|
||||
- **مشکل:** فیلتر هفته از `MudTextField` (ورودی متنی) استفاده میکرد — کاربر باید عدد حدس میزد
|
||||
- **رفع:** جایگزینی با `WeekNumberPicker` — انتخاب هفته از لیست با بایندینگ `WeekDefinitionId`
|
||||
- **فایلها:**
|
||||
- `CMS/src/CMSMicroservice.Protobuf/Protos/commission.proto` — تغییر فیلدها از `StringValue` به `Int64Value`
|
||||
- `CMS/.../CommissionCQ/Queries/GetAllWeeklyPools/GetAllWeeklyPoolsQuery.cs` + `Handler.cs`
|
||||
- `BackOffice/Pages/Commission/WeeklyReports.razor`
|
||||
|
||||
### ۱۳.۳ — فیکس دانلود PDF قراردادها ✅
|
||||
|
||||
- **مشکل:** فیلد `SignedPdfFile` محتوای HTML کامل دارد (نه URL فایل PDF) — دکمه دانلود کار نمیکرد
|
||||
- **رفع:** تابع JS جدید `jsHtmlToPdf()` برای باز کردن HTML در پنجره جدید و چاپ/ذخیره بهعنوان PDF
|
||||
- **فایلها:**
|
||||
- `BackOffice/wwwroot/js/main.js` — تابع `jsHtmlToPdf`
|
||||
- `BackOffice/Pages/Contract/UserContractPage.razor`
|
||||
- `BackOffice/Pages/Contract/ContractDetailsDialog.razor`
|
||||
|
||||
### ۱۳.۴ — یکسانسازی ارتفاع ویرایشگر HTML ✅
|
||||
|
||||
- **مشکل:** ارتفاع `MudHtmlEditor` در صفحات مختلف متفاوت بود — برخی خیلی کوتاه
|
||||
- **رفع:** اضافه شدن `MinHeight="300px"` به تمام ۷ ویرایشگر HTML
|
||||
- **فایلها:** `BlogPostEditDialog`, `SitePageSectionEditDialog`, `DiscountShop/ProductFormDialog`, `Package/CreateDialog`, `Package/UpdateDialog`, `Products/CreateDialog`, `Products/UpdateDialog`
|
||||
|
||||
### ۱۳.۵ — تولید خودکار اسلاگ بلاگ ✅
|
||||
|
||||
- **مشکل:** کاربر باید اسلاگ URL را دستی وارد میکرد — غیرضروری و مستعد خطا
|
||||
- **رفع:** حذف فیلد اسلاگ از فرم، تولید خودکار `post-{GUID}` در بکاند (حداکثر ۲۰ کاراکتر)
|
||||
- **فایلها:**
|
||||
- `CMS/.../BlogPostCQ/Commands/CreateBlogPost/CreateBlogPostCommandHandler.cs`
|
||||
- `CMS/.../BlogPostCQ/Commands/UpdateBlogPost/UpdateBlogPostCommandHandler.cs`
|
||||
- `BackOffice/Pages/Blog/Components/BlogPostEditDialog.razor`
|
||||
|
||||
### ۱۳.۶ — انتقال تگها به منوی جداگانه ✅
|
||||
|
||||
- **مشکل:** تگها فقط از تب سوم بلاگ قابل دسترسی بودند — ولی تگها بین محصولات و بلاگ مشترکند
|
||||
- **رفع:** اضافه شدن مسیر `/tags` به `TagManagementPage`، لینک جدید «مدیریت تگها» در منوی ناوبری، حذف تب تگها از `BlogHub`
|
||||
- **فایلها:**
|
||||
- `BackOffice/Pages/Tag/TagManagementPage.razor`
|
||||
- `BackOffice/Shared/NavMenu.razor`
|
||||
- `BackOffice/Pages/Blog/BlogHub.razor`
|
||||
|
||||
### ۱۳.۷ — تولید خودکار کلید سکشن ✅
|
||||
|
||||
- **مشکل:** کاربر باید کلید سکشن صفحه سایت را دستی وارد میکرد (مثلاً `about-us-section`)
|
||||
- **رفع:** فیلد اختیاری شد، تولید خودکار `section-{GUID}` (حداکثر ۲۰ کاراکتر) در هندلر CMS
|
||||
- **فایلها:**
|
||||
- `CMS/.../SitePageCQ/Commands/CreateSitePageSection/CreateSitePageSectionCommandHandler.cs`
|
||||
- `CMS/.../SitePageCQ/Commands/CreateSitePageSection/CreateSitePageSectionCommandValidator.cs`
|
||||
- `BackOffice/Pages/Content/Components/SitePageSectionEditDialog.razor`
|
||||
|
||||
---
|
||||
|
||||
## 🏷 فاز ۱۲ — فیکس فلوی ثبتنام / ورود FrontOffice (بهمن ۱۴۰۴)
|
||||
|
||||
> **هدف:** رفع ۳ باگ بحرانی در فلوی ثبتنام و ورود FrontOffice
|
||||
> **مستند کامل:** [`totalDoc/cms/REGISTRATION-FLOW-FIXES.md`](../../totalDoc/cms/REGISTRATION-FLOW-FIXES.md)
|
||||
|
||||
### ۱۲.۱ — رفع ایجاد کاربر جدید در VerifyOtpToken 🔴 ✅
|
||||
|
||||
- **مشکل:** `UserCQ/VerifyOtpTokenCommandHandler` کاربران جدید ایجاد نمیکرد — فقط خطای «کاربر یافت نشد»
|
||||
- **رفع:** اضافه شدن لاجیک کامل: اعتبارسنجی کد معرف، بررسی ظرفیت درخت باینری (حداکثر ۲ فرزند)، تعیین شاخه (چپ/راست)، ایجاد User + UserRole + UserWallet، رویدادهای دامنه
|
||||
- **فایل:** `CMS/src/CMSMicroservice.Application/UserCQ/Commands/VerifyOtpToken/VerifyOtpTokenCommandHandler.cs`
|
||||
|
||||
### ۱۲.۲ — رفع جستجوی موبایل در AcceptContract 🔴 ✅
|
||||
|
||||
- **مشکل:** `_currentUserService.Username` = `"{FirstName} {LastName}"` — نه شماره موبایل! در نتیجه OTP و کاربر هیچوقت پیدا نمیشد
|
||||
- **رفع:** ابتدا کاربر از `UserId` پیدا شود، سپس `user.Mobile` برای جستجوی OTP استفاده شود
|
||||
- **فایل:** `CMS/src/CMSMicroservice.Application/UserCQ/Commands/AcceptContract/AcceptContractCommandHandler.cs`
|
||||
|
||||
### ۱۲.۳ — رفع `IsCompleteRegister()` در FrontOffice 🟡 ✅
|
||||
|
||||
- **مشکل:** `InitUserAuthInfo().GetAwaiter()` بدون `.GetResult()` — عملیات async اجرا نمیشود، منوی کناری وضعیت نادرست
|
||||
- **رفع:** تغییر به `InitUserAuthInfo().GetAwaiter().GetResult()`
|
||||
- **فایل:** `FrontOffice/src/FrontOffice.Main/Utilities/AuthService.cs`
|
||||
|
||||
---
|
||||
|
||||
## 🏷 فاز ۱۱ — رفع خطای صفحات + سرویس عمومی تصاویر (بهمن ۱۴۰۴)
|
||||
|
||||
> **هدف:** رفع خطای ۳ صفحه BackOffice + ایجاد اندپوینت HTTP عمومی برای سرو تصاویر
|
||||
|
||||
### ۱۱.۱ — سرویس عمومی سرو تصاویر (CMS) ✅
|
||||
|
||||
- **`UploadsController`** — اندپوینت `GET /uploads/{path}` برای سرو مستقیم فایلها
|
||||
- `[AllowAnonymous]` — بدون نیاز به احراز هویت
|
||||
- کش مرورگر ۲۴ ساعته (`ResponseCache`)
|
||||
- پشتیبانی از Range Requests
|
||||
- **FMS Fallback:** اگر فایل محلی نباشد، از `dl.afrino.co` دانلود و کش میشود
|
||||
- `IHttpClientFactory` با named client `"FMS"` (timeout: 30s)
|
||||
- **فایلها:** `CMS/WebApi/Controllers/UploadsController.cs`, `CMS/WebApi/Program.cs`
|
||||
|
||||
### ۱۱.۲ — رفع صفحه «صفحات سایت» ✅
|
||||
|
||||
- اضافه شدن **دکمه «افزودن صفحه جدید»** به بالای صفحه
|
||||
- اضافه شدن **دکمه «حذف»** (آیکون قرمز) برای هر ردیف
|
||||
- ایجاد **`SitePageCreateDialog`** — دیالوگ ایجاد صفحه جدید با فیلدها:
|
||||
- کلید صفحه (PageKey)، عنوان، توضیحات متا، عنوان هیرو، زیرعنوان هیرو، تصویر هیرو، وضعیت
|
||||
- Proto: اضافه شدن `CreateSitePage` و `DeleteSitePage` RPC
|
||||
- CQRS: `CreateSitePageCommand/Handler` و `DeleteSitePageCommand/Handler`
|
||||
- **فایلها:**
|
||||
- `sitepage.proto` — 2 RPC جدید + پیامهای Request/Response
|
||||
- `SitePageCQ/Commands/CreateSitePage/` — Command + Handler
|
||||
- `SitePageCQ/Commands/DeleteSitePage/` — Command + Handler
|
||||
- `CMS/WebApi/Services/SitePageService.cs` — 2 override جدید
|
||||
- `BackOffice/Services/Content/ISitePageService.cs` — `CreateAsync` + `DeleteAsync` + `SitePageCreateDto`
|
||||
- `BackOffice/Services/Content/SitePageService.cs` — پیادهسازی Create/Delete
|
||||
- `BackOffice/Pages/Content/SitePageManagementPage.razor` — دکمههای Add/Delete
|
||||
- `BackOffice/Pages/Content/SitePageManagementPage.razor.cs` — متدهای CreatePage/DeletePage
|
||||
- `BackOffice/Pages/Content/Components/SitePageCreateDialog.razor` + `.razor.cs`
|
||||
|
||||
### ۱۱.۳ — رفع صفحه «قراردادها» (Contracts) ✅
|
||||
|
||||
- **مشکل:** صفحه `/contracts` اصلاً بارگذاری نمیشد — crash در DI
|
||||
- **علت:** `UserContractContract.UserContractContractClient` در DI ثبت نشده بود
|
||||
- **رفع:** اضافه شدن `using CMSMicroservice.Protobuf.Protos.UserContract` و ثبت gRPC client
|
||||
- **فایل:** `BackOffice/Common/Configure/ConfigureService.cs`
|
||||
|
||||
### ۱۱.۴ — رفع صفحه «کیف پول کاربران» (Wallets) ✅
|
||||
|
||||
- **مشکل:** صفحه `/wallets` اصلاً بارگذاری نمیشد — crash در DI
|
||||
- **علت:** دو سرویس ثبت نشده بود:
|
||||
- `UserWalletContract.UserWalletContractClient`
|
||||
- `UserWalletChangeLogContract.UserWalletChangeLogContractClient`
|
||||
- **رفع:** اضافه شدن usingها و ثبت هر دو gRPC client
|
||||
- **فایل:** `BackOffice/Common/Configure/ConfigureService.cs`
|
||||
|
||||
---
|
||||
|
||||
## 🏷 فاز ۱۰ — بهبود مدیریت محتوا: بلاگ + صفحات سایت (بهمن ۱۴۰۴)
|
||||
|
||||
> **هدف:** فعالسازی آپلود تصویر باینری و ادیتور HTML در صفحات بلاگ و صفحات سایت
|
||||
|
||||
### ۱۰.۱ — بهبود ادیتور پست بلاگ ✅
|
||||
|
||||
- **`MudTextField Lines="10"`** → **`MudHtmlEditor`** برای ویرایش محتوای HTML
|
||||
- **فیلد متنی مسیر تصویر** → **`MudFileUpload`** با پیشنمایش تصویر (`Image` component)
|
||||
- حذف فیلد `FeaturedImageThumbnailPath` (تولید خودکار توسط بکاند)
|
||||
- اضافه شدن `MudForm` با اعتبارسنجی + حالت بارگذاری (`_loading`)
|
||||
- مدیریت خطا: try-catch + Snackbar
|
||||
- **فایلها:** `Pages/Blog/Components/BlogPostEditDialog.razor` + `.razor.cs`
|
||||
|
||||
### ۱۰.۲ — بهبود ادیتور صفحه سایت ✅
|
||||
|
||||
- **فیلد متنی Hero Image** → **`MudFileUpload`** با پیشنمایش تصویر
|
||||
- اضافه شدن `MudForm` + حالت آپلود + مدیریت خطا
|
||||
- **فایلها:** `Pages/Content/Components/SitePageEditDialog.razor` + `.razor.cs`
|
||||
|
||||
### ۱۰.۳ — بهبود ادیتور بخش صفحه سایت (Section) ✅
|
||||
|
||||
- **`MudTextField Lines="8"`** → **`MudHtmlEditor`** برای ویرایش محتوای HTML بخشها
|
||||
- **فیلدهای متنی تصویر/thumbnail** → **`MudFileUpload`** با پیشنمایش
|
||||
- اضافه شدن `MudForm` + حالت آپلود + مدیریت خطا
|
||||
- **فایلها:** `Pages/Content/Components/SitePageSectionEditDialog.razor` + `.razor.cs`
|
||||
|
||||
### ۱۰.۴ — تغییرات Proto ✅
|
||||
|
||||
| Proto | تغییر |
|
||||
|-------|-------|
|
||||
| `blogpost.proto` | اضافه شدن `BlogImageFileModel` + فیلد `image_file` در Create/Update |
|
||||
| `sitepage.proto` | اضافه شدن `SitePageImageFileModel` + فیلد `image_file` در Update/CreateSection/UpdateSection |
|
||||
|
||||
### ۱۰.۵ — تغییرات CQRS (بکاند CMS) ✅
|
||||
|
||||
- **Commands:** اضافه شدن `ImageFileBytes`, `ImageMime`, `ImageFileName` به:
|
||||
- `CreateBlogPostCommand`, `UpdateBlogPostCommand`
|
||||
- `UpdateSitePageCommand`, `CreateSitePageSectionCommand`, `UpdateSitePageSectionCommand`
|
||||
- **Handlers:** تزریق `IFileManager` + فراخوانی `UploadImageAsync`:
|
||||
- BlogPost → `Images/BlogPosts/`
|
||||
- SitePage → `Images/SitePages/`
|
||||
- SitePageSection → `Images/SitePageSections/`
|
||||
|
||||
### ۱۰.۶ — تغییرات سرویسهای BackOffice ✅
|
||||
|
||||
- `BlogPostService.cs` — ارسال `BlogImageFileModel` با `ByteString.CopyFrom(dto.ImageFile)`
|
||||
- `SitePageService.cs` — ارسال `SitePageImageFileModel` در Update/CreateSection/UpdateSection
|
||||
- **DTOs:** اضافه شدن `ImageFile`, `ImageMime`, `ImageFileName` به `BlogPostEditDto`, `SitePageEditDto`, `SitePageSectionEditDto`
|
||||
|
||||
### خلاصه آمار فاز ۱۰
|
||||
| معیار | تعداد |
|
||||
|---|---|
|
||||
| فایلهای proto تغییریافته | 2 |
|
||||
| Commandهای بروزشده | 5 |
|
||||
| Handlerهای بروزشده | 5 |
|
||||
| سرویسهای gRPC تغییریافته | 2 (BlogPostService + SitePageService) |
|
||||
| دیالوگهای UI بازنویسیشده | 3 (BlogPost, SitePage, SitePageSection) |
|
||||
| سرویسهای BackOffice بروزشده | 2 |
|
||||
| DTOهای بروزشده | 3 |
|
||||
| Build | 0 Error ✅ (هر ۳ پروژه) |
|
||||
|
||||
---
|
||||
|
||||
## 🏷 فاز ۹ — معماری مدیریت فایل + رفع سفارشات + مهاجرت DB (بهمن ۱۴۰۴)
|
||||
|
||||
> **مستند کامل معماری فایل:** [`totalDoc/cms/FILE-MANAGEMENT-ARCHITECTURE.md`](../../totalDoc/cms/FILE-MANAGEMENT-ARCHITECTURE.md)
|
||||
|
||||
### ۹.۱ — حذف کد مرده FMS ✅
|
||||
- حذف ۳ فایل غیرقابل استفاده مرتبط با FMS خارجی (`https://dl.afrino.co`):
|
||||
|
||||
| فایل حذفشده | شرح |
|
||||
|-------------|------|
|
||||
| `Infrastructure/Services/FmsFileManager.cs` | پیادهسازی HTTP upload به FMS |
|
||||
| `Application/Common/FileManager/FileManagementService.cs` | سرویس قدیمی مدیریت فایل |
|
||||
| `Application/Common/FileManager/IFileManagementService.cs` | اینترفیس قدیمی |
|
||||
|
||||
### ۹.۲ — رفع صفحه سفارشات (Orders) ✅
|
||||
|
||||
**مشکل ۱ — صفحه خالی (بدون پیغام خطا)**
|
||||
- **علت:** `ServerReload` بدون try-catch — exceptionهای gRPC ساکت میشدند
|
||||
- **رفع:** اضافه شدن try-catch + نمایش خطا با Snackbar
|
||||
- **فایل:** `Pages/UserOrder/UserOrderMainPage.razor.cs`
|
||||
|
||||
**مشکل ۲ — Shadow FK تکراری در OrderVAT**
|
||||
- **علت:** `.WithOne()` بدون inverse navigation → EF Core یک shadow FK اضافی میساخت
|
||||
- **رفع:** `.WithOne()` → `.WithOne(x => x.OrderVAT)`
|
||||
- **فایل:** `Persistence/Configurations/OrderVATConfiguration.cs`
|
||||
|
||||
**مشکل ۳ — فیلترهای oneof همیشه فعال**
|
||||
- **علت:** فیلدهای proto `oneof` مقدار پیشفرض enum دارند (هیچوقت null نیستند)، پس `PaymentStatus != null` همیشه true بود
|
||||
- **رفع:** `PaymentStatus != null` → `HasPaymentStatus == true` (و مشابه برای DeliveryStatus و PaymentMethod)
|
||||
- **فایلها:** `UserOrderService.cs` — هر دو متد `GetAllUserOrderByFilter` و `GetCustomerOrders`
|
||||
|
||||
### ۹.۳ — مهاجرت DB: حذف محدودیت طول ستونهای تصویر ✅
|
||||
|
||||
- **علت:** data URIهای base64 بزرگتر از `nvarchar(500)` بودند → truncation خاموش
|
||||
- **رفع:** حذف `HasMaxLength(500)` از تمام ستونهای image path
|
||||
- **Migration:** `RemoveImagePathMaxLength`
|
||||
|
||||
| Entity | ستونها |
|
||||
|--------|---------|
|
||||
| Product | `ImagePath`, `ImageThumbnailPath` |
|
||||
| DiscountProduct | `ImagePath`, `ImageThumbnailPath` |
|
||||
| Category | `ImagePath` |
|
||||
| DiscountCategory | `ImagePath` |
|
||||
| BlogPost | `FeaturedImagePath`, `FeaturedImageThumbnailPath` |
|
||||
| SitePage | `HeroImagePath` |
|
||||
| SitePageSection | `ImagePath`, `ImageThumbnailPath` |
|
||||
|
||||
### ۹.۴ — بازنویسی کامل مدیریت فایل: ذخیره دیسکی ✅
|
||||
|
||||
**معماری قبلی:** ذخیره data URI مستقیم در DB (ناکارآمد + حجم بالا)
|
||||
|
||||
**معماری جدید:**
|
||||
1. فایلها در دیسک ذخیره میشوند: `Uploads/Images/{folder}/{guid}.jpg`
|
||||
2. مسیر نسبی در DB ذخیره میشود
|
||||
3. `ImagePathResolverInterceptor` هنگام serve تبدیل به base64 میکند
|
||||
|
||||
**فایلهای اصلی:**
|
||||
|
||||
| فایل | شرح |
|
||||
|------|------|
|
||||
| `Application/Common/FileManager/IFileManager.cs` | اینترفیس: Upload, UploadImage, Delete, ResolveImageUrl |
|
||||
| `Infrastructure/Services/LocalFileManager.cs` | پیادهسازی: ImageSharp, main 1200×1200 + thumb 300×300, JPEG Q75 |
|
||||
| `WebApi/Interceptors/ImagePathResolverInterceptor.cs` | gRPC Interceptor: walk بازگشتی response → تبدیل path به data URI |
|
||||
|
||||
**تنظیمات:**
|
||||
- `FileStorage:UploadPath` → پوشه ریشه آپلود (پیشفرض: `AppContext.BaseDirectory/Uploads`)
|
||||
- gRPC MaxReceiveMessageSize: 50MB
|
||||
|
||||
### ۹.۵ — پاکسازی LoggingBehaviour ✅
|
||||
|
||||
- **قبل:** فیلدهای باینری (byte[] تصاویر) کامل در لاگ چاپ میشدند → لاگهای چند مگابایتی
|
||||
- **بعد:**
|
||||
- فرمت: `JsonFormatter.Default.Format()` به جای `{@Request}`
|
||||
- Regex: حذف فیلدهای باینری (`File`, `ImageFile`, `image_file`, `file`)
|
||||
- محدودیت: حداکثر 2000 کاراکتر
|
||||
- **فایل:** `WebApi/Common/Behaviours/LoggingBehaviour.cs`
|
||||
|
||||
### خلاصه آمار فاز ۹
|
||||
| معیار | تعداد |
|
||||
|---|---|
|
||||
| فایلهای حذفشده (کد مرده) | 3 |
|
||||
| فایلهای جدید | 2 (LocalFileManager بازنویسی + ImagePathResolverInterceptor) |
|
||||
| فایلهای ویرایششده | ~12 |
|
||||
| باگهای رفعشده | 4 (Orders page ×3 + DB truncation) |
|
||||
| Migrationهای جدید | 1 (RemoveImagePathMaxLength) |
|
||||
| ستونهای DB تغییریافته | 9 (nvarchar(500) → nvarchar(max)) |
|
||||
| Build | 0 Error ✅ (هر ۳ پروژه) |
|
||||
|
||||
---
|
||||
|
||||
## 🏷 فاز ۸ — یکسانسازی فروشگاه عادی و تخفیفی (بهمن ۱۴۰۴)
|
||||
|
||||
> **مستند کامل:** [`totalDoc/ui-modernization/BACKOFFICE-STORE-UNIFICATION.md`](../../totalDoc/ui-modernization/BACKOFFICE-STORE-UNIFICATION.md)
|
||||
|
||||
### ۸.۱ — بازسازی NavMenu ✅
|
||||
- ایجاد دو NavGroup مجزا: «فروشگاه عادی» و «فروشگاه تخفیفی»
|
||||
- حذف «ویرایش دستهجمعی» از منو
|
||||
- انبارداری → گروه مستقل | پکیجها → آیتم مستقل
|
||||
- **فایل:** `Shared/NavMenu.razor`
|
||||
|
||||
### ۸.۲ — حذف گزارشهای کوچک از سفارشات ✅
|
||||
- حذف کارتهای آماری (TotalOrders + TotalAmount) و نمودار Bar ارسال
|
||||
- حذف فیلدها: `_stats`, `_statusChartLabels`, `_statusChartSeries`
|
||||
- حذف متد `UpdateStats()` و کلاس `OrderStatsViewModel`
|
||||
- عنوان: «سفارشهای کاربر» → «لیست سفارشات»
|
||||
- **فایلها:** `Pages/UserOrder/UserOrderMainPage.razor` + `.razor.cs`
|
||||
|
||||
### ۸.۳ — رفع باگ لیست خالی سفارشات ✅
|
||||
- **علت:** `PaymentDate.ToDateTime()` بدون null check → exception ساکت در WASM
|
||||
- **رفع:** `@if (context.Item.PaymentDate != null)` + fallback "-"
|
||||
- **فایل:** `Pages/UserOrder/UserOrderMainPage.razor`
|
||||
|
||||
### ۸.۴ — بازنویسی صفحه محصولات تخفیفی ✅
|
||||
- تبدیل به `BasePageComponent` (مطابق فروشگاه عادی)
|
||||
- فیلترهای جدید: وضعیت + موجودی
|
||||
- ستونهای جدید: تصویر inline + عنوان truncate + چیپ رنگی موجودی + چیپ وضعیت
|
||||
- **فایلها:** `Pages/DiscountShop/DiscountProductsMainPage.razor` + `.razor.cs` (بازنویسی کامل)
|
||||
|
||||
### ۸.۵ — بازنویسی صفحه دستهبندیهای تخفیفی ✅
|
||||
- تبدیل به `BasePageComponent` (مطابق فروشگاه عادی)
|
||||
- ستونهای جدید: نام لاتین، دستهبندی والد (resolve شده)، تعداد محصولات، ترتیب
|
||||
- code-behind جدید (`@code` درونخطی → `.razor.cs`)
|
||||
- **فایلها:** `Pages/DiscountShop/DiscountCategoriesMainPage.razor` + `.razor.cs` (بازنویسی کامل)
|
||||
|
||||
### خلاصه آمار فاز ۸
|
||||
| معیار | تعداد |
|
||||
|---|---|
|
||||
| فایلهای بازنویسیشده | 4 |
|
||||
| فایلهای ویرایششده | 3 |
|
||||
| باگهای رفعشده | 2 (PaymentDate null + لیست خالی) |
|
||||
| Build | 0 Error ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 🏷 فاز ۷ — پس از اجرای Audit v7.0 (تیر ۱۴۰۴)
|
||||
|
||||
### خلاصه تغییرات
|
||||
- **۵۷ مورد** از ۵۸ یافته audit اجرا شد (≈98%)
|
||||
- **تمام ۱۶ مورد بحرانی 🔴** حل شد
|
||||
- **تمام ۱۸ مورد جزئی 🟢** حل شد
|
||||
- **فازهای ۱ تا ۶ تکمیل شدند** (بهجز DayaLoan و Audit Trail که نیاز به بکاند دارند)
|
||||
- **فاز ۷** — Global Search, Notification Bell, NavMenu Badge, Export Excel 4 صفحه جدید
|
||||
- **۱۳+ فایل جدید** ساخته شد
|
||||
- **۹ صفحه** ادغام شدند (۱۴ صفحه → ۵ صفحه)
|
||||
- **Build**: 0 error ✅
|
||||
|
||||
---
|
||||
|
||||
## فاز ۷ — Global Search + Notifications + Export All + NavMenu Badge (v7.0)
|
||||
|
||||
### 6.5 — Badge Pending در NavMenu — ✅
|
||||
- **`NavMenu.razor`**: اضافه شدن `MudBadge` روی لینک «درخواستهای برداشت»
|
||||
- نمایش تعداد درخواستهای Pending بصورت real-time از gRPC (`CommissionContract.GetWithdrawalRequestsAsync` با `Status=0`)
|
||||
- fire-and-forget در `OnInitializedAsync` با try/catch silent
|
||||
|
||||
### 6.2 — Global Search در AppBar — ✅
|
||||
- **فایل جدید**: `Shared/GlobalSearch.razor`
|
||||
- `MudAutocomplete<SearchResultItem>` با Debounce 400ms
|
||||
- دو نوع جستجو: ۱) نام صفحات (۱۵ صفحه اصلی) ۲) کاربران از gRPC (`UserContract.GetAllUserByFilterAsync`)
|
||||
- Template سفارشی با آیکون + عنوان + زیرنویس
|
||||
- ناوبری خودکار با `Navigation.NavigateTo`
|
||||
- اضافه شده در `MainLayout.razor` داخل `<AuthorizeView>`
|
||||
- CSS در `app.css` — rounded border-radius 24px, compact padding
|
||||
|
||||
### 6.1 — Notification Bell (placeholder) — ✅
|
||||
- **`MainLayout.razor`**: اضافه شدن `MudMenu` + `MudBadge` (Dot) + آیکون Notifications
|
||||
- Dropdown با هدر «اعلانها» + placeholder «اعلان جدیدی وجود ندارد»
|
||||
- آماده اتصال به بکاند — فقط `Visible=true` و populate items
|
||||
|
||||
### 6.3 — Export Excel صفحات جدید — ✅ (۱۰ صفحه کامل)
|
||||
- **۴ صفحه جدید** اضافه شدند به ۶ صفحه قبلی:
|
||||
1. **UserMainPage** — خروجی CSV: شناسه, موبایل, نام, نام خانوادگی, کدملی
|
||||
2. **DiscountOrdersMainPage** — خروجی CSV: شماره سفارش, تاریخ, مبلغ کل, تخفیف, قابل پرداخت, تعداد, وضعیت, پرداخت
|
||||
3. **ManualPayments** — خروجی CSV: شناسه, شناسه کاربر, نام کاربر, مبلغ, نوع, وضعیت, تاریخ
|
||||
4. **InventoryMainPage** — خروجی CSV: شناسه, محصول, انبار, موجودی, رزرو, قابل فروش, حد هشدار, وضعیت
|
||||
- هر ۴ صفحه: دکمه «خروجی Excel» + `ExportToExcel()` + `EscapeCsv()` + UTF-8 BOM + `jsSaveAsFile`
|
||||
- فایلهای code-behind جدید: `ManualPayments.razor.cs`, `DiscountOrdersMainPage.razor.cs`
|
||||
|
||||
---
|
||||
|
||||
## فاز ۶ — تاریخ شمسی + تاریخچهها + Export Excel (v6.0)
|
||||
|
||||
### تاریخ شمسی یکپارچه (3.6) — ✅ کامل
|
||||
- **۱۹+ فایل** آپدیت شدند: تمام تاریخها از فرمت میلادی (`yyyy/MM/dd`) به شمسی تبدیل شدند
|
||||
- از extension methodهای `DateTimeConverterCL` استفاده شد:
|
||||
- `.MiladiToJalali()` — فرمت تاریخ فقط (مثال: ۱۴۰۴/۰۴/۱۲)
|
||||
- `.MiladiToJalaliWithTime()` — فرمت تاریخ و ساعت
|
||||
- **صفحات اصلاحشده شامل**: Club (Members, Statistics, Features, MemberDetailsDialog)، Commission (Dashboard, WeeklyReports, WithdrawalRequests, Payouts, WithdrawalReports)، Network (UserNetworkInfo, BalancesReport, Statistics, NetworkTreeViewer)، UserOrder, Products, Package, Inventory, DiscountShop, Blog, Content, و غیره
|
||||
- `PropertyColumn Format="yyyy/MM/dd"` → `TemplateColumn` با `.MiladiToJalali()`
|
||||
- `MudDatePicker DateFormat` تغییر نکرد (قابلیت تقویم شمسی نیاز به کتابخانه جداگانه دارد)
|
||||
|
||||
### تاریخچه عضویت باشگاه (5.7) — ✅
|
||||
- **`MemberDetailsDialog.razor`**: تب تاریخچه با فیلدهای صحیح proto:
|
||||
- ستونها: `Action`, `Created`, `PackageName`, `Reason`, `PerformedBy`
|
||||
- Request: `PageIndex = 0` (نه PageNumber)
|
||||
|
||||
### تاریخچه تغییرات شبکه (5.8) — ✅
|
||||
- **`UserNetworkInfo.razor`**: تب تاریخچه با فیلدهای تخصصی:
|
||||
- ستونها: `Action`, `Created`, `OldParentId → NewParentId`, `OldNetworkLeg → NewNetworkLeg` (چپ/راست)
|
||||
- ستون `Reason` و `PerformedBy` اضافه شد
|
||||
- Request: `PageIndex = 0`
|
||||
|
||||
### لاگ اجرای Workerها (5.6) — ✅
|
||||
- در تب WorkerControl از SystemHub قبلاً پیادهسازی شده بود
|
||||
|
||||
### Export Excel / CSV (6.3) — ✅ ۶ صفحه
|
||||
قابلیت خروجی CSV (سازگار با Excel) به ۶ صفحه اصلی اضافه شد:
|
||||
|
||||
| صفحه | فایل(ها) | ستونهای خروجی |
|
||||
|---|---|---|
|
||||
| **Products** (قبلاً موجود) | ProductsMainPage.razor.cs | — |
|
||||
| **Club Members** | ClubMembers.razor + .razor.cs | شناسه,کاربر,نام,پکیج,کد فعالسازی,تاریخ فعالسازی,تاریخ انقضا,فعال,منقضی |
|
||||
| **Withdrawal Requests** | WithdrawalRequests.razor + .razor.cs | شناسه,کاربر,نام,مبلغ,وضعیت,روش,شبا,تاریخ درخواست,تاریخ پردازش,مرجع بانکی |
|
||||
| **Weekly Reports** | WeeklyReports.razor + .razor.cs | هفته,مبلغ استخر,موجودیها,ارزش,وضعیت,تاریخ محاسبه |
|
||||
| **User Orders** | UserOrderMainPage.razor + .razor.cs | شناسه,کاربر,نام,مبلغ,وضعیت پرداخت,وضعیت ارسال,روش پرداخت,تاریخ |
|
||||
| **Stock Movements** | MovementsPage.razor + .razor.cs | شناسه,محصول,نوع,تعداد,قبل,بعد,مرجع,یادداشت,تاریخ |
|
||||
|
||||
**الگوی پیادهسازی:**
|
||||
- دکمه `<MudButton>` با آیکون `FileDownload` در ToolBarContent
|
||||
- `StringBuilder` → CSV با هدرهای فارسی
|
||||
- UTF-8 BOM (`Encoding.UTF8.GetPreamble()`) برای نمایش صحیح فارسی در Excel
|
||||
- `jsSaveAsFile(filename, base64)` از `wwwroot/js/main.js`
|
||||
- متد `EscapeCsv()` برای مقادیر حاوی کاما/کوتیشن
|
||||
- تاریخها در خروجی نیز شمسی هستند
|
||||
|
||||
### فایلهای جدید (این نسخه)
|
||||
|
||||
| فایل | عملکرد |
|
||||
|---|---|
|
||||
| `Pages/Commission/WeeklyReports.razor.cs` | Code-behind برای export (بهدلیل محدودیت Razor parser با escaped quotes) |
|
||||
|
||||
---
|
||||
|
||||
## نسخه قبلی — Audit v5.0 (خرداد ۱۴۰۴)
|
||||
|
||||
### تم رنگی یکپارچه
|
||||
- **`CustomMudTheme.cs`**: تم Primary از `#0380C0` به `#6366f1` (Indigo) تغییر کرد — هماهنگ با FrontOffice
|
||||
- **`PaletteDark`** اضافه شد: Surface `#1a1a2e`, Background `#16213e`, AppbarBackground `#0f3460`
|
||||
- **`app.css`**: رنگ loading از `#1b6ec2` به `var(--mud-palette-primary)` تغییر کرد
|
||||
- **`NavMenu.razor.css`**: رنگ active از `#0380C0` هاردکد به CSS variable تغییر کرد
|
||||
- **`MainLayout.razor`**: حذف inline style `border: 1px solid #5e4df9`
|
||||
|
||||
### Dark Mode Toggle
|
||||
- آیکون ماه/آفتاب در AppBar اضافه شد
|
||||
- `_isDarkMode` state با `MudThemeProvider` اتصال دارد
|
||||
- تعویض بین `PaletteLight` و `PaletteDark` در CustomMudTheme
|
||||
|
||||
### عنوان اپلیکیشن
|
||||
- "پنل مدیریت **فرصت**" → "پنل مدیریت **کارا بازار سلامت**"
|
||||
|
||||
---
|
||||
|
||||
## فاز ۲ — بازطراحی لایوت صفحات
|
||||
|
||||
### BasePageComponent بازنویسی کامل
|
||||
- **قبل**: فیلتر ثابت سمت راست (20%) + جدول (80%)، ارتفاع 85vh هاردکد
|
||||
- **بعد**: فیلتر collapsible بالای جدول، فیلدها در `MudGrid` با `MudItem`، responsive
|
||||
- **۱۳ صفحه** آپدیت شدند: User, Products, Package, Category, UserOrder, UserAddress, UserRole, Role, BlogPost, BlogCategory, Tag, Transactions, Content
|
||||
|
||||
### DataGrid Height یکسانسازی
|
||||
- همه DataGridها: `Height="calc(100vh - 240px)"`
|
||||
- **صفحات اصلاحشده**: تمام ~33 صفحه دارای DataGrid
|
||||
|
||||
### Pager یکسانسازی
|
||||
- **PageSize**: همه `20, 50, 100`
|
||||
- **متن فارسی**: `RowsPerPageString="تعداد در صفحه"` و `InfoFormat="سطر {first_item} تا {last_item} از {all_items}"`
|
||||
|
||||
### MaxWidth یکسانسازی
|
||||
- `MainLayout.razor` MudContainer wrapper اضافه شد → MaxWidth یکسان برای همه صفحات
|
||||
- **۳۱ صفحه**: حذف `MudContainer` تکراری (double nesting)
|
||||
|
||||
### Breadcrumb
|
||||
- **`Shared/AppBreadcrumb.razor`** ساخته شد
|
||||
- جداکننده چپبهراست (`ChevronLeft`)
|
||||
- نقشه فارسی URL segments (60+ مسیر)
|
||||
- آخرین آیتم disabled (صفحه فعلی)
|
||||
|
||||
### Login + VerifyCode بازطراحی
|
||||
- عرض ثابت 35% → responsive `MaxWidth.Small`
|
||||
- لوگو + برندینگ "کارا بازار سلامت" اضافه شد
|
||||
- طراحی gradient background
|
||||
|
||||
---
|
||||
|
||||
## فاز ۲.۵ — پاکسازی ساختاری
|
||||
|
||||
### NavMenu بازسازی کامل
|
||||
- **لینک مرده `categories-dragdrop`**: حذف شد
|
||||
- **Transactions**: حذف از NavMenu (صفحه stub خالی)
|
||||
- **AlertsMonitoring**: حذف از NavMenu (100% mock)
|
||||
- **WorkerControl**: به SystemHub منتقل شد (قبلاً لینک نداشت)
|
||||
- **«پیامهای عمومی»**: از بخش فروشگاه تخفیفی به «مدیریت محتوا» منتقل شد
|
||||
- **ساختار گروهبندی** اصلاح شد:
|
||||
- کمیسیون و شبکه: NavGroup واضح
|
||||
- باشگاه مشتریان: اعضا و آمار + فیچرها
|
||||
- فروشگاه: پکیجها، محصولات (group)، سفارشها، فروشگاه تخفیفی (group)
|
||||
- مدیریت: کاربران، نقشها، پرداخت دستی، **کیفپول (جدید)**، **قراردادها (جدید)**
|
||||
- محتوا: بلاگ، صفحات سایت، پیامهای عمومی
|
||||
- سیستم: مدیریت سیستم، نسخه اپها
|
||||
|
||||
---
|
||||
|
||||
## فاز ۳ — UX بهبودها
|
||||
|
||||
### NoRecordsContent (Empty State)
|
||||
- **۳۳ DataGrid** آپدیت شدند
|
||||
- پیام: `<MudAlert Severity="Info">موردی یافت نشد.</MudAlert>`
|
||||
|
||||
### Delete Confirmation با نام آیتم
|
||||
- **۱۱ صفحه** اصلاح شدند:
|
||||
- Role → `«{model.Title}»`
|
||||
- User → `«{model.FirstName} {model.LastName}»`
|
||||
- Package → `«{model.Title}»`
|
||||
- UserAddress → `«{model.Title}»`
|
||||
- UserRole → `شناسه «{model.Id}»`
|
||||
- UserOrder → `شماره «{model.Id}»`
|
||||
- Category → `«{model.Title}»`
|
||||
- Products → `«{model.Title}»`
|
||||
- DiscountCategories → `«{category?.Title}»` (lookup اضافه شد)
|
||||
- DiscountProducts → `«{product?.Title}»` (lookup اضافه شد)
|
||||
- PublicMessages → `«{message?.Title}»` (lookup اضافه شد)
|
||||
|
||||
### User Menu در AppBar
|
||||
- `MudMenu` جایگزین متن ساده شماره موبایل شد
|
||||
- آیتمها: نام کاربر (غیرفعال) + خروج
|
||||
- آیکون `AccountCircle`
|
||||
- خروج (`signout`) از NavMenu به AppBar منتقل شد
|
||||
|
||||
---
|
||||
|
||||
## فاز ۳.۵ — ادغام صفحات
|
||||
|
||||
### Dashboard Merge ← `Pages/Index.razor`
|
||||
- **`Index.razor` + `SystemOverview.razor`** ادغام شدند
|
||||
- Routes: `/` + `/dashboard/overview`
|
||||
- بخشها:
|
||||
- 6 کارت آمار CMS (کاربران، محصولات، پکیجها، دستهبندیها، سفارشها، مبلغ پرداختشده)
|
||||
- آمار کمیسیون (هفته جاری، استخر، موجودیها، وضعیت محاسبه)
|
||||
- آمار باشگاه (کل/فعال/غیرفعال)
|
||||
- جدول آخرین ۵ سفارش پرداختشده
|
||||
- ۶ دکمه Quick Action
|
||||
- بارگذاری موازی با `Task.WhenAll` + `.ResponseAsync`
|
||||
- `SystemOverview.razor` **حذف شد**
|
||||
|
||||
### DiscountShopHub ← `Pages/DiscountShop/DiscountShopHub.razor`
|
||||
- Routes: `/discount-shop`, `/discount-orders`, `/discount-sales-reports`
|
||||
- ۲ تب: DiscountOrdersMainPage + SalesReports
|
||||
- انتخاب تب از URL
|
||||
|
||||
### ClubHub ← `Pages/Club/ClubHub.razor`
|
||||
- Routes: `/club`, `/club/members`, `/club/statistics`
|
||||
- ۲ تب: ClubMembers + Statistics
|
||||
|
||||
### BlogHub ← `Pages/Blog/BlogHub.razor`
|
||||
- Routes: `/blog`, `/blog/posts`, `/blog/categories`, `/tags`
|
||||
- ۳ تب: BlogPostManagementPage + BlogCategoryManagementPage + TagManagementPage
|
||||
|
||||
### SystemHub ← `Pages/SystemManagement/SystemHub.razor`
|
||||
- Routes: `/system`, `/system/configuration`, `/system/worker-control`, `/system/health`
|
||||
- ۳ تب: Configuration + WorkerControl + HealthDashboard
|
||||
|
||||
### ۱۰ صفحه — `@page` directive حذف شد
|
||||
صفحاتی که به hub منتقل شدند دیگر route مستقل ندارند:
|
||||
DiscountOrdersMainPage, SalesReports, ClubMembers, Statistics, BlogPostManagementPage, BlogCategoryManagementPage, TagManagementPage, Configuration, WorkerControl, HealthDashboard
|
||||
|
||||
---
|
||||
|
||||
## فاز ۴ — فیکسهای بیزینسی
|
||||
|
||||
### Dashboard آمار اصلاح شد
|
||||
- **قبل**: آمار از ۱۰ رکورد اول محاسبه میشد (نادرست)
|
||||
- **بعد**: از `metadata.TotalCount` و APIهای جداگانه استفاده میشود
|
||||
|
||||
### ManualPayments — تأیید/رد
|
||||
- دکمههای Approve/Reject وقتی `Status==0` (در انتظار) نمایش داده میشوند
|
||||
- Confirmation dialog قبل از اجرا
|
||||
- از `ManualPaymentId` (نه `Id`) استفاده میشود
|
||||
|
||||
### UserOrder — فیلتر Cancelled
|
||||
- `<MudSelectItem Value="@(5)">لغو شده</MudSelectItem>` اضافه شد
|
||||
|
||||
### WithdrawalRequests — بهبود کامل
|
||||
- **Pager**: 10/25/50/100 → 20/50/100
|
||||
- **`WithdrawalDetailsDialog.razor`** (جدید): نمایش کامل اطلاعات کاربر، درخواست، و بانکی
|
||||
- **`RejectReasonDialog.razor`** (جدید): ورود دلیل رد با اعتبارسنجی
|
||||
- ViewDetails: از Snackbar TODO به دیالوگ واقعی تغییر کرد
|
||||
- RejectRequest: از دلیل هاردکد به دیالوگ custom تغییر کرد
|
||||
|
||||
---
|
||||
|
||||
## فاز ۵ — صفحات مفقود (CMS-Derived)
|
||||
|
||||
### WalletManagementPage ← `Pages/Wallet/WalletManagementPage.razor`
|
||||
- Route: `/wallets`
|
||||
- **تب ۱ — کیفپولها**: DataGrid با ServerData، فیلتر UserAutoComplete
|
||||
- ستونها: شناسه، کاربر، موجودی اصلی، موجودی شبکه، مشاهده تغییرات
|
||||
- نوع: `GetAllUserWalletByFilterResponseModel`
|
||||
- **تب ۲ — تاریخچه تغییرات**: DataGrid با ServerData، فیلتر کاربر + نوع (افزایش/کاهش)
|
||||
- ستونها: شناسه، کیفپول، نوع (چیپ رنگی)، تغییر موجودی، موجودی فعلی، تغییر شبکه، موجودی شبکه فعلی، شناسه مرجع، تاریخ
|
||||
- نوع: `GetAllUserWalletChangeLogByFilterResponseModel`
|
||||
- فیلدهای proto با typo: `ChangeNerworkValue`, `RefrenceId`
|
||||
|
||||
### UserContractPage ← `Pages/Contract/UserContractPage.razor`
|
||||
- Route: `/contracts`
|
||||
- DataGrid با ServerData، فیلتر UserAutoComplete
|
||||
- ستونها: شناسه، کاربر، قرارداد، کد امضا، لینک PDF، جزئیات
|
||||
- نوع: `GetAllUserContractByFilterResponseModel`
|
||||
|
||||
### ContractDetailsDialog ← `Pages/Contract/ContractDetailsDialog.razor`
|
||||
- جدول ساده: شناسه، کاربر، قالب قرارداد، کد امضا، لینک دانلود PDF
|
||||
|
||||
### ClubFeaturesPage ← `Pages/Club/ClubFeaturesPage.razor`
|
||||
- Route: `/club/features`
|
||||
- **تب ۱ — فیچرهای تعریفشده**: کارتهای MudPaper با عنوان، توضیحات، وضعیت (فعال/غیرفعال)، ترتیب
|
||||
- از `ConfigClient.GetClubFeaturesAsync`
|
||||
- **تب ۲ — فیچرهای کاربران**: DataGrid با فیلتر کاربر، سوئیچ فعال/غیرفعال
|
||||
- از `ClubClient.GetUserClubFeaturesAsync` + `ToggleUserClubFeatureAsync`
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ نکات فنی مهم
|
||||
|
||||
### الگوهای Proto
|
||||
- **فیلتر**: `new GetAllXxxByFilterFilter()` (سطح بالا، بدون `.Types.`)
|
||||
- **پاسخ لیست**: `GetAllXxxByFilterResponseModel` (نه `GetXxxResponse`)
|
||||
- **Typoها در proto**: `ChangeNerworkValue` و `RefrenceId` — باید دقیقاً همینها استفاده شوند
|
||||
|
||||
### _Imports.razor — سرویسهای Global
|
||||
سرویسهای زیر از `_Imports.razor` تزریق میشوند و نباید در code-behind با `[Inject]` دوباره تعریف شوند:
|
||||
- `NavigationManager Navigation`
|
||||
- `ILocalStorageService LocalStorageService`
|
||||
- `ISnackbar Snackbar`
|
||||
- `IJSRuntime jsRuntime`
|
||||
- `IDialogService DialogService`
|
||||
- `AuthenticationStateProvider`
|
||||
|
||||
### MudBlazor محدودیتها
|
||||
- `Variant.Flat` وجود ندارد → از `Variant.Text` استفاده شود
|
||||
- `GutterSize` روی `MudGrid` deprecated است
|
||||
|
||||
---
|
||||
|
||||
## فاز ۴ (ادامه) — تکمیل فیکسهای بیزینسی
|
||||
|
||||
### UserSettings — حذف تبهای جعلی (2.5.5)
|
||||
- **`UserSettings.razor`**: تب Notifications (۷ تاگل جعلی بدون بکاند) → placeholder «بهزودی فعال میشود» با آیکون NotificationsActive
|
||||
- **`UserSettings.razor`**: تب Security (فرم تغییر رمز + 2FA بدون بکاند) → placeholder «بهزودی فعال میشود» با آیکون Lock
|
||||
- **حذف کد مرده**: ~۱۵ متغیر notification/security + متدهای `SaveNotificationSettings()`, `SaveSecuritySettings()`, `ChangePassword()`
|
||||
|
||||
### HealthDashboard — حذف متریکهای Mock (2.5.6)
|
||||
- **`HealthDashboard.razor`**: بخش System Resources (۴ کارت CPU=45.2%, Memory=62.8%, Disk=73.5%, Network) → یک کارت با پیام «مانیتورینگ منابع سیستم در نسخههای آینده با اتصال به Prometheus/Grafana فعال خواهد شد»
|
||||
- **حذف کد مرده**: ۹ متغیر mock (`_cpuUsage`, `_memoryUsage`, etc.) + متد `GetResourceColor()`
|
||||
|
||||
### حذف cursor:pointer زائد (4.10)
|
||||
- `Style="cursor:pointer;"` از **۱۷ فایل** حذف شد (MudButton/MudIconButton خودشان cursor:pointer دارند)
|
||||
- شامل: ProductsMainPage, GalleryDialog, Image.razor (base component), RoleMainPage, UserOrderMainPage, PackageMainPage, etc.
|
||||
|
||||
### حذف Console.WriteLine (4.11)
|
||||
- از **۱۱ فایل** حذف شد: DiscountProductService, DiscountCategoryService, NetworkTreeViewer, UserSettings, CategoryMainPage, UserOrderMainPage, Commission/Dashboard, DiscountProductsMainPage, DiscountCategoryMultiSelectCombo, UserRole/CreateDialog
|
||||
- **`Commission/Dashboard.razor.cs`**: `using System.Text.Json` هم حذف شد (دیگر استفاده نمیشد)
|
||||
|
||||
### DiscountProducts — فیلتر دستهبندی (4.7)
|
||||
- **`DiscountProductsMainPage.razor`**: `@inject IDiscountCategoryService` اضافه شد
|
||||
- دستهبندیهای فعال از سرویس بارگذاری و در MudSelect نمایش داده میشوند
|
||||
- جایگزینی `@* TODO: Load categories *@` با `@foreach` واقعی
|
||||
|
||||
### DiscountProducts — Items → ServerData (4.8)
|
||||
- **`DiscountProductsMainPage.razor`**: تبدیل از `Items="@_products"` به `ServerData="LoadServerData"`
|
||||
- متد `LoadServerData(GridState<T>)` اضافه شد با `PageNumber` و `PageSize` از state
|
||||
- `Height="calc(100vh - 300px)"` + `FixedHeader="true"` اضافه شد
|
||||
- Snackbar اطلاعرسانی اضافی در هر بارگذاری حذف شد (redundant)
|
||||
|
||||
### UserOrder — فعالسازی آمار (4.6)
|
||||
- **`UserOrderMainPage.razor`**: بخش آمار (MudCard + MudChart) از حالت comment خارج شد
|
||||
- نمایش تعداد سفارشها + مجموع مبلغ + نمودار Bar وضعیت ارسال
|
||||
|
||||
### UserSettings — persist با localStorage (4.9)
|
||||
- تب General از قبل با localStorage کار میکرد ✅ (فقط تبهای جعلی حذف شدند)
|
||||
|
||||
---
|
||||
|
||||
## فاز ۴.۵ — Cross-Navigation
|
||||
|
||||
### لینک نام کاربر → پروفایل (4.5.1, 4.5.2)
|
||||
- **`UserOrderMainPage.razor`**: ستون `UserFullName` از PropertyColumn به TemplateColumn تبدیل شد — MudLink به `/network/user-info/{UserId}`
|
||||
- **`UserPayouts.razor`**: نام/شناسه کاربر به MudLink تبدیل شد → `/network/user-info/{UserId}`
|
||||
- **`WithdrawalRequests.razor`**: نام کاربر به MudLink تبدیل شد → `/network/user-info/{UserId}`
|
||||
|
||||
### لینک محصول ↔ انبار (4.5.3, 4.5.4) — Skip
|
||||
- صفحه جزئیات محصول وجود ندارد (فقط لیست + دیالوگ ویرایش)
|
||||
- لینک cross-navigation بدون صفحه مقصد معنادار نیست
|
||||
|
||||
### Extract shared status helpers (3.5.6) — Skip
|
||||
- بررسی نشان داد هر context مقادیر متفاوتی دارد:
|
||||
- UserPayouts: status 1 = Success/پرداخت شده
|
||||
- WithdrawalRequests: status 1 = Info/واریز شده
|
||||
- PayoutDetailsDialog: فقط ۳ وضعیت (نه ۶)
|
||||
- استخراج به helper مشترک باعث از دست رفتن معنای domain-specific میشود
|
||||
|
||||
---
|
||||
|
||||
## 📈 آمار تغییرات
|
||||
|
||||
| معیار | تعداد |
|
||||
|---|---|
|
||||
| فایلهای جدید | 11 |
|
||||
| فایلهای حذفشده | 1 (SystemOverview.razor) |
|
||||
| فایلهای ویرایششده | ~65+ |
|
||||
| DataGridهای آپدیتشده | 33 |
|
||||
| Delete confirmation اصلاحشده | 11 |
|
||||
| صفحات ادغامشده | 14 → 5 |
|
||||
| خطاهای build حلشده | 9+ |
|
||||
| Console.WriteLine حذفشده | 22+ (از ۱۱ فایل) |
|
||||
| cursor:pointer حذفشده | 17 فایل |
|
||||
| Cross-link اضافهشده | 3 صفحه (UserOrder, UserPayouts, WithdrawalRequests) |
|
||||
@@ -1,34 +1,54 @@
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Class="pa-2 pr-4 rounded" Style="height:85vh;">
|
||||
<MudItem xs="2">
|
||||
<MudPaper Height="85vh" Style="overflow:hidden; position:relative;" Outlined="true" Class="rounded-lg">
|
||||
<MudStack Class="pa-5" Style="height:100%;">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.subtitle2">فیلتر ها</MudText>
|
||||
<MudSpacer />
|
||||
@if (IsFiltered)
|
||||
{
|
||||
<MudButton Color="Color.Error" Variant="Variant.Text" Size="Size.Small" OnClick="@(async () => await OnClearFilterClick.InvokeAsync())">حذف فیلتر</MudButton>
|
||||
}
|
||||
</MudStack>
|
||||
<MudItem Style="height:100%;">
|
||||
<MudStack StretchItems="StretchItems.None" Class="filter-item" Style="height:100%;">
|
||||
<MudStack>
|
||||
@Filters
|
||||
</MudStack>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Size="Size.Small" FullWidth="true" Class="mt-auto" OnClick="@(async () => await OnSubmitClick.InvokeAsync())">جستجو</MudButton>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
@* ── Responsive base page: filter above grid, collapsible ── *@
|
||||
<MudStack Spacing="0" Class="base-page">
|
||||
|
||||
@* ── Header row: page title + filter toggle ── *@
|
||||
@if (Filters != null)
|
||||
{
|
||||
<MudPaper Outlined="true" Class="rounded-lg mb-3 base-page__filter-panel">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Class="pa-3 cursor-pointer"
|
||||
@onclick="ToggleFilterPanel">
|
||||
<MudIcon Icon="@Icons.Material.Filled.FilterList" Size="Size.Small"
|
||||
Color="@(IsFiltered ? Color.Primary : Color.Default)" />
|
||||
<MudText Typo="Typo.subtitle2" Class="mx-1">فیلترها</MudText>
|
||||
@if (IsFiltered)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Primary" Size="Size.Small" Variant="Variant.Text">فعال</MudChip>
|
||||
}
|
||||
<MudSpacer />
|
||||
<MudIcon Icon="@(_filterOpen ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)"
|
||||
Size="Size.Small" />
|
||||
</MudStack>
|
||||
|
||||
@if (_filterOpen)
|
||||
{
|
||||
<MudDivider />
|
||||
<div class="pa-3">
|
||||
<MudGrid Spacing="2">
|
||||
@Filters
|
||||
</MudGrid>
|
||||
<MudStack Row="true" Justify="Justify.FlexEnd" Class="mt-3" Spacing="2">
|
||||
@if (IsFiltered)
|
||||
{
|
||||
<MudButton Color="Color.Error" Variant="Variant.Text" Size="Size.Small"
|
||||
OnClick="@(async () => await OnClearFilterClick.InvokeAsync())">
|
||||
پاک کردن فیلتر
|
||||
</MudButton>
|
||||
}
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.Search"
|
||||
OnClick="@(async () => await OnSubmitClick.InvokeAsync())">
|
||||
جستجو
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</div>
|
||||
}
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
</MudItem>
|
||||
<MudItem xs="10">
|
||||
<MudPaper Height="85vh" Style="overflow:hidden; position:relative;" Outlined="true" Class="rounded-lg">
|
||||
@* ── Content area (DataGrid or custom layout) ── *@
|
||||
<MudPaper Outlined="true" Class="rounded-lg base-page__content" Style="overflow:hidden; position:relative;">
|
||||
@Content
|
||||
</MudPaper>
|
||||
|
||||
@Content
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudStack>
|
||||
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace BackOffice.Common.BaseComponents;
|
||||
|
||||
public partial class BasePageComponent
|
||||
{
|
||||
[Parameter] public RenderFragment Filters { get; set; } = default!;
|
||||
[Parameter] public RenderFragment? Filters { get; set; }
|
||||
[Parameter] public RenderFragment Content { get; set; } = default!;
|
||||
[Parameter] public bool IsFiltered { get; set; }
|
||||
[Parameter] public EventCallback OnSubmitClick { get; set; }
|
||||
[Parameter] public EventCallback OnClearFilterClick { get; set; }
|
||||
|
||||
/// <summary>Whether the filter panel is expanded.</summary>
|
||||
private bool _filterOpen;
|
||||
|
||||
private void ToggleFilterPanel() => _filterOpen = !_filterOpen;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/* ── BasePageComponent scoped styles ── */
|
||||
|
||||
.base-page {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.base-page__filter-panel {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.base-page__content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
@@ -7,6 +7,6 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudImage Src="@src" Alt="@Alt" Elevation="@Elevation" Class="@Class" Fluid="@Fluid" Height="@Height" ObjectFit="@ObjectFit" ObjectPosition="@ObjectPosition" Style="@(Style + ";cursor:pointer;")" Tag="@Tag" Width="@Width" @onclick="async()=>await OnClick.InvokeAsync()" />
|
||||
<MudImage Src="@src" Alt="@Alt" Elevation="@Elevation" Class="@Class" Fluid="@Fluid" Height="@Height" ObjectFit="@ObjectFit" ObjectPosition="@ObjectPosition" Style="@Style" Tag="@Tag" Width="@Width" @onclick="async()=>await OnClick.InvokeAsync()" />
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Common.BaseComponents
|
||||
@@ -13,13 +14,24 @@ namespace BackOffice.Common.BaseComponents
|
||||
{
|
||||
private string src = string.Empty;
|
||||
[Parameter] public EventCallback OnClick { get; set; }
|
||||
[Inject] private IConfiguration Configuration { get; set; } = default!;
|
||||
|
||||
private string FileBaseUrl => Configuration["GwUrl"]?.TrimEnd('/') ?? "";
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
await base.OnParametersSetAsync();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Src))
|
||||
{
|
||||
src = Src.Contains("data:") ? Src : "https://dl.afrino.co" + Src;
|
||||
if (Src.Contains("data:") || Src.StartsWith("http"))
|
||||
src = Src;
|
||||
else
|
||||
src = FileBaseUrl + "/uploads/" + Src.TrimStart('/');
|
||||
}
|
||||
else
|
||||
{
|
||||
src = string.Empty;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,12 +23,21 @@ using CMSMicroservice.Protobuf.Protos.ProductTag;
|
||||
using CMSMicroservice.Protobuf.Protos.ManualPayment;
|
||||
using CMSMicroservice.Protobuf.Protos;
|
||||
using CMSMicroservice.Protobuf.Protos.Inventory;
|
||||
using CMSMicroservice.Protobuf.Protos.BlogPost;
|
||||
using CMSMicroservice.Protobuf.Protos.BlogCategory;
|
||||
using CMSMicroservice.Protobuf.Protos.BlogPostImage;
|
||||
using CMSMicroservice.Protobuf.Protos.SitePage;
|
||||
using CMSMicroservice.Protobuf.Protos.UserContract;
|
||||
using CMSMicroservice.Protobuf.Protos.UserWallet;
|
||||
using CMSMicroservice.Protobuf.Protos.UserWalletChangeLog;
|
||||
using BackOffice.Common.Utilities;
|
||||
using BackOffice.Services.DiscountProduct;
|
||||
using BackOffice.Services.DiscountCategory;
|
||||
using BackOffice.Services.DiscountOrder;
|
||||
using BackOffice.Services.PublicMessage;
|
||||
using BackOffice.Services.Tag;
|
||||
using BackOffice.Services.Blog;
|
||||
using BackOffice.Services.Content;
|
||||
using Blazored.LocalStorage;
|
||||
using Grpc.Core;
|
||||
using Grpc.Core.Interceptors;
|
||||
@@ -77,6 +86,12 @@ public static class ConfigureServices
|
||||
services.AddScoped<IPublicMessageService, PublicMessageService>();
|
||||
services.AddScoped<ITagService, TagService>();
|
||||
|
||||
// Blog & Content Management Services
|
||||
services.AddScoped<IBlogPostService, BlogPostService>();
|
||||
services.AddScoped<IBlogCategoryService, BlogCategoryService>();
|
||||
services.AddScoped<IBlogPostImageService, BlogPostImageService>();
|
||||
services.AddScoped<ISitePageService, SitePageService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -138,6 +153,19 @@ public static class ConfigureServices
|
||||
// Inventory Management Service
|
||||
services.AddTransient(sp => new InventoryContract.InventoryContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
|
||||
// Blog & Content Management Services
|
||||
services.AddTransient(sp => new BlogPostContract.BlogPostContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new BlogCategoryContract.BlogCategoryContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new BlogPostImageContract.BlogPostImageContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new SitePageContract.SitePageContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
|
||||
// User Contracts Service
|
||||
services.AddTransient(sp => new UserContractContract.UserContractContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
|
||||
// User Wallet Services
|
||||
services.AddTransient(sp => new UserWalletContract.UserWalletContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new UserWalletChangeLogContract.UserWalletChangeLogContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ public static class RouteConstance
|
||||
public const string UserPage = "/UserPage/";
|
||||
public const string UserOrder = "/UserOrderPage/";
|
||||
public const string Orders = "/OrdersPage/";
|
||||
public const string OrdersSalesReports = "/OrdersSalesReportsPage/";
|
||||
public const string UserRole = "/UserRolePage/";
|
||||
public const string UserAddress = "/UserAddressPage/";
|
||||
public const string Products = "/ProductsPage/";
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
@ref="_selectRef"
|
||||
Label="@Label"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Disabled="@Disabled"
|
||||
MultiSelection="true"
|
||||
MultiSelectionTextFunc="@GetSelectedText"
|
||||
SelectedValues="_internalSelectedIds"
|
||||
SelectedValuesChanged="@(_ => OnSelectedValuesChangedAsync())">
|
||||
SelectedValuesChanged="OnSelectedValuesChangedAsync">
|
||||
@foreach (var item in _items)
|
||||
{
|
||||
<MudSelectItem T="long" Value="@item.Id">
|
||||
|
||||
@@ -62,11 +62,9 @@ public partial class CategoryMultiSelectCombo
|
||||
: new HashSet<long>();
|
||||
}
|
||||
|
||||
private async Task OnSelectedValuesChangedAsync()
|
||||
private async Task OnSelectedValuesChangedAsync(IEnumerable<long> values)
|
||||
{
|
||||
var current = _selectRef?.SelectedValues ?? System.Array.Empty<long>();
|
||||
|
||||
_internalSelectedIds = current.Where(id => id > 0).Distinct().ToHashSet();
|
||||
_internalSelectedIds = values.Where(id => id > 0).Distinct().ToHashSet();
|
||||
SelectedIds = _internalSelectedIds.ToList();
|
||||
|
||||
if (SelectedIdsChanged.HasDelegate)
|
||||
@@ -74,5 +72,27 @@ public partial class CategoryMultiSelectCombo
|
||||
await SelectedIdsChanged.InvokeAsync(SelectedIds);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetSelectedText(List<string> selectedValues)
|
||||
{
|
||||
if (selectedValues == null || selectedValues.Count == 0)
|
||||
return string.Empty;
|
||||
|
||||
var titles = new List<string>();
|
||||
foreach (var val in selectedValues)
|
||||
{
|
||||
if (long.TryParse(val, out var id))
|
||||
{
|
||||
var item = _items.FirstOrDefault(x => x.Id == id);
|
||||
titles.Add(item != null ? item.Title : val);
|
||||
}
|
||||
else
|
||||
{
|
||||
titles.Add(val);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Join("، ", titles);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<MudSelect @ref="_selectRef"
|
||||
T="long"
|
||||
MultiSelection="true"
|
||||
MultiSelectionTextFunc="@GetSelectedText"
|
||||
SelectedValues="_internalSelectedIds"
|
||||
SelectedValuesChanged="OnSelectedValuesChangedAsync"
|
||||
Label="@Label"
|
||||
@@ -11,6 +12,6 @@
|
||||
Disabled="@Disabled">
|
||||
@foreach (var item in _items)
|
||||
{
|
||||
<MudSelectItem Value="@item.Id">@item.Title</MudSelectItem>
|
||||
<MudSelectItem T="long" Value="@item.Id">@item.Title</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
@@ -34,30 +34,22 @@ public partial class DiscountCategoryMultiSelectCombo
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.WriteLine("Starting to load DiscountCategories...");
|
||||
var tree = await CategoryService.GetCategoriesAsync(isActive: true);
|
||||
Console.WriteLine($"Received {tree?.Count ?? 0} root categories");
|
||||
_items = FlattenCategories(tree ?? new List<DiscountCategoryDto>());
|
||||
Console.WriteLine($"DiscountCategory loaded: {_items.Count} items");
|
||||
|
||||
// Log first few items
|
||||
if (_items.Count > 0)
|
||||
{
|
||||
Console.WriteLine("Sample categories:");
|
||||
foreach (var item in _items.Take(5))
|
||||
{
|
||||
Console.WriteLine($" - {item.Id}: {item.Title} (Active: {item.IsActive})");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error loading DiscountCategories: {ex.GetType().Name} - {ex.Message}");
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Console.WriteLine($"Inner Exception: {ex.InnerException.Message}");
|
||||
}
|
||||
Console.WriteLine($"Stack Trace: {ex.StackTrace}");
|
||||
_items = new List<DiscountCategoryDto>();
|
||||
}
|
||||
}
|
||||
@@ -102,4 +94,26 @@ public partial class DiscountCategoryMultiSelectCombo
|
||||
await SelectedIdsChanged.InvokeAsync(SelectedIds);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetSelectedText(List<string> selectedValues)
|
||||
{
|
||||
if (selectedValues == null || selectedValues.Count == 0)
|
||||
return string.Empty;
|
||||
|
||||
var titles = new List<string>();
|
||||
foreach (var val in selectedValues)
|
||||
{
|
||||
if (long.TryParse(val, out var id))
|
||||
{
|
||||
var item = _items.FirstOrDefault(x => x.Id == id);
|
||||
titles.Add(item != null ? item.Title : val);
|
||||
}
|
||||
else
|
||||
{
|
||||
titles.Add(val);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Join("، ", titles);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
@* Merged into /blog *@
|
||||
@attribute [Authorize(Roles = "Administrator")]
|
||||
|
||||
@using BackOffice.Services.Blog
|
||||
|
||||
@inject IBlogCategoryService BlogCategoryService
|
||||
|
||||
<div>
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.h5">مدیریت دستهبندیهای بلاگ</MudText>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="CreateCategory">
|
||||
افزودن دستهبندی
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
<MudDataGrid T="BlogCategoryItemDto"
|
||||
@ref="_grid"
|
||||
ServerData="LoadServerData"
|
||||
Hover="true"
|
||||
Elevation="1"
|
||||
Dense="true"
|
||||
Height="calc(100vh - 240px)">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
<PropertyColumn Property="x => x.Title" Title="عنوان" />
|
||||
<PropertyColumn Property="x => x.Slug" Title="اسلاگ" />
|
||||
<PropertyColumn Property="x => x.IconName" Title="آیکون" />
|
||||
<PropertyColumn Property="x => x.PostCount" Title="تعداد پست" />
|
||||
<PropertyColumn Property="x => x.IsActive" Title="وضعیت">
|
||||
<CellTemplate>
|
||||
<MudChip Color="@(context.Item.IsActive ? Color.Success : Color.Error)"
|
||||
Size="Size.Small">
|
||||
@(context.Item.IsActive ? "فعال" : "غیرفعال")
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<PropertyColumn Property="x => x.SortOrder" Title="ترتیب" />
|
||||
<TemplateColumn Title="عملیات" Sortable="false">
|
||||
<CellTemplate>
|
||||
<MudStack Row="true" Spacing="1">
|
||||
<MudTooltip Text="ویرایش">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Size="Size.Small"
|
||||
Color="Color.Primary"
|
||||
OnClick="@(() => EditCategory(context.Item))" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="حذف">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Size="Size.Small"
|
||||
Color="Color.Error"
|
||||
OnClick="@(() => DeleteCategory(context.Item))" />
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="BlogCategoryItemDto" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudStack>
|
||||
</div>
|
||||
@@ -0,0 +1,102 @@
|
||||
using BackOffice.Services.Blog;
|
||||
using BackOffice.Pages.Blog.Components;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Blog;
|
||||
|
||||
public partial class BlogCategoryManagementPage
|
||||
{
|
||||
private MudDataGrid<BlogCategoryItemDto> _grid = default!;
|
||||
|
||||
private async Task<GridData<BlogCategoryItemDto>> LoadServerData(GridState<BlogCategoryItemDto> state)
|
||||
{
|
||||
var filter = new BlogCategoryFilterDto
|
||||
{
|
||||
PageNumber = state.Page + 1,
|
||||
PageSize = state.PageSize
|
||||
};
|
||||
|
||||
var result = await BlogCategoryService.GetAllAsync(filter);
|
||||
|
||||
return new GridData<BlogCategoryItemDto>
|
||||
{
|
||||
Items = result.Items,
|
||||
TotalItems = result.TotalCount
|
||||
};
|
||||
}
|
||||
|
||||
private async Task ReloadAsync()
|
||||
{
|
||||
if (_grid != null)
|
||||
{
|
||||
await _grid.ReloadServerData();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CreateCategory()
|
||||
{
|
||||
var parameters = new DialogParameters<BlogCategoryEditDialog>
|
||||
{
|
||||
{ nameof(BlogCategoryEditDialog.Model), new BlogCategoryEditDto() },
|
||||
{ nameof(BlogCategoryEditDialog.IsEditMode), false }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<BlogCategoryEditDialog>("ایجاد دستهبندی جدید", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true });
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
Snackbar.Add("دستهبندی با موفقیت ایجاد شد", Severity.Success);
|
||||
await ReloadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EditCategory(BlogCategoryItemDto item)
|
||||
{
|
||||
var dto = new BlogCategoryEditDto
|
||||
{
|
||||
Title = item.Title,
|
||||
Slug = item.Slug,
|
||||
Description = item.Description,
|
||||
IconName = item.IconName,
|
||||
SortOrder = item.SortOrder,
|
||||
IsActive = item.IsActive
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters<BlogCategoryEditDialog>
|
||||
{
|
||||
{ nameof(BlogCategoryEditDialog.Model), dto },
|
||||
{ nameof(BlogCategoryEditDialog.CategoryId), item.Id },
|
||||
{ nameof(BlogCategoryEditDialog.IsEditMode), true }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<BlogCategoryEditDialog>("ویرایش دستهبندی", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true });
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
Snackbar.Add("دستهبندی با موفقیت بهروزرسانی شد", Severity.Success);
|
||||
await ReloadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteCategory(BlogCategoryItemDto item)
|
||||
{
|
||||
var confirmed = await DialogService.ShowMessageBox(
|
||||
"حذف دستهبندی",
|
||||
$"آیا از حذف دستهبندی «{item.Title}» مطمئن هستید؟",
|
||||
yesText: "حذف",
|
||||
cancelText: "لغو");
|
||||
|
||||
if (confirmed == true)
|
||||
{
|
||||
await BlogCategoryService.DeleteAsync(item.Id);
|
||||
Snackbar.Add("دستهبندی حذف شد", Severity.Success);
|
||||
await ReloadAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
@page "/blog"
|
||||
@page "/blog/posts"
|
||||
@page "/blog/categories"
|
||||
@attribute [Authorize(Roles = "Administrator")]
|
||||
|
||||
<div>
|
||||
<MudText Typo="Typo.h5" Class="mb-3">مدیریت بلاگ</MudText>
|
||||
|
||||
<MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true"
|
||||
PanelClass="pt-4" @bind-ActivePanelIndex="_activeTab">
|
||||
<MudTabPanel Text="پستها" Icon="@Icons.Material.Filled.PostAdd">
|
||||
<BlogPostManagementPage />
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="دستهبندیها" Icon="@Icons.Material.Filled.Category">
|
||||
<BlogCategoryManagementPage />
|
||||
</MudTabPanel>
|
||||
</MudTabs>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private int _activeTab;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
var uri = Navigation.ToBaseRelativePath(Navigation.Uri).ToLowerInvariant().TrimEnd('/');
|
||||
_activeTab = uri switch
|
||||
{
|
||||
"blog/categories" => 1,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
@* Merged into /blog *@
|
||||
@attribute [Authorize(Roles = "Administrator")]
|
||||
|
||||
@using BackOffice.Services.Blog
|
||||
|
||||
@inject IBlogPostService BlogPostService
|
||||
@inject IBlogCategoryService BlogCategoryService
|
||||
|
||||
<div>
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.h5">مدیریت پستهای بلاگ</MudText>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="CreatePost">
|
||||
افزودن پست
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
||||
<MudTextField @bind-Value="_search"
|
||||
Label="جستجو بر اساس عنوان"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||
Immediate="true"
|
||||
OnBlur="ReloadAsync"
|
||||
OnKeyDown="OnSearchKeyDown" />
|
||||
|
||||
<MudSelect @bind-Value="_statusFilter"
|
||||
Label="وضعیت"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Style="width: 160px;">
|
||||
<MudSelectItem T="int?" Value="@( (int?)null )">همه</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="0">پیشنویس</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="1">منتشر شده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="2">آرشیو شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
|
||||
<MudSelect @bind-Value="_categoryFilter"
|
||||
Label="دستهبندی"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Style="width: 200px;">
|
||||
<MudSelectItem T="long?" Value="@( (long?)null )">همه</MudSelectItem>
|
||||
@foreach (var cat in _categories)
|
||||
{
|
||||
<MudSelectItem T="long?" Value="@( (long?)cat.Id )">@cat.Title</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Primary"
|
||||
OnClick="ReloadAsync">
|
||||
اعمال فیلتر
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
<MudDataGrid T="BlogPostListItemDto"
|
||||
@ref="_grid"
|
||||
ServerData="LoadServerData"
|
||||
Hover="true"
|
||||
Elevation="1"
|
||||
Dense="true"
|
||||
Height="calc(100vh - 240px)">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
<PropertyColumn Property="x => x.Title" Title="عنوان" />
|
||||
<PropertyColumn Property="x => x.Slug" Title="اسلاگ" />
|
||||
<PropertyColumn Property="x => x.StatusName" Title="وضعیت">
|
||||
<CellTemplate>
|
||||
<MudChip Color="@GetStatusColor(context.Item.Status)"
|
||||
Size="Size.Small">
|
||||
@context.Item.StatusName
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<PropertyColumn Property="x => x.IsFeatured" Title="ویژه">
|
||||
<CellTemplate>
|
||||
@if (context.Item.IsFeatured)
|
||||
{
|
||||
<MudIcon Icon="@Icons.Material.Filled.Star" Color="Color.Warning" Size="Size.Small" />
|
||||
}
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<PropertyColumn Property="x => x.ViewCount" Title="بازدید" />
|
||||
<TemplateColumn Title="دستهبندیها" Sortable="false">
|
||||
<CellTemplate>
|
||||
<MudStack Row="true" Spacing="1">
|
||||
@foreach (var cat in context.Item.Categories)
|
||||
{
|
||||
<MudChip Size="Size.Small" Color="Color.Info">@cat.Title</MudChip>
|
||||
}
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="تاریخ ایجاد">
|
||||
<CellTemplate>
|
||||
@context.Item.Created.MiladiToJalali()
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="عملیات" Sortable="false">
|
||||
<CellTemplate>
|
||||
<MudStack Row="true" Spacing="1">
|
||||
@if (context.Item.Status == 0)
|
||||
{
|
||||
<MudTooltip Text="انتشار">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Publish"
|
||||
Size="Size.Small"
|
||||
Color="Color.Success"
|
||||
OnClick="@(() => PublishPost(context.Item))" />
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (context.Item.Status == 1)
|
||||
{
|
||||
<MudTooltip Text="آرشیو">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Archive"
|
||||
Size="Size.Small"
|
||||
Color="Color.Warning"
|
||||
OnClick="@(() => ArchivePost(context.Item))" />
|
||||
</MudTooltip>
|
||||
}
|
||||
<MudTooltip Text="ویرایش">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Size="Size.Small"
|
||||
Color="Color.Primary"
|
||||
OnClick="@(() => EditPost(context.Item))" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="حذف">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Size="Size.Small"
|
||||
Color="Color.Error"
|
||||
OnClick="@(() => DeletePost(context.Item))" />
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="BlogPostListItemDto" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudStack>
|
||||
</div>
|
||||
@@ -0,0 +1,189 @@
|
||||
using BackOffice.Services.Blog;
|
||||
using BackOffice.Pages.Blog.Components;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Blog;
|
||||
|
||||
public partial class BlogPostManagementPage
|
||||
{
|
||||
private MudDataGrid<BlogPostListItemDto> _grid = default!;
|
||||
private string? _search;
|
||||
private int? _statusFilter;
|
||||
private long? _categoryFilter;
|
||||
private List<BlogCategoryItemDto> _categories = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_categories = await BlogCategoryService.GetActiveAsync();
|
||||
}
|
||||
|
||||
private async Task<GridData<BlogPostListItemDto>> LoadServerData(GridState<BlogPostListItemDto> state)
|
||||
{
|
||||
var filter = new BlogPostFilterDto
|
||||
{
|
||||
PageNumber = state.Page + 1,
|
||||
PageSize = state.PageSize,
|
||||
SearchTerm = _search,
|
||||
Status = _statusFilter,
|
||||
CategoryId = _categoryFilter
|
||||
};
|
||||
|
||||
var result = await BlogPostService.GetAllAsync(filter);
|
||||
|
||||
return new GridData<BlogPostListItemDto>
|
||||
{
|
||||
Items = result.Items,
|
||||
TotalItems = result.TotalCount
|
||||
};
|
||||
}
|
||||
|
||||
private async Task ReloadAsync()
|
||||
{
|
||||
if (_grid != null)
|
||||
{
|
||||
await _grid.ReloadServerData();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSearchKeyDown(KeyboardEventArgs args)
|
||||
{
|
||||
if (args.Key == "Enter")
|
||||
{
|
||||
_ = ReloadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private static Color GetStatusColor(int status) => status switch
|
||||
{
|
||||
0 => Color.Default, // Draft
|
||||
1 => Color.Success, // Published
|
||||
2 => Color.Warning, // Archived
|
||||
_ => Color.Default
|
||||
};
|
||||
|
||||
private async Task CreatePost()
|
||||
{
|
||||
var parameters = new DialogParameters<BlogPostEditDialog>
|
||||
{
|
||||
{ nameof(BlogPostEditDialog.Model), new BlogPostEditDto() },
|
||||
{ nameof(BlogPostEditDialog.IsEditMode), false },
|
||||
{ nameof(BlogPostEditDialog.Categories), _categories }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<BlogPostEditDialog>("ایجاد پست جدید", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Large, FullWidth = true });
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
Snackbar.Add("پست با موفقیت ایجاد شد", Severity.Success);
|
||||
await ReloadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EditPost(BlogPostListItemDto item)
|
||||
{
|
||||
var details = await BlogPostService.GetByIdAsync(item.Id);
|
||||
if (details == null)
|
||||
{
|
||||
Snackbar.Add("خطا در دریافت اطلاعات پست", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var dto = new BlogPostEditDto
|
||||
{
|
||||
Title = details.Title,
|
||||
Slug = details.Slug,
|
||||
Summary = details.Summary,
|
||||
HtmlContent = details.HtmlContent,
|
||||
FeaturedImagePath = details.FeaturedImagePath,
|
||||
FeaturedImageThumbnailPath = details.FeaturedImageThumbnailPath,
|
||||
CategoryIds = details.Categories.Select(c => c.Id).ToList(),
|
||||
TagIds = details.Tags.Select(t => t.Id).ToList(),
|
||||
IsFeatured = details.IsFeatured,
|
||||
SortOrder = details.SortOrder
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters<BlogPostEditDialog>
|
||||
{
|
||||
{ nameof(BlogPostEditDialog.Model), dto },
|
||||
{ nameof(BlogPostEditDialog.PostId), item.Id },
|
||||
{ nameof(BlogPostEditDialog.IsEditMode), true },
|
||||
{ nameof(BlogPostEditDialog.Categories), _categories }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<BlogPostEditDialog>("ویرایش پست", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Large, FullWidth = true });
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
Snackbar.Add("پست با موفقیت بهروزرسانی شد", Severity.Success);
|
||||
await ReloadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PublishPost(BlogPostListItemDto item)
|
||||
{
|
||||
var confirmed = await DialogService.ShowMessageBox(
|
||||
"انتشار پست",
|
||||
$"آیا از انتشار پست «{item.Title}» مطمئن هستید؟",
|
||||
yesText: "انتشار",
|
||||
cancelText: "لغو");
|
||||
|
||||
if (confirmed == true)
|
||||
{
|
||||
var result = await BlogPostService.PublishAsync(item.Id);
|
||||
if (result.Success)
|
||||
{
|
||||
Snackbar.Add("پست با موفقیت منتشر شد", Severity.Success);
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add(result.Message, Severity.Error);
|
||||
}
|
||||
await ReloadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ArchivePost(BlogPostListItemDto item)
|
||||
{
|
||||
var confirmed = await DialogService.ShowMessageBox(
|
||||
"آرشیو پست",
|
||||
$"آیا از آرشیو کردن پست «{item.Title}» مطمئن هستید؟",
|
||||
yesText: "آرشیو",
|
||||
cancelText: "لغو");
|
||||
|
||||
if (confirmed == true)
|
||||
{
|
||||
var result = await BlogPostService.ArchiveAsync(item.Id);
|
||||
if (result.Success)
|
||||
{
|
||||
Snackbar.Add("پست آرشیو شد", Severity.Success);
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add(result.Message, Severity.Error);
|
||||
}
|
||||
await ReloadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeletePost(BlogPostListItemDto item)
|
||||
{
|
||||
var confirmed = await DialogService.ShowMessageBox(
|
||||
"حذف پست",
|
||||
$"آیا از حذف پست «{item.Title}» مطمئن هستید؟",
|
||||
yesText: "حذف",
|
||||
cancelText: "لغو");
|
||||
|
||||
if (confirmed == true)
|
||||
{
|
||||
await BlogPostService.DeleteAsync(item.Id);
|
||||
Snackbar.Add("پست حذف شد", Severity.Success);
|
||||
await ReloadAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
@using BackOffice.Services.Blog
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<EditForm Model="@Model" OnValidSubmit="HandleValidSubmit">
|
||||
<MudStack Spacing="2">
|
||||
<MudTextField @bind-Value="Model.Title"
|
||||
Label="عنوان"
|
||||
Required="true"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudTextField @bind-Value="Model.Slug"
|
||||
Label="اسلاگ (URL)"
|
||||
Required="true"
|
||||
Variant="Variant.Outlined"
|
||||
HelperText="مثال: tech-news" />
|
||||
|
||||
<MudTextField @bind-Value="Model.Description"
|
||||
Label="توضیحات"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="3" />
|
||||
|
||||
<MudTextField @bind-Value="Model.IconName"
|
||||
Label="نام آیکون"
|
||||
Variant="Variant.Outlined"
|
||||
HelperText="مثال: category" />
|
||||
|
||||
<MudNumericField T="int"
|
||||
@bind-Value="Model.SortOrder"
|
||||
Label="ترتیب نمایش"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudSwitch T="bool" @bind-Value="Model.IsActive" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.body2">فعال</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</EditForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="SubmitAsync">
|
||||
@(_isEditMode ? "ذخیره تغییرات" : "ایجاد دستهبندی")
|
||||
</MudButton>
|
||||
<MudButton Color="Color.Default" Variant="Variant.Text" OnClick="Cancel">
|
||||
انصراف
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,41 @@
|
||||
using BackOffice.Services.Blog;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Blog.Components;
|
||||
|
||||
public partial class BlogCategoryEditDialog
|
||||
{
|
||||
[CascadingParameter] IMudDialogInstance DialogInstance { get; set; } = default!;
|
||||
[Inject] public IBlogCategoryService BlogCategoryService { get; set; } = default!;
|
||||
|
||||
[Parameter] public BlogCategoryEditDto Model { get; set; } = new();
|
||||
[Parameter] public long? CategoryId { get; set; }
|
||||
[Parameter] public bool IsEditMode { get; set; }
|
||||
|
||||
private bool _isEditMode => IsEditMode && CategoryId.HasValue;
|
||||
|
||||
private async Task HandleValidSubmit()
|
||||
{
|
||||
await SubmitAsync();
|
||||
}
|
||||
|
||||
private async Task SubmitAsync()
|
||||
{
|
||||
if (_isEditMode && CategoryId.HasValue)
|
||||
{
|
||||
await BlogCategoryService.UpdateAsync(CategoryId.Value, Model);
|
||||
}
|
||||
else
|
||||
{
|
||||
await BlogCategoryService.CreateAsync(Model);
|
||||
}
|
||||
|
||||
DialogInstance.Close(DialogResult.Ok(true));
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
DialogInstance.Cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
@using BackOffice.Services.Blog
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Tizzani.MudBlazor.HtmlEditor
|
||||
@using BackOffice.Common.BaseComponents
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudForm @ref="_form" @bind-IsValid="@_isValid">
|
||||
<MudStack Spacing="2">
|
||||
<MudTextField @bind-Value="Model.Title"
|
||||
Label="عنوان پست"
|
||||
Required="true"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudTextField @bind-Value="Model.Summary"
|
||||
Label="خلاصه"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="3" />
|
||||
|
||||
<MudText Typo="Typo.subtitle2">محتوای HTML</MudText>
|
||||
<MudHtmlEditor @bind-Html="Model.HtmlContent" MinHeight="300px">
|
||||
<MudHtmlToolbarOptions InsertImage="false" />
|
||||
</MudHtmlEditor>
|
||||
|
||||
<MudText Typo="Typo.subtitle2">تصویر شاخص</MudText>
|
||||
<MudStack Justify="Justify.Center" AlignItems="AlignItems.Center">
|
||||
@if (!string.IsNullOrWhiteSpace(_imagePreview))
|
||||
{
|
||||
<Image Src="@_imagePreview" Width="280" Height="180" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Model.FeaturedImagePath))
|
||||
{
|
||||
<Image Src="@Model.FeaturedImagePath" Width="280" Height="180" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper Class="d-flex align-center justify-center" Style="width:280px;height:180px;">
|
||||
<MudText Typo="Typo.caption">تصویری انتخاب نشده است</MudText>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
<MudFileUpload T="IBrowserFile" Accept="image/*" FilesChanged="OnImageSelected">
|
||||
<ActivatorContent>
|
||||
<MudButton HtmlTag="label"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
ButtonType="ButtonType.Button"
|
||||
StartIcon="@Icons.Material.Filled.Image">
|
||||
انتخاب تصویر شاخص
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
<SelectedTemplate>
|
||||
@if (context != null)
|
||||
{
|
||||
<MudText Class="mt-1" Typo="Typo.caption">@context.Name</MudText>
|
||||
}
|
||||
</SelectedTemplate>
|
||||
</MudFileUpload>
|
||||
</MudStack>
|
||||
|
||||
<MudSelect T="long"
|
||||
Label="دستهبندیها"
|
||||
MultiSelection="true"
|
||||
@bind-SelectedValues="_selectedCategoryIds"
|
||||
Variant="Variant.Outlined"
|
||||
MultiSelectionTextFunc="@(new Func<List<string>, string>(GetCategorySelectionText))">
|
||||
@foreach (var cat in Categories)
|
||||
{
|
||||
<MudSelectItem T="long" Value="@cat.Id">@cat.Title</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudSwitch T="bool" @bind-Value="Model.IsFeatured" Color="Color.Warning" />
|
||||
<MudText Typo="Typo.body2">پست ویژه</MudText>
|
||||
</MudStack>
|
||||
|
||||
<MudNumericField T="int"
|
||||
@bind-Value="Model.SortOrder"
|
||||
Label="ترتیب نمایش"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="SubmitAsync" Disabled="@(!_isValid || _loading)">
|
||||
@(_isEditMode ? "ذخیره تغییرات" : "ایجاد پست")
|
||||
</MudButton>
|
||||
<MudButton Color="Color.Default" Variant="Variant.Text" OnClick="Cancel">
|
||||
انصراف
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,115 @@
|
||||
using BackOffice.Services.Blog;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Blog.Components;
|
||||
|
||||
public partial class BlogPostEditDialog
|
||||
{
|
||||
[CascadingParameter] IMudDialogInstance DialogInstance { get; set; } = default!;
|
||||
[Inject] public IBlogPostService BlogPostService { get; set; } = default!;
|
||||
|
||||
[Parameter] public BlogPostEditDto Model { get; set; } = new();
|
||||
[Parameter] public long? PostId { get; set; }
|
||||
[Parameter] public bool IsEditMode { get; set; }
|
||||
[Parameter] public List<BlogCategoryItemDto> Categories { get; set; } = new();
|
||||
|
||||
private bool _isEditMode => IsEditMode && PostId.HasValue;
|
||||
private MudForm? _form;
|
||||
private bool _isValid = true;
|
||||
private bool _loading;
|
||||
|
||||
private IBrowserFile? _imageFile;
|
||||
private byte[]? _imageBuffer;
|
||||
private string? _imagePreview;
|
||||
private readonly long _maxAllowedSize = (1024 * 1024) * 5;
|
||||
|
||||
private IEnumerable<long> _selectedCategoryIds
|
||||
{
|
||||
get => Model.CategoryIds;
|
||||
set => Model.CategoryIds = value.ToList();
|
||||
}
|
||||
|
||||
private string GetCategorySelectionText(List<string> selectedValues)
|
||||
{
|
||||
if (selectedValues == null || !selectedValues.Any())
|
||||
return "انتخاب دستهبندی";
|
||||
|
||||
var names = new List<string>();
|
||||
foreach (var val in selectedValues)
|
||||
{
|
||||
if (long.TryParse(val, out var id))
|
||||
{
|
||||
var cat = Categories.FirstOrDefault(c => c.Id == id);
|
||||
if (cat != null) names.Add(cat.Title);
|
||||
}
|
||||
}
|
||||
|
||||
return names.Any() ? string.Join("، ", names) : "انتخاب دستهبندی";
|
||||
}
|
||||
|
||||
private async Task OnImageSelected(IBrowserFile? file)
|
||||
{
|
||||
_imageFile = file;
|
||||
if (file != null)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await file.OpenReadStream(_maxAllowedSize).CopyToAsync(ms);
|
||||
_imageBuffer = ms.ToArray();
|
||||
_imagePreview = $"data:{file.ContentType};base64," + Convert.ToBase64String(_imageBuffer);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleValidSubmit()
|
||||
{
|
||||
await SubmitAsync();
|
||||
}
|
||||
|
||||
private async Task SubmitAsync()
|
||||
{
|
||||
if (_form != null)
|
||||
{
|
||||
await _form.Validate();
|
||||
if (!_isValid) return;
|
||||
}
|
||||
|
||||
_loading = true;
|
||||
|
||||
// Handle image upload
|
||||
if (_imageFile != null && _imageBuffer != null)
|
||||
{
|
||||
Model.ImageFile = _imageBuffer;
|
||||
Model.ImageMime = _imageFile.ContentType;
|
||||
Model.ImageFileName = Path.GetFileNameWithoutExtension(_imageFile.Name);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_isEditMode && PostId.HasValue)
|
||||
{
|
||||
await BlogPostService.UpdateAsync(PostId.Value, Model);
|
||||
}
|
||||
else
|
||||
{
|
||||
await BlogPostService.CreateAsync(Model);
|
||||
}
|
||||
|
||||
DialogInstance.Close(DialogResult.Ok(true));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
DialogInstance.Cancel();
|
||||
}
|
||||
}
|
||||
@@ -7,21 +7,25 @@
|
||||
|
||||
<BasePageComponent @ref="_basePage" OnClearFilterClick="OnFilterCleared" OnSubmitClick="OnFilterSubmit">
|
||||
<Filters>
|
||||
<MudTextField T="string" Clearable="true" Label="عنوان" @bind-Value="@_request.Filter.Title" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
<CategoryAutoComplete Value="_parentFilterId"
|
||||
ValueChanged="OnParentFilterChanged"
|
||||
Label="فیلتر بر اساس دستهبندی والد" />
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField T="string" Clearable="true" Label="عنوان" @bind-Value="@_request.Filter.Title" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<CategoryAutoComplete Value="_parentFilterId"
|
||||
ValueChanged="OnParentFilterChanged"
|
||||
Label="فیلتر بر اساس دستهبندی والد" />
|
||||
</MudItem>
|
||||
</Filters>
|
||||
<Content>
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="3">
|
||||
<MudPaper Class="pa-2" Style="height:72vh; overflow:auto;">
|
||||
<MudPaper Class="pa-2" Style="height:calc(100vh - 240px); overflow:auto;">
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2">درخت دستهبندیها</MudText>
|
||||
<MudList T="string" Dense="true">
|
||||
<MudListItem T="string"
|
||||
OnClick="@(() => OnTreeNodeSelected(null))"
|
||||
Selected="@(_parentFilterId == null)"
|
||||
Style="cursor:pointer;">
|
||||
>
|
||||
<MudText Typo="Typo.body2">همه دستهبندیها</MudText>
|
||||
</MudListItem>
|
||||
@foreach (var node in _treeNodes)
|
||||
@@ -29,7 +33,7 @@
|
||||
<MudListItem T="string"
|
||||
OnClick="@(() => OnTreeNodeSelected(node.Id))"
|
||||
Selected="@(_parentFilterId == node.Id)"
|
||||
Style="cursor:pointer;">
|
||||
>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
<div style="width:@(node.Level * 16)px;"></div>
|
||||
<MudText Typo="Typo.body2">@node.Title</MudText>
|
||||
@@ -44,13 +48,14 @@
|
||||
ServerData="@(new Func<GridState<DataModel>, Task<GridData<DataModel>>>(ServerReload))"
|
||||
Hover="true"
|
||||
@ref="_gridData"
|
||||
Height="72vh">
|
||||
Height="calc(100vh - 240px)">
|
||||
<ColGroup>
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
<col style="width: 80px;" />
|
||||
</ColGroup>
|
||||
<ToolBarContent>
|
||||
@@ -62,7 +67,7 @@
|
||||
Size="Size.Large"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="CreateNew"
|
||||
Style="cursor:pointer;">
|
||||
>
|
||||
افزودن
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
@@ -85,6 +90,8 @@
|
||||
<MudText Typo="Typo.body2">@parentTitle</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<PropertyColumn Property="x => x.SortOrder" Title="ترتیب" />
|
||||
<PropertyColumn Property="x => x.ProductCount" Title="تعداد محصولات" />
|
||||
<TemplateColumn Title="فعال؟">
|
||||
<CellTemplate>
|
||||
<MudChip Color="@(context.Item.IsActive ? Color.Success : Color.Error)"
|
||||
@@ -102,15 +109,16 @@
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => Update(context.Item))"
|
||||
Style="cursor:pointer;" />
|
||||
/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="حذف">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.DeleteOutline"
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
Disabled="@HasChildren(context.Item.Id)"
|
||||
OnClick="@(() => OnDelete(context.Item))"
|
||||
Style="cursor:pointer;" />
|
||||
/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="مدیریت محصولات این دسته (درگ و دراپ)">
|
||||
@@ -118,17 +126,29 @@
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => OpenCategoryProducts(context.Item))"
|
||||
Style="cursor:pointer;" />
|
||||
/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="افزودن زیردسته">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.CreateNewFolder"
|
||||
Size="Size.Small"
|
||||
Color="Color.Primary"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => CreateSubcategory(context.Item))"
|
||||
/>
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="DataModel"
|
||||
PageSizeOptions="@(new int[] { 30, 60, 90 })"
|
||||
PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد سطرهای صفحه" />
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudItem>
|
||||
|
||||
@@ -152,12 +152,30 @@ public partial class CategoryMainPage
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateSubcategory(DataModel parent)
|
||||
{
|
||||
var dialog = await DialogService.ShowAsync<CreateOrUpdateCategoryDialog>(
|
||||
$"افزودن زیردسته برای «{parent.Title}»",
|
||||
new DialogParameters<CreateOrUpdateCategoryDialog>
|
||||
{
|
||||
{ x => x.Model, new CreateNewCategoryRequest { IsActive = true, ParentId = parent.Id } }
|
||||
},
|
||||
new DialogOptions { CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.Small });
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
await ReloadData();
|
||||
await LoadTreeAsync();
|
||||
Snackbar.Add("زیردسته با موفقیت ایجاد شد.", Severity.Success);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Update(DataModel model)
|
||||
{
|
||||
var getResponse = await CategoryContract.GetCategoryAsync(new GetCategoryRequest { Id = model.Id });
|
||||
|
||||
var editModel = getResponse.Adapt<UpdateCategoryRequest>();
|
||||
// Console.WriteLine(JsonSerializer.Serialize(editModel));
|
||||
var dialog = await DialogService.ShowAsync<CreateOrUpdateCategoryDialog>(
|
||||
"ویرایش دستهبندی",
|
||||
new DialogParameters<CreateOrUpdateCategoryDialog>
|
||||
@@ -180,7 +198,7 @@ public partial class CategoryMainPage
|
||||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small };
|
||||
bool? result = await DialogService.ShowMessageBox(
|
||||
"اخطار",
|
||||
"آیا از حذف این دستهبندی مطمئن هستید؟",
|
||||
$"آیا از حذف دستهبندی «{model.Title}» مطمئن هستید؟",
|
||||
yesText: "حذف",
|
||||
cancelText: "لغو",
|
||||
options: options);
|
||||
@@ -238,4 +256,9 @@ public partial class CategoryMainPage
|
||||
{
|
||||
Navigation.NavigateTo($"{RouteConstance.CategoryProducts}{model.Id}");
|
||||
}
|
||||
|
||||
private bool HasChildren(long categoryId)
|
||||
{
|
||||
return _treeNodes.Any(n => n.ParentId == categoryId);
|
||||
}
|
||||
}
|
||||
@@ -3,44 +3,73 @@
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6">
|
||||
<MudIcon Icon="@(IsEdit ? Icons.Material.Filled.Edit : Icons.Material.Filled.Add)" Class="ml-2" />
|
||||
@(IsEdit ? "ویرایش دستهبندی" : "ایجاد دستهبندی جدید")
|
||||
</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudStack Spacing="2">
|
||||
<MudTextField T="string"
|
||||
@bind-Value="Name"
|
||||
Label="نام لاتین"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
<MudForm @ref="_form" @bind-IsValid="@_isValid">
|
||||
<MudStack Spacing="2">
|
||||
<MudTextField T="string"
|
||||
@bind-Value="Name"
|
||||
Label="نام لاتین"
|
||||
Required="true"
|
||||
RequiredError="نام لاتین الزامی است"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
|
||||
<MudTextField T="string"
|
||||
@bind-Value="Title"
|
||||
Label="عنوان"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
<MudTextField T="string"
|
||||
@bind-Value="Title"
|
||||
Label="عنوان"
|
||||
Required="true"
|
||||
RequiredError="عنوان الزامی است"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
|
||||
<MudTextField T="string"
|
||||
@bind-Value="Description"
|
||||
Label="توضیحات"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Lines="3"
|
||||
TextArea="true" />
|
||||
<MudTextField T="string"
|
||||
@bind-Value="Description"
|
||||
Label="توضیحات"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Lines="3"
|
||||
TextArea="true" />
|
||||
|
||||
<CategoryAutoComplete Value="ParentId"
|
||||
ValueChanged="OnParentChanged"
|
||||
Label="دستهبندی والد (اختیاری)" />
|
||||
<MudTextField T="string"
|
||||
@bind-Value="ImagePath"
|
||||
Label="آدرس تصویر"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
|
||||
<MudNumericField T="int"
|
||||
@bind-Value="SortOrder"
|
||||
HideSpinButtons="true"
|
||||
Label="ترتیب نمایش"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
<CategoryAutoComplete Value="ParentId"
|
||||
ValueChanged="OnParentChanged"
|
||||
Label="دستهبندی والد (اختیاری)" />
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">برای دسته اصلی خالی بگذارید</MudText>
|
||||
|
||||
<MudCheckBox T="bool" @bind-Value="IsActive" Label="فعال" />
|
||||
</MudStack>
|
||||
<MudNumericField T="int"
|
||||
@bind-Value="SortOrder"
|
||||
HideSpinButtons="true"
|
||||
Label="ترتیب نمایش"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
HelperText="عدد کمتر = اولویت بالاتر" />
|
||||
|
||||
<MudSwitch T="bool" @bind-Value="IsActive" Label="فعال" Color="Color.Success" />
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel">لغو</MudButton>
|
||||
<MudButton Color="Color.Primary" OnClick="Save">ثبت</MudButton>
|
||||
<MudButton OnClick="Cancel" Variant="Variant.Text">لغو</MudButton>
|
||||
<MudButton Color="Color.Primary"
|
||||
Variant="Variant.Filled"
|
||||
OnClick="Save"
|
||||
Disabled="@(!_isValid || _isSaving)">
|
||||
@if (_isSaving)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="ml-2" />
|
||||
}
|
||||
@(IsEdit ? "ذخیره تغییرات" : "ایجاد دستهبندی")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@@ -14,9 +14,14 @@ public partial class CreateOrUpdateCategoryDialog
|
||||
|
||||
private bool IsEdit => EditModel != null;
|
||||
|
||||
private MudForm? _form;
|
||||
private bool _isValid;
|
||||
private bool _isSaving;
|
||||
|
||||
private string Name { get; set; } = string.Empty;
|
||||
private string Title { get; set; } = string.Empty;
|
||||
private string? Description { get; set; }
|
||||
private string? ImagePath { get; set; }
|
||||
private long? ParentId { get; set; }
|
||||
private bool IsActive { get; set; } = true;
|
||||
private int SortOrder { get; set; }
|
||||
@@ -28,9 +33,9 @@ public partial class CreateOrUpdateCategoryDialog
|
||||
Name = EditModel.Name;
|
||||
Title = EditModel.Title;
|
||||
Description = EditModel.Description;
|
||||
ImagePath = EditModel.ImagePath;
|
||||
ParentId = EditModel.ParentId;
|
||||
IsActive = EditModel.IsActive;
|
||||
Console.WriteLine(EditModel.IsActive);
|
||||
SortOrder = EditModel.SortOrder;
|
||||
}
|
||||
else if (Model != null)
|
||||
@@ -38,6 +43,7 @@ public partial class CreateOrUpdateCategoryDialog
|
||||
Name = Model.Name;
|
||||
Title = Model.Title;
|
||||
Description = Model.Description;
|
||||
ImagePath = Model.ImagePath;
|
||||
ParentId = Model.ParentId;
|
||||
IsActive = Model.IsActive;
|
||||
SortOrder = Model.SortOrder;
|
||||
@@ -46,37 +52,60 @@ public partial class CreateOrUpdateCategoryDialog
|
||||
|
||||
private async Task Save()
|
||||
{
|
||||
if (IsEdit && EditModel != null)
|
||||
if (_form != null)
|
||||
{
|
||||
var request = new UpdateCategoryRequest
|
||||
{
|
||||
Id = EditModel.Id,
|
||||
Name = Name,
|
||||
Title = Title,
|
||||
Description = Description,
|
||||
ParentId = ParentId,
|
||||
IsActive = IsActive,
|
||||
SortOrder = SortOrder
|
||||
};
|
||||
|
||||
await CategoryContract.UpdateCategoryAsync(request);
|
||||
}
|
||||
else
|
||||
{
|
||||
var request = new CreateNewCategoryRequest
|
||||
{
|
||||
Name = Name,
|
||||
Title = Title,
|
||||
Description = Description,
|
||||
ParentId = ParentId,
|
||||
IsActive = IsActive,
|
||||
SortOrder = SortOrder
|
||||
};
|
||||
|
||||
await CategoryContract.CreateNewCategoryAsync(request);
|
||||
await _form.Validate();
|
||||
if (!_isValid) return;
|
||||
}
|
||||
|
||||
MudDialog.Close(DialogResult.Ok(true));
|
||||
_isSaving = true;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
if (IsEdit && EditModel != null)
|
||||
{
|
||||
var request = new UpdateCategoryRequest
|
||||
{
|
||||
Id = EditModel.Id,
|
||||
Name = Name,
|
||||
Title = Title,
|
||||
Description = Description,
|
||||
ImagePath = ImagePath,
|
||||
ParentId = ParentId,
|
||||
IsActive = IsActive,
|
||||
SortOrder = SortOrder
|
||||
};
|
||||
|
||||
await CategoryContract.UpdateCategoryAsync(request);
|
||||
}
|
||||
else
|
||||
{
|
||||
var request = new CreateNewCategoryRequest
|
||||
{
|
||||
Name = Name,
|
||||
Title = Title,
|
||||
Description = Description,
|
||||
ImagePath = ImagePath,
|
||||
ParentId = ParentId,
|
||||
IsActive = IsActive,
|
||||
SortOrder = SortOrder
|
||||
};
|
||||
|
||||
await CategoryContract.CreateNewCategoryAsync(request);
|
||||
}
|
||||
|
||||
MudDialog.Close(DialogResult.Ok(true));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSaving = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private Task OnParentChanged(long? parentId)
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
@page "/club/features"
|
||||
@attribute [Authorize(Roles = "Administrator")]
|
||||
|
||||
@using CMSMicroservice.Protobuf.Protos.Configuration
|
||||
@using CMSMicroservice.Protobuf.Protos.ClubMembership
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
|
||||
@inject ConfigurationContract.ConfigurationContractClient ConfigClient
|
||||
@inject ClubMembershipContract.ClubMembershipContractClient ClubClient
|
||||
|
||||
<div>
|
||||
<MudText Typo="Typo.h5" Class="mb-3">فیچرهای باشگاه</MudText>
|
||||
|
||||
<MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true"
|
||||
PanelClass="pt-4" @bind-ActivePanelIndex="_activeTab">
|
||||
|
||||
@* ── تب اول: لیست فیچرها ── *@
|
||||
<MudTabPanel Text="فیچرهای تعریفشده" Icon="@Icons.Material.Filled.Star">
|
||||
@if (_loadingFeatures)
|
||||
{
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
|
||||
}
|
||||
else if (_features.Count == 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text">فیچری تعریف نشده است.</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudGrid Spacing="2">
|
||||
@foreach (var feature in _features)
|
||||
{
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
|
||||
<MudStack Spacing="1">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.subtitle1"><strong>@feature.Title</strong></MudText>
|
||||
<MudChip T="string" Size="Size.Small"
|
||||
Color="@(feature.IsEnabled ? Color.Success : Color.Error)">
|
||||
@(feature.IsEnabled ? "فعال" : "غیرفعال")
|
||||
</MudChip>
|
||||
</MudStack>
|
||||
@if (!string.IsNullOrWhiteSpace(feature.Description))
|
||||
{
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">@feature.Description</MudText>
|
||||
}
|
||||
<MudText Typo="Typo.caption">ترتیب: @feature.DisplayOrder</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
}
|
||||
</MudTabPanel>
|
||||
|
||||
@* ── تب دوم: فیچرهای کاربران ── *@
|
||||
<MudTabPanel Text="فیچرهای کاربران" Icon="@Icons.Material.Filled.PeopleAlt">
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center" Class="mb-3">
|
||||
<UserAutoComplete Label="جستجوی کاربر" @bind-SelectedUserId="_featureFilterUserId" />
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Search"
|
||||
OnClick="LoadUserFeatures">جستجو</MudButton>
|
||||
</MudStack>
|
||||
|
||||
@if (_loadingUserFeatures)
|
||||
{
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
|
||||
}
|
||||
else if (_featureFilterUserId == null || _featureFilterUserId == 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text">لطفاً یک کاربر انتخاب کنید.</MudAlert>
|
||||
}
|
||||
else if (_userFeatures.Count == 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text">فیچری برای این کاربر یافت نشد.</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudDataGrid T="UserClubFeatureModel" Items="_userFeatures"
|
||||
Hover="true" Dense="true"
|
||||
Height="calc(100vh - 340px)">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
<PropertyColumn Property="x => x.FeatureTitle" Title="عنوان فیچر" />
|
||||
<TemplateColumn Title="توضیحات">
|
||||
<CellTemplate>
|
||||
<MudText Typo="Typo.body2">@(string.IsNullOrWhiteSpace(context.Item.FeatureDescription) ? "—" : context.Item.FeatureDescription)</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="وضعیت">
|
||||
<CellTemplate>
|
||||
<MudSwitch T="bool" Value="context.Item.IsActive"
|
||||
Color="Color.Success"
|
||||
ValueChanged="@(val => ToggleFeature(context.Item, val))" />
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="تاریخ اختصاص">
|
||||
<CellTemplate>
|
||||
@if (context.Item.GrantedAt != null)
|
||||
{
|
||||
@context.Item.GrantedAt.ToDateTime().ToLocalTime().MiladiToJalali()
|
||||
}
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<PropertyColumn Property="x => x.Notes" Title="یادداشت" />
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="UserClubFeatureModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
}
|
||||
</MudTabPanel>
|
||||
</MudTabs>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private int _activeTab;
|
||||
private bool _loadingFeatures;
|
||||
private bool _loadingUserFeatures;
|
||||
|
||||
// Features tab
|
||||
private List<ClubFeatureModel> _features = new();
|
||||
|
||||
// User features tab
|
||||
private long? _featureFilterUserId;
|
||||
private List<UserClubFeatureModel> _userFeatures = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadFeatures();
|
||||
}
|
||||
|
||||
private async Task LoadFeatures()
|
||||
{
|
||||
_loadingFeatures = true;
|
||||
try
|
||||
{
|
||||
var result = await ConfigClient.GetClubFeaturesAsync(new Google.Protobuf.WellKnownTypes.Empty());
|
||||
_features = result?.Features?.ToList() ?? new();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری فیچرها: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loadingFeatures = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadUserFeatures()
|
||||
{
|
||||
if (_featureFilterUserId == null || _featureFilterUserId == 0) return;
|
||||
|
||||
_loadingUserFeatures = true;
|
||||
try
|
||||
{
|
||||
var result = await ClubClient.GetUserClubFeaturesAsync(new GetUserClubFeaturesRequest
|
||||
{
|
||||
UserId = _featureFilterUserId.Value
|
||||
});
|
||||
_userFeatures = result?.Features?.ToList() ?? new();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loadingUserFeatures = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ToggleFeature(UserClubFeatureModel feature, bool isActive)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ClubClient.ToggleUserClubFeatureAsync(new ToggleUserClubFeatureRequest
|
||||
{
|
||||
UserId = feature.UserId,
|
||||
ClubFeatureId = feature.ClubFeatureId,
|
||||
IsActive = isActive
|
||||
});
|
||||
|
||||
feature.IsActive = isActive;
|
||||
Snackbar.Add($"فیچر «{feature.FeatureTitle}» {(isActive ? "فعال" : "غیرفعال")} شد", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
@page "/club"
|
||||
@page "/club/members"
|
||||
@page "/club/statistics"
|
||||
@attribute [Authorize]
|
||||
|
||||
<div>
|
||||
<MudText Typo="Typo.h5" Class="mb-3">باشگاه مشتریان</MudText>
|
||||
|
||||
<MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true"
|
||||
PanelClass="pt-4" @bind-ActivePanelIndex="_activeTab">
|
||||
<MudTabPanel Text="اعضای باشگاه" Icon="@Icons.Material.Filled.Groups">
|
||||
<ClubMembers />
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="آمار باشگاه" Icon="@Icons.Material.Filled.PieChart">
|
||||
<Statistics />
|
||||
</MudTabPanel>
|
||||
</MudTabs>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private int _activeTab;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
var uri = Navigation.ToBaseRelativePath(Navigation.Uri).ToLowerInvariant().TrimEnd('/');
|
||||
_activeTab = uri switch
|
||||
{
|
||||
"club/statistics" => 1,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
@page "/club/members"
|
||||
@* Merged into /club *@
|
||||
@attribute [Authorize]
|
||||
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
@using BackOffice.Pages.Club.Components
|
||||
@using CMSMicroservice.Protobuf.Protos.ClubMembership
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<div>
|
||||
<MudText Typo="Typo.h4" Class="mb-4">مدیریت اعضای باشگاه</MudText>
|
||||
|
||||
<MudDataGrid T="ClubMembershipModel"
|
||||
ServerData="@(new Func<GridState<ClubMembershipModel>, Task<GridData<ClubMembershipModel>>>(ServerReload))"
|
||||
Hover="true"
|
||||
@ref="_gridData"
|
||||
Height="75vh">
|
||||
Height="calc(100vh - 240px)">
|
||||
<ToolBarContent>
|
||||
<MudText Typo="Typo.h6">لیست اعضا</MudText>
|
||||
<MudSpacer />
|
||||
@@ -32,6 +32,12 @@
|
||||
StartIcon="@Icons.Material.Filled.Add">
|
||||
فعالسازی عضو جدید
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Info"
|
||||
OnClick="ExportToExcel"
|
||||
StartIcon="@Icons.Material.Filled.FileDownload">
|
||||
خروجی Excel
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</ToolBarContent>
|
||||
<Columns>
|
||||
@@ -58,14 +64,14 @@
|
||||
|
||||
<PropertyColumn Property="x => x.ActivatedAt" Title="تاریخ فعالسازی">
|
||||
<CellTemplate>
|
||||
@context.Item.ActivatedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd")
|
||||
@(context.Item.ActivatedAt != null ? context.Item.ActivatedAt.ToDateTime().ToLocalTime().MiladiToJalali() : "—")
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.ExpiresAt" Title="تاریخ انقضا">
|
||||
<CellTemplate>
|
||||
<MudText Color="@(context.Item.IsExpired ? Color.Error : Color.Success)">
|
||||
@context.Item.ExpiresAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd")
|
||||
@(context.Item.ExpiresAt != null ? context.Item.ExpiresAt.ToDateTime().ToLocalTime().MiladiToJalali() : "—")
|
||||
</MudText>
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
@@ -102,11 +108,14 @@
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="ClubMembershipModel"
|
||||
PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد سطرهای صفحه" />
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudContainer>
|
||||
</div>
|
||||
|
||||
@@ -3,12 +3,16 @@ using MudBlazor;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using BackOffice.Pages.Club.Components;
|
||||
using CMSMicroservice.Protobuf.Protos.ClubMembership;
|
||||
using Microsoft.JSInterop;
|
||||
using System.Text;
|
||||
using DateTimeConverterCL;
|
||||
|
||||
namespace BackOffice.Pages.Club;
|
||||
|
||||
public partial class ClubMembers
|
||||
{
|
||||
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubContract { get; set; }
|
||||
[Inject] public IJSRuntime JsRuntime { get; set; }
|
||||
|
||||
private MudDataGrid<ClubMembershipModel> _gridData;
|
||||
private bool _filterIsActive = true;
|
||||
@@ -108,4 +112,61 @@ public partial class ClubMembers
|
||||
await ApplyFilter();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExportToExcel()
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new GetAllClubMembershipsRequest
|
||||
{
|
||||
PageIndex = 1,
|
||||
PageSize = 1000,
|
||||
IsActive = _filterIsActive
|
||||
};
|
||||
|
||||
var result = await ClubContract.GetAllClubMembershipsAsync(request);
|
||||
|
||||
if (result?.Models == null || !result.Models.Any())
|
||||
{
|
||||
Snackbar.Add("دادهای برای خروجی وجود ندارد.", Severity.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("شناسه,کاربر,نام کاربر,پکیج,کد فعالسازی,تاریخ فعالسازی,تاریخ انقضا,فعال,منقضی");
|
||||
|
||||
foreach (var m in result.Models)
|
||||
{
|
||||
sb.AppendLine(string.Join(",",
|
||||
m.Id,
|
||||
m.UserId,
|
||||
EscapeCsv(m.UserName),
|
||||
EscapeCsv(m.PackageName),
|
||||
EscapeCsv(m.ActivationCode),
|
||||
m.ActivatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime(),
|
||||
m.ExpiresAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime(),
|
||||
m.IsActive ? "فعال" : "غیرفعال",
|
||||
m.IsExpired ? "بله" : "خیر"));
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
var filename = $"club-members-{DateTime.Now:yyyyMMddHHmmss}.csv";
|
||||
|
||||
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
|
||||
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private static string EscapeCsv(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return "";
|
||||
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
|
||||
return $"\"{value.Replace("\"", "\"\"")}\"";
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,13 +41,13 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>تاریخ فعالسازی:</strong></td>
|
||||
<td>@Member.ActivatedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm")</td>
|
||||
<td>@Member.ActivatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>تاریخ انقضا:</strong></td>
|
||||
<td>
|
||||
<MudText Color="@(Member.IsExpired ? Color.Error : Color.Success)">
|
||||
@Member.ExpiresAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm")
|
||||
@Member.ExpiresAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()
|
||||
</MudText>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -82,10 +82,38 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>تاریخ ایجاد:</strong></td>
|
||||
<td>@Member.Created.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss")</td>
|
||||
<td>@Member.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
|
||||
@if (_historyLoading)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" Color="Color.Primary" Class="my-2" />
|
||||
}
|
||||
else if (_history.Any())
|
||||
{
|
||||
<MudTable T="ClubMembershipHistoryModel" Items="_history" Dense="true" Hover="true" Class="mt-2">
|
||||
<HeaderContent>
|
||||
<MudTh>عملیات</MudTh>
|
||||
<MudTh>تاریخ</MudTh>
|
||||
<MudTh>دلیل</MudTh>
|
||||
<MudTh>توسط</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="عملیات">
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Info">@context.Action</MudChip>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="تاریخ">@context.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</MudTd>
|
||||
<MudTd DataLabel="دلیل">@(string.IsNullOrEmpty(context.Reason) ? "-" : context.Reason)</MudTd>
|
||||
<MudTd DataLabel="توسط">@(string.IsNullOrEmpty(context.PerformedBy) ? "-" : context.PerformedBy)</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="mt-2">تاریخچهای یافت نشد.</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
</MudStack>
|
||||
}
|
||||
@@ -98,6 +126,39 @@
|
||||
@code {
|
||||
[CascadingParameter] IMudDialogInstance MudDialog { get; set; }
|
||||
[Parameter] public ClubMembershipModel Member { get; set; }
|
||||
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubClient { get; set; }
|
||||
|
||||
private List<ClubMembershipHistoryModel> _history = new();
|
||||
private bool _historyLoading;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadHistory();
|
||||
}
|
||||
|
||||
private async Task LoadHistory()
|
||||
{
|
||||
_historyLoading = true;
|
||||
try
|
||||
{
|
||||
var request = new GetClubMembershipHistoryRequest
|
||||
{
|
||||
UserId = Member.UserId,
|
||||
PageIndex = 0,
|
||||
PageSize = 50
|
||||
};
|
||||
var response = await ClubClient.GetClubMembershipHistoryAsync(request);
|
||||
_history = response.Models?.ToList() ?? new();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_history = new();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_historyLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Close() => MudDialog.Close();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
@page "/club/statistics"
|
||||
@* Merged into /club *@
|
||||
|
||||
@using MudBlazor
|
||||
@using CMSMicroservice.Protobuf.Protos.ClubMembership
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<div>
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">آمار باشگاه</MudText>
|
||||
<MudText Typo="Typo.body1" Color="Color.Secondary" Class="mb-4">
|
||||
نمای کلی از وضعیت عضویتهای باشگاه
|
||||
@@ -139,7 +139,7 @@
|
||||
<MudTh>عملیات</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="تاریخ">@context.ActivatedAt.ToString("yyyy/MM/dd")</MudTd>
|
||||
<MudTd DataLabel="تاریخ">@context.ActivatedAt.MiladiToJalali()</MudTd>
|
||||
<MudTd DataLabel="شناسه کاربر">@context.UserId</MudTd>
|
||||
<MudTd DataLabel="نام کاربر">@context.UserName</MudTd>
|
||||
<MudTd DataLabel="پکیج">
|
||||
@@ -166,7 +166,7 @@
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
}
|
||||
</MudContainer>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubClient { get; set; }
|
||||
|
||||
@@ -89,12 +89,12 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>تاریخ ایجاد:</strong></td>
|
||||
<td>@Payout.Created.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss")</td>
|
||||
<td>@Payout.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>آخرین ویرایش:</strong></td>
|
||||
@* CMS UserCommissionPayoutModel does not have LastModified - using Created as fallback *@
|
||||
<td>@Payout.Created.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss")</td>
|
||||
<td>@Payout.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudStack Spacing="3">
|
||||
<MudAlert Severity="Severity.Warning" Dense="true">
|
||||
درخواست برداشت @Amount.ToString("N0") ریال برای @UserName رد خواهد شد.
|
||||
</MudAlert>
|
||||
<MudTextField @bind-Value="_reason"
|
||||
Label="دلیل رد درخواست"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="3"
|
||||
Required="true"
|
||||
RequiredError="لطفاً دلیل رد درخواست را وارد کنید" />
|
||||
</MudStack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel">انصراف</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Error" OnClick="Submit"
|
||||
Disabled="@string.IsNullOrWhiteSpace(_reason)">
|
||||
رد درخواست
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
[Parameter] public string UserName { get; set; } = "";
|
||||
[Parameter] public long Amount { get; set; }
|
||||
|
||||
private string _reason = "رد توسط مدیر";
|
||||
|
||||
private void Cancel() => MudDialog.Cancel();
|
||||
private void Submit() => MudDialog.Close(DialogResult.Ok(_reason));
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudStack Spacing="3">
|
||||
<MudPaper Class="pa-3" Elevation="0" Outlined="true">
|
||||
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="mb-2">اطلاعات کاربر</MudText>
|
||||
<MudSimpleTable Dense="true">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>نام کاربر:</strong></td>
|
||||
<td>@Request.UserName</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>شناسه کاربر:</strong></td>
|
||||
<td>@Request.UserId</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
</MudPaper>
|
||||
|
||||
<MudPaper Class="pa-3" Elevation="0" Outlined="true">
|
||||
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="mb-2">اطلاعات درخواست</MudText>
|
||||
<MudSimpleTable Dense="true">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>شناسه درخواست:</strong></td>
|
||||
<td>@Request.Id</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>مبلغ:</strong></td>
|
||||
<td><MudText Color="Color.Primary"><strong>@Request.Amount.ToString("N0") ریال</strong></MudText></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>روش برداشت:</strong></td>
|
||||
<td>
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Info">@MethodText</MudChip>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>وضعیت:</strong></td>
|
||||
<td>
|
||||
<MudChip T="string" Size="Size.Small" Color="@GetStatusColor()">@StatusText</MudChip>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>تاریخ درخواست:</strong></td>
|
||||
<td>@Request.RequestedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td>
|
||||
</tr>
|
||||
@if (Request.ProcessedDate.HasValue)
|
||||
{
|
||||
<tr>
|
||||
<td><strong>تاریخ پردازش:</strong></td>
|
||||
<td>@Request.ProcessedDate.Value.MiladiToJalaliWithTime()</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
</MudPaper>
|
||||
|
||||
<MudPaper Class="pa-3" Elevation="0" Outlined="true">
|
||||
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="mb-2">اطلاعات بانکی</MudText>
|
||||
<MudSimpleTable Dense="true">
|
||||
<tbody>
|
||||
@if (!string.IsNullOrWhiteSpace(Request.IbanNumber))
|
||||
{
|
||||
<tr>
|
||||
<td><strong>شماره شبا:</strong></td>
|
||||
<td>@Request.IbanNumber</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(Request.BankReferenceId))
|
||||
{
|
||||
<tr>
|
||||
<td><strong>شماره مرجع بانک:</strong></td>
|
||||
<td>@Request.BankReferenceId</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(Request.BankTrackingCode))
|
||||
{
|
||||
<tr>
|
||||
<td><strong>کد پیگیری بانکی:</strong></td>
|
||||
<td>@Request.BankTrackingCode</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(Request.PaymentFailureReason))
|
||||
{
|
||||
<tr>
|
||||
<td><strong>دلیل خطای پرداخت:</strong></td>
|
||||
<td><MudText Color="Color.Error">@Request.PaymentFailureReason</MudText></td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(Request.AdminNote))
|
||||
{
|
||||
<tr>
|
||||
<td><strong>یادداشت مدیر:</strong></td>
|
||||
<td>@Request.AdminNote</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
</MudPaper>
|
||||
</MudStack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel">بستن</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
[Parameter] public WithdrawalRequestModel Request { get; set; } = null!;
|
||||
[Parameter] public string StatusText { get; set; } = "";
|
||||
[Parameter] public string MethodText { get; set; } = "";
|
||||
|
||||
private Color GetStatusColor()
|
||||
{
|
||||
return Request.Status switch
|
||||
{
|
||||
0 => Color.Warning,
|
||||
1 => Color.Info,
|
||||
2 => Color.Warning,
|
||||
3 => Color.Success,
|
||||
4 => Color.Error,
|
||||
5 => Color.Default,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private void Cancel() => MudDialog.Cancel();
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
@using MudBlazor
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<div>
|
||||
<MudText Typo="Typo.h4" Class="mb-4">داشبورد کمیسیون</MudText>
|
||||
|
||||
@if (_isLoading)
|
||||
@@ -90,7 +90,7 @@
|
||||
|
||||
@if (_availableWeeks?.CurrentWeek != null)
|
||||
{
|
||||
<MudSelectItem Value="@_availableWeeks.CurrentWeek.WeekDefinitionId">
|
||||
<MudSelectItem T="long" Value="@_availableWeeks.CurrentWeek.WeekDefinitionId">
|
||||
<MudText>
|
||||
<MudIcon Icon="@Icons.Material.Filled.Today" Size="Size.Small" Class="ml-2" />
|
||||
@_availableWeeks.CurrentWeek.DisplayText
|
||||
@@ -104,7 +104,7 @@
|
||||
<MudListSubheader>هفتههای محاسبه شده</MudListSubheader>
|
||||
@foreach (var week in _availableWeeks.CalculatedWeeks)
|
||||
{
|
||||
<MudSelectItem Value="@week.WeekDefinitionId">
|
||||
<MudSelectItem T="long" Value="@week.WeekDefinitionId">
|
||||
<MudText>
|
||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" Class="ml-2" />
|
||||
@week.DisplayText
|
||||
@@ -125,7 +125,7 @@
|
||||
<MudListSubheader>هفتههای محاسبه نشده</MudListSubheader>
|
||||
@foreach (var week in _availableWeeks.PendingWeeks)
|
||||
{
|
||||
<MudSelectItem Value="@week.WeekDefinitionId">
|
||||
<MudSelectItem T="long" Value="@week.WeekDefinitionId">
|
||||
<MudText>
|
||||
<MudIcon Icon="@Icons.Material.Filled.AccessTime" Color="Color.Warning" Size="Size.Small" Class="ml-2" />
|
||||
@week.DisplayText
|
||||
@@ -140,7 +140,7 @@
|
||||
<MudListSubheader>هفتههای آینده</MudListSubheader>
|
||||
@foreach (var week in _availableWeeks.FutureWeeks)
|
||||
{
|
||||
<MudSelectItem Value="@week.WeekDefinitionId">
|
||||
<MudSelectItem T="long" Value="@week.WeekDefinitionId">
|
||||
<MudText>
|
||||
<MudIcon Icon="@Icons.Material.Filled.CalendarMonth" Color="Color.Info" Size="Size.Small" Class="ml-2" />
|
||||
@week.DisplayText
|
||||
@@ -204,7 +204,7 @@
|
||||
<td>
|
||||
@if (_poolData.CalculatedAt != null)
|
||||
{
|
||||
@_poolData.CalculatedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm")
|
||||
@_poolData.CalculatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -214,7 +214,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>تاریخ ایجاد:</strong></td>
|
||||
<td>@_poolData.Created.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm")</td>
|
||||
<td>@_poolData.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
@@ -247,4 +247,4 @@
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
</MudContainer>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using CMSMicroservice.Protobuf.Protos.Commission;
|
||||
using BackOffice.Services;
|
||||
|
||||
@@ -111,7 +110,6 @@ public partial class Dashboard
|
||||
};
|
||||
|
||||
_poolData = await CommissionContract.GetWeeklyCommissionPoolAsync(request);
|
||||
Console.WriteLine(JsonSerializer.Serialize(_poolData));
|
||||
Snackbar.Add($"اطلاعات Pool هفته {_currentWeekDisplayName} بارگذاری شد", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
@using MudBlazor
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<div>
|
||||
<MudText Typo="Typo.h4" Class="mb-4"> پرداخت های کاربران</MudText>
|
||||
|
||||
<MudDataGrid T="UserCommissionPayoutModel"
|
||||
ServerData="@(new Func<GridState<UserCommissionPayoutModel>, Task<GridData<UserCommissionPayoutModel>>>(ServerReload))"
|
||||
Hover="true"
|
||||
@ref="_gridData"
|
||||
Height="75vh">
|
||||
Height="calc(100vh - 240px)">
|
||||
<ToolBarContent>
|
||||
<MudText Typo="Typo.h6">لیست پرداخت ها</MudText>
|
||||
<MudSpacer />
|
||||
@@ -28,13 +28,13 @@
|
||||
Margin="Margin.Dense"
|
||||
Clearable="true"
|
||||
Style="max-width: 150px;">
|
||||
<MudSelectItem Value="@((int?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem Value="@((int?)0)">در انتظار</MudSelectItem>
|
||||
<MudSelectItem Value="@((int?)1)">پرداخت شده</MudSelectItem>
|
||||
<MudSelectItem Value="@((int?)2)">درخواست برداشت</MudSelectItem>
|
||||
<MudSelectItem Value="@((int?)3)">برداشت شده</MudSelectItem>
|
||||
<MudSelectItem Value="@((int?)4)">شکست خورده</MudSelectItem>
|
||||
<MudSelectItem Value="@((int?)5)">لغو شده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)0)">در انتظار</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)1)">پرداخت شده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)2)">درخواست برداشت</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)3)">برداشت شده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)4)">شکست خورده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)5)">لغو شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
@@ -50,7 +50,9 @@
|
||||
<PropertyColumn Property="x => x.UserId" Title="کاربر">
|
||||
<CellTemplate>
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.body2"><strong>ID:</strong> @context.Item.UserId</MudText>
|
||||
<MudLink Href="@($"/network/user-info/{context.Item.UserId}")" Underline="Underline.Hover" Color="Color.Primary">
|
||||
<strong>ID:</strong> @context.Item.UserId
|
||||
</MudLink>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">@context.Item.UserName</MudText>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
@@ -131,11 +133,14 @@
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="UserCommissionPayoutModel"
|
||||
PageSizeOptions="@(new int[] { 10, 25, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد سطرهای صفحه" />
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudContainer>
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
@using MudBlazor
|
||||
@using CMSMicroservice.Protobuf.Protos.Commission
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<div>
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">گزارشهای هفتگی کمیسیون</MudText>
|
||||
<MudText Typo="Typo.body1" Color="Color.Secondary" Class="mb-4">
|
||||
مشاهده تاریخچه محاسبات کمیسیون هفتگی
|
||||
@@ -14,29 +15,32 @@
|
||||
<MudCardContent>
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudTextField @bind-Value="_filterFromWeek"
|
||||
Label="از هفته"
|
||||
Placeholder="2025-W01"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.DateRange" />
|
||||
<WeekNumberPicker Label="از هفته"
|
||||
@bind-SelectedWeekDefinitionId="_filterFromWeekId"
|
||||
PastWeeksCount="24" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudTextField @bind-Value="_filterToWeek"
|
||||
Label="تا هفته"
|
||||
Placeholder="2025-W48"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.DateRange" />
|
||||
<WeekNumberPicker Label="تا هفته"
|
||||
@bind-SelectedWeekDefinitionId="_filterToWeekId"
|
||||
PastWeeksCount="24" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4" Class="d-flex align-center">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
OnClick="ApplyFilter"
|
||||
StartIcon="@Icons.Material.Filled.Search"
|
||||
FullWidth="true">
|
||||
جستجو
|
||||
</MudButton>
|
||||
<MudStack Row="true" Spacing="2" Style="width:100%">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
OnClick="ApplyFilter"
|
||||
StartIcon="@Icons.Material.Filled.Search"
|
||||
FullWidth="true">
|
||||
جستجو
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Info"
|
||||
OnClick="ExportToExcel"
|
||||
StartIcon="@Icons.Material.Filled.FileDownload"
|
||||
FullWidth="true">
|
||||
خروجی Excel
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudCardContent>
|
||||
@@ -85,7 +89,7 @@
|
||||
<CellTemplate>
|
||||
@if (context.Item.CalculatedAt != null)
|
||||
{
|
||||
<MudText>@context.Item.CalculatedAt.ToDateTime().ToString("yyyy/MM/dd HH:mm")</MudText>
|
||||
<MudText>@context.Item.CalculatedAt.ToDateTime().MiladiToJalaliWithTime()</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -151,15 +155,15 @@
|
||||
</MudGrid>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudContainer>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Inject] public CommissionContract.CommissionContractClient CommissionClient { get; set; }
|
||||
|
||||
private List<WeeklyPoolReportModel> _reports = new();
|
||||
private bool _loading = false;
|
||||
private string _filterFromWeek = "";
|
||||
private string _filterToWeek = "";
|
||||
private long? _filterFromWeekId;
|
||||
private long? _filterToWeekId;
|
||||
|
||||
// Statistics
|
||||
private long _totalPoolSum = 0;
|
||||
@@ -183,14 +187,14 @@
|
||||
PageSize = 100
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_filterFromWeek))
|
||||
if (_filterFromWeekId.HasValue && _filterFromWeekId.Value > 0)
|
||||
{
|
||||
request.FromWeek = _filterFromWeek;
|
||||
request.FromWeekDefinitionId = _filterFromWeekId.Value;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_filterToWeek))
|
||||
if (_filterToWeekId.HasValue && _filterToWeekId.Value > 0)
|
||||
{
|
||||
request.ToWeek = _filterToWeek;
|
||||
request.ToWeekDefinitionId = _filterToWeekId.Value;
|
||||
}
|
||||
|
||||
var response = await CommissionClient.GetAllWeeklyPoolsAsync(request);
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
using System.Text;
|
||||
using DateTimeConverterCL;
|
||||
using Microsoft.JSInterop;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Commission;
|
||||
|
||||
public partial class WeeklyReports
|
||||
{
|
||||
private async Task ExportToExcel()
|
||||
{
|
||||
if (_reports == null || !_reports.Any())
|
||||
{
|
||||
Snackbar.Add("دادهای برای خروجی وجود ندارد.", Severity.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("هفته,مبلغ کل استخر,مجموع موجودیها,ارزش هر موجودی,وضعیت,تاریخ محاسبه");
|
||||
|
||||
foreach (var r in _reports)
|
||||
{
|
||||
sb.AppendLine(string.Join(",",
|
||||
EscapeCsv(r.WeekDisplayName),
|
||||
r.TotalPoolAmount,
|
||||
r.TotalBalances,
|
||||
r.ValuePerBalance,
|
||||
r.IsCalculated ? "محاسبه شده" : "در انتظار",
|
||||
r.CalculatedAt != null ? r.CalculatedAt.ToDateTime().MiladiToJalaliWithTime() : "-"));
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetPreamble()
|
||||
.Concat(Encoding.UTF8.GetBytes(sb.ToString()))
|
||||
.ToArray();
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
var filename = $"weekly-reports-{DateTime.Now:yyyyMMddHHmmss}.csv";
|
||||
|
||||
await jsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
|
||||
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private static string EscapeCsv(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return "";
|
||||
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
|
||||
return $"\"{value.Replace("\"", "\"\"")}\"";
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
@using System.Text
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<div>
|
||||
<MudText Typo="Typo.h4" Class="mb-1">گزارش برداشتها</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary" Class="mb-4">
|
||||
گزارش تجمیعی برداشتهای کمیسیون بر اساس بازه تاریخ و نوع دوره
|
||||
@@ -53,13 +53,13 @@
|
||||
Label="وضعیت (اختیاری)"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense">
|
||||
<MudSelectItem Value="@(null as int?)">همه وضعیتها</MudSelectItem>
|
||||
<MudSelectItem Value="0">در انتظار واریز به کیف پول</MudSelectItem>
|
||||
<MudSelectItem Value="1">واریز شده به کیف پول طلایی</MudSelectItem>
|
||||
<MudSelectItem Value="2">درخواست برداشت ثبت شده</MudSelectItem>
|
||||
<MudSelectItem Value="3">برداشت انجام شده</MudSelectItem>
|
||||
<MudSelectItem Value="4">خطا در پرداخت بانکی</MudSelectItem>
|
||||
<MudSelectItem Value="5">لغو شده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(null as int?)">همه وضعیتها</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="0">در انتظار واریز به کیف پول</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="1">واریز شده به کیف پول طلایی</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="2">درخواست برداشت ثبت شده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="3">برداشت انجام شده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="4">خطا در پرداخت بانکی</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="5">لغو شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4" Class="d-flex align-end">
|
||||
@@ -175,12 +175,12 @@
|
||||
<PropertyColumn Property="x => x.PeriodLabel" Title="دوره" />
|
||||
<PropertyColumn Property="x => x.StartDate" Title="از تاریخ">
|
||||
<CellTemplate>
|
||||
@context.Item.StartDate.ToString("yyyy/MM/dd")
|
||||
@context.Item.StartDate.MiladiToJalali()
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<PropertyColumn Property="x => x.EndDate" Title="تا تاریخ">
|
||||
<CellTemplate>
|
||||
@context.Item.EndDate.ToString("yyyy/MM/dd")
|
||||
@context.Item.EndDate.MiladiToJalali()
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<PropertyColumn Property="x => x.TotalRequests" Title="تعداد درخواستها" />
|
||||
@@ -203,11 +203,14 @@
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
</MudDataGrid>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
}
|
||||
</MudContainer>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Inject] public CommissionContract.CommissionContractClient CommissionClient { get; set; }
|
||||
@@ -394,8 +397,8 @@
|
||||
{
|
||||
sb.AppendLine(string.Join(",",
|
||||
EscapeCsv(r.PeriodLabel),
|
||||
r.StartDate.ToString("yyyy-MM-dd"),
|
||||
r.EndDate.ToString("yyyy-MM-dd"),
|
||||
r.StartDate.MiladiToJalali(),
|
||||
r.EndDate.MiladiToJalali(),
|
||||
r.TotalRequests,
|
||||
r.CompletedCount,
|
||||
r.FailedCount,
|
||||
@@ -423,7 +426,7 @@
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("گزارش برداشتها");
|
||||
sb.AppendLine($"دوره: {_periodType} از تاریخ: {_startDate:yyyy/MM/dd} تا تاریخ: {_endDate:yyyy/MM/dd}");
|
||||
sb.AppendLine($"دوره: {_periodType} از تاریخ: {_startDate?.MiladiToJalali() ?? "-"} تا تاریخ: {_endDate?.MiladiToJalali() ?? "-"}");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("خلاصه:");
|
||||
@@ -441,7 +444,7 @@
|
||||
foreach (var r in _periodReports.OrderBy(r => r.StartDate))
|
||||
{
|
||||
sb.AppendLine(
|
||||
$"{r.PeriodLabel} | {r.StartDate:yyyy/MM/dd} | {r.EndDate:yyyy/MM/dd} | {r.TotalRequests} | {r.CompletedCount} | {r.FailedCount} | {r.PendingCount} | {r.TotalAmount:N0} | {r.PaidAmount:N0} | {r.PendingAmount:N0}");
|
||||
$"{r.PeriodLabel} | {r.StartDate.MiladiToJalali()} | {r.EndDate.MiladiToJalali()} | {r.TotalRequests} | {r.CompletedCount} | {r.FailedCount} | {r.PendingCount} | {r.TotalAmount:N0} | {r.PaidAmount:N0} | {r.PendingAmount:N0}");
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(sb.ToString());
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.Commission
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<div>
|
||||
<MudText Typo="Typo.h4" Class="mb-4">درخواستهای برداشت</MudText>
|
||||
|
||||
<MudDataGrid T="WithdrawalRequestModel"
|
||||
ServerData="@(new Func<GridState<WithdrawalRequestModel>, Task<GridData<WithdrawalRequestModel>>>(ServerReload))"
|
||||
Hover="true"
|
||||
@ref="_gridData"
|
||||
Height="75vh">
|
||||
Height="calc(100vh - 240px)">
|
||||
<ToolBarContent>
|
||||
<MudText Typo="Typo.h6">درخواستهای برداشت</MudText>
|
||||
<MudSpacer />
|
||||
@@ -30,13 +30,13 @@
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Style="max-width: 150px;">
|
||||
<MudSelectItem Value="@((int?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem Value="@((int?)0)">در انتظار واریز</MudSelectItem>
|
||||
<MudSelectItem Value="@((int?)1)">واریز شده به کیف پول</MudSelectItem>
|
||||
<MudSelectItem Value="@((int?)2)">درخواست برداشت</MudSelectItem>
|
||||
<MudSelectItem Value="@((int?)3)">برداشت شده</MudSelectItem>
|
||||
<MudSelectItem Value="@((int?)4)">خطای پرداخت بانکی</MudSelectItem>
|
||||
<MudSelectItem Value="@((int?)5)">لغو شده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)0)">در انتظار واریز</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)1)">واریز شده به کیف پول</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)2)">درخواست برداشت</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)3)">برداشت شده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)4)">خطای پرداخت بانکی</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)5)">لغو شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
@@ -44,6 +44,12 @@
|
||||
StartIcon="@Icons.Material.Filled.Search">
|
||||
جستجو
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Info"
|
||||
OnClick="ExportToExcel"
|
||||
StartIcon="@Icons.Material.Filled.FileDownload">
|
||||
خروجی Excel
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</ToolBarContent>
|
||||
<Columns>
|
||||
@@ -52,7 +58,9 @@
|
||||
<PropertyColumn Property="x => x.UserId" Title="کاربر">
|
||||
<CellTemplate>
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.body2"><strong>@context.Item.UserName</strong></MudText>
|
||||
<MudLink Href="@($"/network/user-info/{context.Item.UserId}")" Underline="Underline.Hover" Color="Color.Primary">
|
||||
<strong>@context.Item.UserName</strong>
|
||||
</MudLink>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">ID: @context.Item.UserId</MudText>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
@@ -101,7 +109,7 @@
|
||||
|
||||
<PropertyColumn Property="x => x.RequestedAt" Title="تاریخ درخواست">
|
||||
<CellTemplate>
|
||||
@context.Item.RequestedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm")
|
||||
@context.Item.RequestedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
|
||||
@@ -142,11 +150,14 @@
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="WithdrawalRequestModel"
|
||||
PageSizeOptions="@(new int[] { 10, 25, 50, 100 })"
|
||||
PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد سطرهای صفحه" />
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudContainer>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,10 @@ using CMSMicroservice.Protobuf.Protos.Commission;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using BackOffice.Pages.Commission.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using System.Text;
|
||||
using DateTimeConverterCL;
|
||||
using GrpcWithdrawalRequestModel = CMSMicroservice.Protobuf.Protos.Commission.WithdrawalRequestModel;
|
||||
using GrpcGetWithdrawalRequestsRequest = CMSMicroservice.Protobuf.Protos.Commission.GetWithdrawalRequestsRequest;
|
||||
|
||||
@@ -10,6 +14,7 @@ namespace BackOffice.Pages.Commission;
|
||||
public partial class WithdrawalRequests
|
||||
{
|
||||
[Inject] public CommissionContract.CommissionContractClient CommissionContract { get; set; }
|
||||
[Inject] public IJSRuntime JsRuntime { get; set; }
|
||||
|
||||
private MudDataGrid<WithdrawalRequestModel> _gridData;
|
||||
private int? _filterStatus;
|
||||
@@ -142,10 +147,16 @@ public partial class WithdrawalRequests
|
||||
};
|
||||
}
|
||||
|
||||
private void ViewDetails(WithdrawalRequestModel request)
|
||||
private async Task ViewDetails(WithdrawalRequestModel request)
|
||||
{
|
||||
Snackbar.Add($"جزئیات درخواست {request.Id}", Severity.Info);
|
||||
// TODO: Open details dialog
|
||||
var parameters = new DialogParameters<WithdrawalDetailsDialog>
|
||||
{
|
||||
{ x => x.Request, request },
|
||||
{ x => x.StatusText, GetStatusText(request.Status) },
|
||||
{ x => x.MethodText, GetMethodText(request.Method) }
|
||||
};
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
await DialogService.ShowAsync<WithdrawalDetailsDialog>("جزئیات درخواست برداشت", parameters, options);
|
||||
}
|
||||
|
||||
private async Task ApproveRequest(WithdrawalRequestModel request)
|
||||
@@ -177,19 +188,23 @@ public partial class WithdrawalRequests
|
||||
|
||||
private async Task RejectRequest(WithdrawalRequestModel request)
|
||||
{
|
||||
var confirmed = await DialogService.ShowMessageBox(
|
||||
"رد درخواست",
|
||||
$"آیا از رد درخواست برداشت {request.Amount:N0} ریال برای {request.UserName} مطمئن هستید؟",
|
||||
yesText: "رد", cancelText: "لغو");
|
||||
var parameters = new DialogParameters<RejectReasonDialog>
|
||||
{
|
||||
{ x => x.UserName, request.UserName },
|
||||
{ x => x.Amount, request.Amount }
|
||||
};
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<RejectReasonDialog>("رد درخواست برداشت", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (confirmed == true)
|
||||
if (result is { Canceled: false, Data: string reason })
|
||||
{
|
||||
try
|
||||
{
|
||||
var rejectRequest = new RejectWithdrawalRequest
|
||||
{
|
||||
PayoutId = request.Id,
|
||||
Reason = "رد توسط ادمین پنل مدیریت"
|
||||
Reason = reason
|
||||
};
|
||||
|
||||
await CommissionContract.RejectWithdrawalAsync(rejectRequest);
|
||||
@@ -230,4 +245,66 @@ public partial class WithdrawalRequests
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExportToExcel()
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new GrpcGetWithdrawalRequestsRequest
|
||||
{
|
||||
PageIndex = 1,
|
||||
PageSize = 1000
|
||||
};
|
||||
|
||||
if (_filterStatus.HasValue) request.Status = _filterStatus.Value;
|
||||
if (_filterUserId.HasValue && _filterUserId.Value > 0) request.UserId = _filterUserId.Value;
|
||||
if (!string.IsNullOrWhiteSpace(_filterIban)) request.IbanNumber = _filterIban;
|
||||
|
||||
var result = await CommissionContract.GetWithdrawalRequestsAsync(request);
|
||||
|
||||
if (result?.Models == null || !result.Models.Any())
|
||||
{
|
||||
Snackbar.Add("دادهای برای خروجی وجود ندارد.", Severity.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("شناسه,کاربر,نام کاربر,مبلغ,وضعیت,روش,شماره شبا,تاریخ درخواست,تاریخ پردازش,شماره مرجع بانکی");
|
||||
|
||||
foreach (var r in result.Models)
|
||||
{
|
||||
var mapped = MapToViewModel(r);
|
||||
sb.AppendLine(string.Join(",",
|
||||
mapped.Id,
|
||||
mapped.UserId,
|
||||
EscapeCsv(mapped.UserName),
|
||||
mapped.Amount,
|
||||
EscapeCsv(GetStatusText(mapped.Status)),
|
||||
EscapeCsv(GetMethodText(mapped.Method)),
|
||||
EscapeCsv(mapped.IbanNumber ?? ""),
|
||||
mapped.RequestedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime(),
|
||||
mapped.ProcessedDate?.MiladiToJalaliWithTime() ?? "-",
|
||||
EscapeCsv(mapped.BankReferenceId ?? "")));
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
var filename = $"withdrawal-requests-{DateTime.Now:yyyyMMddHHmmss}.csv";
|
||||
|
||||
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
|
||||
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private static string EscapeCsv(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return "";
|
||||
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
|
||||
return $"\"{value.Replace("\"", "\"\"")}\"";
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
@using BackOffice.Services.Content
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using BackOffice.Common.BaseComponents
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudForm @ref="_form" @bind-IsValid="@_isValid">
|
||||
<MudStack Spacing="2">
|
||||
<MudTextField @bind-Value="Model.PageKey"
|
||||
Label="کلید صفحه (مثال: about, contact)"
|
||||
Required="true"
|
||||
Variant="Variant.Outlined"
|
||||
HelperText="این مقدار باید یکتا باشد و فقط حروف انگلیسی و خط فاصله مجاز است" />
|
||||
|
||||
<MudTextField @bind-Value="Model.Title"
|
||||
Label="عنوان صفحه"
|
||||
Required="true"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudTextField @bind-Value="Model.MetaDescription"
|
||||
Label="توضیحات متا (SEO)"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="2" />
|
||||
|
||||
<MudTextField @bind-Value="Model.HeroTitle"
|
||||
Label="عنوان هیرو"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudTextField @bind-Value="Model.HeroSubtitle"
|
||||
Label="زیرعنوان هیرو"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudText Typo="Typo.subtitle2">تصویر هیرو</MudText>
|
||||
<MudStack Justify="Justify.Center" AlignItems="AlignItems.Center">
|
||||
@if (!string.IsNullOrWhiteSpace(_imagePreview))
|
||||
{
|
||||
<Image Src="@_imagePreview" Width="280" Height="160" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper Class="d-flex align-center justify-center" Style="width:280px;height:160px;">
|
||||
<MudText Typo="Typo.caption">تصویری انتخاب نشده است</MudText>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
<MudFileUpload T="IBrowserFile" Accept="image/*" FilesChanged="OnImageSelected">
|
||||
<ActivatorContent>
|
||||
<MudButton HtmlTag="label"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
ButtonType="ButtonType.Button"
|
||||
StartIcon="@Icons.Material.Filled.Image">
|
||||
انتخاب تصویر هیرو
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
<SelectedTemplate>
|
||||
@if (context != null)
|
||||
{
|
||||
<MudText Class="mt-1" Typo="Typo.caption">@context.Name</MudText>
|
||||
}
|
||||
</SelectedTemplate>
|
||||
</MudFileUpload>
|
||||
</MudStack>
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudSwitch T="bool" @bind-Value="Model.IsActive" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.body2">فعال</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="SubmitAsync" Disabled="@(!_isValid || _loading)">
|
||||
ایجاد صفحه
|
||||
</MudButton>
|
||||
<MudButton Color="Color.Default" Variant="Variant.Text" OnClick="Cancel">
|
||||
انصراف
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,74 @@
|
||||
using BackOffice.Services.Content;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Content.Components;
|
||||
|
||||
public partial class SitePageCreateDialog
|
||||
{
|
||||
[CascadingParameter] IMudDialogInstance DialogInstance { get; set; } = default!;
|
||||
[Inject] public ISitePageService SitePageService { get; set; } = default!;
|
||||
|
||||
[Parameter] public SitePageCreateDto Model { get; set; } = new();
|
||||
|
||||
private MudForm? _form;
|
||||
private bool _isValid = true;
|
||||
private bool _loading;
|
||||
|
||||
private IBrowserFile? _imageFile;
|
||||
private byte[]? _imageBuffer;
|
||||
private string? _imagePreview;
|
||||
private readonly long _maxAllowedSize = (1024 * 1024) * 5;
|
||||
|
||||
private async Task OnImageSelected(IBrowserFile? file)
|
||||
{
|
||||
_imageFile = file;
|
||||
if (file != null)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await file.OpenReadStream(_maxAllowedSize).CopyToAsync(ms);
|
||||
_imageBuffer = ms.ToArray();
|
||||
_imagePreview = $"data:{file.ContentType};base64," + Convert.ToBase64String(_imageBuffer);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SubmitAsync()
|
||||
{
|
||||
if (_form != null)
|
||||
{
|
||||
await _form.Validate();
|
||||
if (!_isValid) return;
|
||||
}
|
||||
|
||||
_loading = true;
|
||||
|
||||
// Handle image upload
|
||||
if (_imageFile != null && _imageBuffer != null)
|
||||
{
|
||||
Model.ImageFile = _imageBuffer;
|
||||
Model.ImageMime = _imageFile.ContentType;
|
||||
Model.ImageFileName = Path.GetFileNameWithoutExtension(_imageFile.Name);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await SitePageService.CreateAsync(Model);
|
||||
DialogInstance.Close(DialogResult.Ok(true));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
DialogInstance.Cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
@using BackOffice.Services.Content
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using BackOffice.Common.BaseComponents
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudForm @ref="_form" @bind-IsValid="@_isValid">
|
||||
<MudStack Spacing="2">
|
||||
<MudTextField @bind-Value="Model.Title"
|
||||
Label="عنوان صفحه"
|
||||
Required="true"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudTextField @bind-Value="Model.MetaDescription"
|
||||
Label="توضیحات متا (SEO)"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="2" />
|
||||
|
||||
<MudTextField @bind-Value="Model.HeroTitle"
|
||||
Label="عنوان هیرو"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudTextField @bind-Value="Model.HeroSubtitle"
|
||||
Label="زیرعنوان هیرو"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudText Typo="Typo.subtitle2">تصویر هیرو</MudText>
|
||||
<MudStack Justify="Justify.Center" AlignItems="AlignItems.Center">
|
||||
@if (!string.IsNullOrWhiteSpace(_imagePreview))
|
||||
{
|
||||
<Image Src="@_imagePreview" Width="280" Height="160" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Model.HeroImagePath))
|
||||
{
|
||||
<Image Src="@Model.HeroImagePath" Width="280" Height="160" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper Class="d-flex align-center justify-center" Style="width:280px;height:160px;">
|
||||
<MudText Typo="Typo.caption">تصویری انتخاب نشده است</MudText>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
<MudFileUpload T="IBrowserFile" Accept="image/*" FilesChanged="OnImageSelected">
|
||||
<ActivatorContent>
|
||||
<MudButton HtmlTag="label"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
ButtonType="ButtonType.Button"
|
||||
StartIcon="@Icons.Material.Filled.Image">
|
||||
انتخاب تصویر هیرو
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
<SelectedTemplate>
|
||||
@if (context != null)
|
||||
{
|
||||
<MudText Class="mt-1" Typo="Typo.caption">@context.Name</MudText>
|
||||
}
|
||||
</SelectedTemplate>
|
||||
</MudFileUpload>
|
||||
</MudStack>
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudSwitch T="bool" @bind-Value="Model.IsActive" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.body2">فعال</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="SubmitAsync" Disabled="@(!_isValid || _loading)">
|
||||
ذخیره تغییرات
|
||||
</MudButton>
|
||||
<MudButton Color="Color.Default" Variant="Variant.Text" OnClick="Cancel">
|
||||
انصراف
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,80 @@
|
||||
using BackOffice.Services.Content;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Content.Components;
|
||||
|
||||
public partial class SitePageEditDialog
|
||||
{
|
||||
[CascadingParameter] IMudDialogInstance DialogInstance { get; set; } = default!;
|
||||
[Inject] public ISitePageService SitePageService { get; set; } = default!;
|
||||
|
||||
[Parameter] public SitePageEditDto Model { get; set; } = new();
|
||||
[Parameter] public long PageId { get; set; }
|
||||
|
||||
private MudForm? _form;
|
||||
private bool _isValid = true;
|
||||
private bool _loading;
|
||||
|
||||
private IBrowserFile? _imageFile;
|
||||
private byte[]? _imageBuffer;
|
||||
private string? _imagePreview;
|
||||
private readonly long _maxAllowedSize = (1024 * 1024) * 5;
|
||||
|
||||
private async Task OnImageSelected(IBrowserFile? file)
|
||||
{
|
||||
_imageFile = file;
|
||||
if (file != null)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await file.OpenReadStream(_maxAllowedSize).CopyToAsync(ms);
|
||||
_imageBuffer = ms.ToArray();
|
||||
_imagePreview = $"data:{file.ContentType};base64," + Convert.ToBase64String(_imageBuffer);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleValidSubmit()
|
||||
{
|
||||
await SubmitAsync();
|
||||
}
|
||||
|
||||
private async Task SubmitAsync()
|
||||
{
|
||||
if (_form != null)
|
||||
{
|
||||
await _form.Validate();
|
||||
if (!_isValid) return;
|
||||
}
|
||||
|
||||
_loading = true;
|
||||
|
||||
// Handle image upload
|
||||
if (_imageFile != null && _imageBuffer != null)
|
||||
{
|
||||
Model.ImageFile = _imageBuffer;
|
||||
Model.ImageMime = _imageFile.ContentType;
|
||||
Model.ImageFileName = Path.GetFileNameWithoutExtension(_imageFile.Name);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await SitePageService.UpdateAsync(PageId, Model);
|
||||
DialogInstance.Close(DialogResult.Ok(true));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
DialogInstance.Cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
@using BackOffice.Services.Content
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Tizzani.MudBlazor.HtmlEditor
|
||||
@using BackOffice.Common.BaseComponents
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudForm @ref="_form" @bind-IsValid="@_isValid">
|
||||
<MudStack Spacing="2">
|
||||
<MudTextField @bind-Value="Model.SectionKey"
|
||||
Label="کلید بخش (اختیاری)"
|
||||
Variant="Variant.Outlined"
|
||||
HelperText="در صورت خالی بودن، به صورت خودکار تولید میشود" />
|
||||
|
||||
<MudTextField @bind-Value="Model.Title"
|
||||
Label="عنوان"
|
||||
Required="true"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudTextField @bind-Value="Model.Subtitle"
|
||||
Label="زیرعنوان"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudText Typo="Typo.subtitle2">محتوای HTML</MudText>
|
||||
<MudHtmlEditor @bind-Html="Model.HtmlContent" MinHeight="300px">
|
||||
<MudHtmlToolbarOptions InsertImage="false" />
|
||||
</MudHtmlEditor>
|
||||
|
||||
<MudTextField @bind-Value="Model.IconName"
|
||||
Label="نام آیکون"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudText Typo="Typo.subtitle2">تصویر بخش</MudText>
|
||||
<MudStack Justify="Justify.Center" AlignItems="AlignItems.Center">
|
||||
@if (!string.IsNullOrWhiteSpace(_imagePreview))
|
||||
{
|
||||
<Image Src="@_imagePreview" Width="220" Height="140" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Model.ImagePath))
|
||||
{
|
||||
<Image Src="@Model.ImagePath" Width="220" Height="140" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper Class="d-flex align-center justify-center" Style="width:220px;height:140px;">
|
||||
<MudText Typo="Typo.caption">تصویری انتخاب نشده است</MudText>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
<MudFileUpload T="IBrowserFile" Accept="image/*" FilesChanged="OnImageSelected">
|
||||
<ActivatorContent>
|
||||
<MudButton HtmlTag="label"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
ButtonType="ButtonType.Button"
|
||||
StartIcon="@Icons.Material.Filled.Image">
|
||||
انتخاب تصویر بخش
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
<SelectedTemplate>
|
||||
@if (context != null)
|
||||
{
|
||||
<MudText Class="mt-1" Typo="Typo.caption">@context.Name</MudText>
|
||||
}
|
||||
</SelectedTemplate>
|
||||
</MudFileUpload>
|
||||
</MudStack>
|
||||
|
||||
<MudNumericField T="int"
|
||||
@bind-Value="Model.SortOrder"
|
||||
Label="ترتیب نمایش"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudSwitch T="bool" @bind-Value="Model.IsActive" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.body2">فعال</MudText>
|
||||
</MudStack>
|
||||
|
||||
<MudTextField @bind-Value="Model.ExtraData"
|
||||
Label="دادههای اضافی (JSON)"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="3"
|
||||
HelperText="اطلاعات اضافی به صورت JSON" />
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="SubmitAsync" Disabled="@(!_isValid || _loading)">
|
||||
@(_isEditMode ? "ذخیره تغییرات" : "ایجاد بخش")
|
||||
</MudButton>
|
||||
<MudButton Color="Color.Default" Variant="Variant.Text" OnClick="Cancel">
|
||||
انصراف
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,90 @@
|
||||
using BackOffice.Services.Content;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Content.Components;
|
||||
|
||||
public partial class SitePageSectionEditDialog
|
||||
{
|
||||
[CascadingParameter] IMudDialogInstance DialogInstance { get; set; } = default!;
|
||||
[Inject] public ISitePageService SitePageService { get; set; } = default!;
|
||||
|
||||
[Parameter] public SitePageSectionEditDto Model { get; set; } = new();
|
||||
[Parameter] public long? SectionId { get; set; }
|
||||
[Parameter] public bool IsEditMode { get; set; }
|
||||
|
||||
private bool _isEditMode => IsEditMode && SectionId.HasValue;
|
||||
private MudForm? _form;
|
||||
private bool _isValid = true;
|
||||
private bool _loading;
|
||||
|
||||
private IBrowserFile? _imageFile;
|
||||
private byte[]? _imageBuffer;
|
||||
private string? _imagePreview;
|
||||
private readonly long _maxAllowedSize = (1024 * 1024) * 5;
|
||||
|
||||
private async Task OnImageSelected(IBrowserFile? file)
|
||||
{
|
||||
_imageFile = file;
|
||||
if (file != null)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await file.OpenReadStream(_maxAllowedSize).CopyToAsync(ms);
|
||||
_imageBuffer = ms.ToArray();
|
||||
_imagePreview = $"data:{file.ContentType};base64," + Convert.ToBase64String(_imageBuffer);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleValidSubmit()
|
||||
{
|
||||
await SubmitAsync();
|
||||
}
|
||||
|
||||
private async Task SubmitAsync()
|
||||
{
|
||||
if (_form != null)
|
||||
{
|
||||
await _form.Validate();
|
||||
if (!_isValid) return;
|
||||
}
|
||||
|
||||
_loading = true;
|
||||
|
||||
// Handle image upload
|
||||
if (_imageFile != null && _imageBuffer != null)
|
||||
{
|
||||
Model.ImageFile = _imageBuffer;
|
||||
Model.ImageMime = _imageFile.ContentType;
|
||||
Model.ImageFileName = Path.GetFileNameWithoutExtension(_imageFile.Name);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_isEditMode && SectionId.HasValue)
|
||||
{
|
||||
await SitePageService.UpdateSectionAsync(SectionId.Value, Model);
|
||||
}
|
||||
else
|
||||
{
|
||||
await SitePageService.CreateSectionAsync(Model);
|
||||
}
|
||||
|
||||
DialogInstance.Close(DialogResult.Ok(true));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
DialogInstance.Cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
@using BackOffice.Services.Content
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.subtitle1">بخشهای صفحه</MudText>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="AddSection">
|
||||
افزودن بخش
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
<MudDataGrid T="SitePageSectionItemDto"
|
||||
Items="Sections"
|
||||
Hover="true"
|
||||
Dense="true"
|
||||
Elevation="0">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.SectionKey" Title="کلید بخش" />
|
||||
<PropertyColumn Property="x => x.Title" Title="عنوان" />
|
||||
<PropertyColumn Property="x => x.SortOrder" Title="ترتیب" />
|
||||
<PropertyColumn Property="x => x.IsActive" Title="وضعیت">
|
||||
<CellTemplate>
|
||||
<MudChip Color="@(context.Item.IsActive ? Color.Success : Color.Error)"
|
||||
Size="Size.Small">
|
||||
@(context.Item.IsActive ? "فعال" : "غیرفعال")
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<TemplateColumn Title="عملیات" Sortable="false">
|
||||
<CellTemplate>
|
||||
<MudStack Row="true" Spacing="1">
|
||||
<MudTooltip Text="ویرایش">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Size="Size.Small"
|
||||
Color="Color.Primary"
|
||||
OnClick="@(() => EditSection(context.Item))" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="حذف">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Size="Size.Small"
|
||||
Color="Color.Error"
|
||||
OnClick="@(() => DeleteSection(context.Item))" />
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
</MudDataGrid>
|
||||
</MudStack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Color="Color.Default" Variant="Variant.Text" OnClick="Close">
|
||||
بستن
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,114 @@
|
||||
using BackOffice.Services.Content;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Content.Components;
|
||||
|
||||
public partial class SitePageSectionsDialog
|
||||
{
|
||||
[CascadingParameter] IMudDialogInstance DialogInstance { get; set; } = default!;
|
||||
[Inject] public ISitePageService SitePageService { get; set; } = default!;
|
||||
|
||||
[Parameter] public long PageId { get; set; }
|
||||
[Parameter] public string PageTitle { get; set; } = string.Empty;
|
||||
[Parameter] public List<SitePageSectionItemDto> Sections { get; set; } = new();
|
||||
|
||||
private bool _changed;
|
||||
|
||||
private async Task AddSection()
|
||||
{
|
||||
var dto = new SitePageSectionEditDto
|
||||
{
|
||||
SitePageId = PageId,
|
||||
SortOrder = Sections.Count + 1
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters<SitePageSectionEditDialog>
|
||||
{
|
||||
{ nameof(SitePageSectionEditDialog.Model), dto },
|
||||
{ nameof(SitePageSectionEditDialog.IsEditMode), false }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<SitePageSectionEditDialog>("افزودن بخش جدید", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true });
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
Snackbar.Add("بخش با موفقیت ایجاد شد", Severity.Success);
|
||||
_changed = true;
|
||||
await RefreshSections();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EditSection(SitePageSectionItemDto item)
|
||||
{
|
||||
var dto = new SitePageSectionEditDto
|
||||
{
|
||||
SitePageId = PageId,
|
||||
SectionKey = item.SectionKey,
|
||||
Title = item.Title,
|
||||
Subtitle = item.Subtitle,
|
||||
HtmlContent = item.HtmlContent,
|
||||
IconName = item.IconName,
|
||||
ImagePath = item.ImagePath,
|
||||
ImageThumbnailPath = item.ImageThumbnailPath,
|
||||
SortOrder = item.SortOrder,
|
||||
IsActive = item.IsActive,
|
||||
ExtraData = item.ExtraData
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters<SitePageSectionEditDialog>
|
||||
{
|
||||
{ nameof(SitePageSectionEditDialog.Model), dto },
|
||||
{ nameof(SitePageSectionEditDialog.SectionId), item.Id },
|
||||
{ nameof(SitePageSectionEditDialog.IsEditMode), true }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<SitePageSectionEditDialog>("ویرایش بخش", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true });
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
Snackbar.Add("بخش با موفقیت بهروزرسانی شد", Severity.Success);
|
||||
_changed = true;
|
||||
await RefreshSections();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteSection(SitePageSectionItemDto item)
|
||||
{
|
||||
var confirmed = await DialogService.ShowMessageBox(
|
||||
"حذف بخش",
|
||||
$"آیا از حذف بخش «{item.Title}» مطمئن هستید؟",
|
||||
yesText: "حذف",
|
||||
cancelText: "لغو");
|
||||
|
||||
if (confirmed == true)
|
||||
{
|
||||
await SitePageService.DeleteSectionAsync(item.Id);
|
||||
Snackbar.Add("بخش حذف شد", Severity.Success);
|
||||
_changed = true;
|
||||
await RefreshSections();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshSections()
|
||||
{
|
||||
var details = await SitePageService.GetByIdAsync(PageId);
|
||||
if (details != null)
|
||||
{
|
||||
Sections = details.Sections;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void Close()
|
||||
{
|
||||
if (_changed)
|
||||
DialogInstance.Close(DialogResult.Ok(true));
|
||||
else
|
||||
DialogInstance.Cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
@page "/content/pages"
|
||||
@attribute [Authorize(Roles = "Administrator")]
|
||||
|
||||
@using BackOffice.Services.Content
|
||||
|
||||
@inject ISitePageService SitePageService
|
||||
|
||||
<div>
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.h5">مدیریت صفحات سایت</MudText>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="CreatePage">
|
||||
افزودن صفحه جدید
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
<MudDataGrid T="SitePageSummaryDto"
|
||||
Items="_pages"
|
||||
Hover="true"
|
||||
Elevation="1"
|
||||
Dense="true"
|
||||
Loading="_loading">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
<PropertyColumn Property="x => x.PageKey" Title="کلید صفحه" />
|
||||
<PropertyColumn Property="x => x.Title" Title="عنوان" />
|
||||
<PropertyColumn Property="x => x.IsActive" Title="وضعیت">
|
||||
<CellTemplate>
|
||||
<MudChip Color="@(context.Item.IsActive ? Color.Success : Color.Error)"
|
||||
Size="Size.Small">
|
||||
@(context.Item.IsActive ? "فعال" : "غیرفعال")
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<PropertyColumn Property="x => x.SectionCount" Title="تعداد بخش" />
|
||||
<TemplateColumn Title="آخرین ویرایش">
|
||||
<CellTemplate>
|
||||
@context.Item.LastModified.MiladiToJalaliWithTime()
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="عملیات" Sortable="false">
|
||||
<CellTemplate>
|
||||
<MudStack Row="true" Spacing="1">
|
||||
<MudTooltip Text="ویرایش">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Size="Size.Small"
|
||||
Color="Color.Primary"
|
||||
OnClick="@(() => EditPage(context.Item))" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="مدیریت بخشها">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ViewList"
|
||||
Size="Size.Small"
|
||||
Color="Color.Info"
|
||||
OnClick="@(() => ManageSections(context.Item))" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="حذف">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Size="Size.Small"
|
||||
Color="Color.Error"
|
||||
OnClick="@(() => DeletePage(context.Item))" />
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
</MudDataGrid>
|
||||
</MudStack>
|
||||
</div>
|
||||
@@ -0,0 +1,128 @@
|
||||
using BackOffice.Services.Content;
|
||||
using BackOffice.Pages.Content.Components;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Content;
|
||||
|
||||
public partial class SitePageManagementPage
|
||||
{
|
||||
private List<SitePageSummaryDto> _pages = new();
|
||||
private bool _loading = true;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadPagesAsync();
|
||||
}
|
||||
|
||||
private async Task LoadPagesAsync()
|
||||
{
|
||||
_loading = true;
|
||||
_pages = await SitePageService.GetAllAsync();
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private async Task CreatePage()
|
||||
{
|
||||
var model = new SitePageCreateDto();
|
||||
var parameters = new DialogParameters<SitePageCreateDialog>
|
||||
{
|
||||
{ nameof(SitePageCreateDialog.Model), model }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<SitePageCreateDialog>("افزودن صفحه جدید", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true });
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
Snackbar.Add("صفحه جدید با موفقیت ایجاد شد", Severity.Success);
|
||||
await LoadPagesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EditPage(SitePageSummaryDto item)
|
||||
{
|
||||
var details = await SitePageService.GetByIdAsync(item.Id);
|
||||
if (details == null)
|
||||
{
|
||||
Snackbar.Add("خطا در دریافت اطلاعات صفحه", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var dto = new SitePageEditDto
|
||||
{
|
||||
Title = details.Title,
|
||||
MetaDescription = details.MetaDescription,
|
||||
HeroTitle = details.HeroTitle,
|
||||
HeroSubtitle = details.HeroSubtitle,
|
||||
HeroImagePath = details.HeroImagePath,
|
||||
IsActive = details.IsActive
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters<SitePageEditDialog>
|
||||
{
|
||||
{ nameof(SitePageEditDialog.Model), dto },
|
||||
{ nameof(SitePageEditDialog.PageId), item.Id }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<SitePageEditDialog>("ویرایش صفحه", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true });
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
Snackbar.Add("صفحه با موفقیت بهروزرسانی شد", Severity.Success);
|
||||
await LoadPagesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ManageSections(SitePageSummaryDto item)
|
||||
{
|
||||
var details = await SitePageService.GetByIdAsync(item.Id);
|
||||
if (details == null)
|
||||
{
|
||||
Snackbar.Add("خطا در دریافت اطلاعات صفحه", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var parameters = new DialogParameters<SitePageSectionsDialog>
|
||||
{
|
||||
{ nameof(SitePageSectionsDialog.PageId), item.Id },
|
||||
{ nameof(SitePageSectionsDialog.PageTitle), item.Title },
|
||||
{ nameof(SitePageSectionsDialog.Sections), details.Sections }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<SitePageSectionsDialog>(
|
||||
$"مدیریت بخشهای «{item.Title}»", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Large, FullWidth = true });
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
await LoadPagesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeletePage(SitePageSummaryDto item)
|
||||
{
|
||||
var confirm = await DialogService.ShowMessageBox(
|
||||
"تایید حذف",
|
||||
$"آیا از حذف صفحه «{item.Title}» اطمینان دارید؟ بخشهای وابسته نیز حذف خواهند شد.",
|
||||
yesText: "بله، حذف شود",
|
||||
cancelText: "انصراف");
|
||||
|
||||
if (confirm != true) return;
|
||||
|
||||
try
|
||||
{
|
||||
await SitePageService.DeleteAsync(item.Id);
|
||||
Snackbar.Add("صفحه با موفقیت حذف شد", Severity.Success);
|
||||
await LoadPagesAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در حذف: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.UserContract
|
||||
@inject Microsoft.JSInterop.IJSRuntime JsRuntime
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudSimpleTable Dense="true">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>شناسه قرارداد:</strong></td>
|
||||
<td>@Contract.Id</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>شناسه کاربر:</strong></td>
|
||||
<td>@Contract.UserId</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>شناسه قالب قرارداد:</strong></td>
|
||||
<td>@Contract.ContractId</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>کد امضا:</strong></td>
|
||||
<td>@Contract.SignGuid</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>فایل PDF:</strong></td>
|
||||
<td>
|
||||
@if (!string.IsNullOrWhiteSpace(Contract.SignedPdfFile))
|
||||
{
|
||||
<MudButton Variant="Variant.Text" Color="Color.Primary" Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.PictureAsPdf"
|
||||
OnClick="DownloadPdf">
|
||||
دانلود PDF
|
||||
</MudButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">بدون فایل</MudText>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Close">بستن</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
[Parameter] public GetAllUserContractByFilterResponseModel Contract { get; set; } = null!;
|
||||
|
||||
private void Close() => MudDialog.Cancel();
|
||||
|
||||
private async Task DownloadPdf()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(Contract.SignedPdfFile))
|
||||
{
|
||||
var title = $"قرارداد کاربر {Contract.UserId}";
|
||||
await JsRuntime.InvokeVoidAsync("jsHtmlToPdf", Contract.SignedPdfFile, title);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
@page "/contracts"
|
||||
@attribute [Authorize(Roles = "Administrator")]
|
||||
|
||||
@using CMSMicroservice.Protobuf.Protos.UserContract
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
@inject Microsoft.JSInterop.IJSRuntime JsRuntime
|
||||
@inject UserContractContract.UserContractContractClient UserContractClient
|
||||
|
||||
<div>
|
||||
<MudText Typo="Typo.h5" Class="mb-3">قراردادهای کاربران</MudText>
|
||||
|
||||
<MudDataGrid T="GetAllUserContractByFilterResponseModel" @ref="_grid"
|
||||
ServerData="LoadContracts"
|
||||
Hover="true" Dense="true"
|
||||
Height="calc(100vh - 240px)">
|
||||
<ToolBarContent>
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
||||
<UserAutoComplete Label="جستجوی کاربر" @bind-SelectedUserId="_filterUserId" />
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Search"
|
||||
OnClick="ApplyFilter">جستجو</MudButton>
|
||||
</MudStack>
|
||||
</ToolBarContent>
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
<PropertyColumn Property="x => x.UserId" Title="شناسه کاربر" />
|
||||
<PropertyColumn Property="x => x.ContractId" Title="شناسه قرارداد" />
|
||||
<PropertyColumn Property="x => x.SignGuid" Title="کد امضا" />
|
||||
<TemplateColumn Title="فایل PDF" Sortable="false">
|
||||
<CellTemplate>
|
||||
@if (!string.IsNullOrWhiteSpace(context.Item.SignedPdfFile))
|
||||
{
|
||||
<MudButton Variant="Variant.Text" Color="Color.Primary" Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.PictureAsPdf"
|
||||
OnClick="@(() => DownloadPdf(context.Item))">
|
||||
دانلود PDF
|
||||
</MudButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">بدون فایل</MudText>
|
||||
}
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="عملیات" Sortable="false">
|
||||
<CellTemplate>
|
||||
<MudTooltip Text="جزئیات">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Visibility"
|
||||
Size="Size.Small" Color="Color.Info"
|
||||
OnClick="@(() => ViewDetails(context.Item))" />
|
||||
</MudTooltip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">قراردادی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="GetAllUserContractByFilterResponseModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private MudDataGrid<GetAllUserContractByFilterResponseModel>? _grid;
|
||||
private long? _filterUserId;
|
||||
|
||||
private async Task<GridData<GetAllUserContractByFilterResponseModel>> LoadContracts(GridState<GetAllUserContractByFilterResponseModel> state)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new GetAllUserContractByFilterRequest
|
||||
{
|
||||
PaginationState = new() { PageNumber = state.Page + 1, PageSize = state.PageSize },
|
||||
Filter = new GetAllUserContractByFilterFilter()
|
||||
};
|
||||
|
||||
if (_filterUserId.HasValue && _filterUserId.Value > 0)
|
||||
request.Filter.UserId = _filterUserId.Value;
|
||||
|
||||
var result = await UserContractClient.GetAllUserContractByFilterAsync(request);
|
||||
return new GridData<GetAllUserContractByFilterResponseModel>
|
||||
{
|
||||
Items = result?.Models?.ToList() ?? new(),
|
||||
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
return new GridData<GetAllUserContractByFilterResponseModel>();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplyFilter()
|
||||
{
|
||||
if (_grid != null) await _grid.ReloadServerData();
|
||||
}
|
||||
|
||||
private async Task ViewDetails(GetAllUserContractByFilterResponseModel contract)
|
||||
{
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
["Contract"] = contract
|
||||
};
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
await DialogService.ShowAsync<ContractDetailsDialog>("جزئیات قرارداد", parameters, options);
|
||||
}
|
||||
|
||||
private async Task DownloadPdf(GetAllUserContractByFilterResponseModel contract)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(contract.SignedPdfFile))
|
||||
{
|
||||
var title = $"قرارداد کاربر {contract.UserId}";
|
||||
await JsRuntime.InvokeVoidAsync("jsHtmlToPdf", contract.SignedPdfFile, title);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,7 +118,7 @@
|
||||
.OrderBy(g => g.Key)
|
||||
.ToList();
|
||||
|
||||
_dailyLabels = groups.Select(g => g.Key.ToString("MM/dd")).ToArray();
|
||||
_dailyLabels = groups.Select(g => { var j = g.Key.MiladiToJalali(); return j.Length >= 7 ? j.Substring(5) : j; }).ToArray();
|
||||
|
||||
_dailySeries = new List<ChartSeries>
|
||||
{
|
||||
|
||||
@@ -1,340 +0,0 @@
|
||||
@page "/dashboard/overview"
|
||||
@attribute [Authorize]
|
||||
|
||||
@using CMSMicroservice.Protobuf.Protos.Commission
|
||||
@using CMSMicroservice.Protobuf.Protos.ClubMembership
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">داشبورد سیستم</MudText>
|
||||
<MudText Typo="Typo.body1" Color="Color.Secondary" Class="mb-4">
|
||||
مرور کلی عملکرد سیستم شبکه، باشگاه و کمیسیون
|
||||
</MudText>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Commission Stats -->
|
||||
<MudCard Class="mb-4">
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
<MudText Typo="Typo.h6">
|
||||
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Class="mr-2" />
|
||||
کمیسیون هفته جاری
|
||||
</MudText>
|
||||
</CardHeaderContent>
|
||||
<CardHeaderActions>
|
||||
<MudButton Variant="Variant.Text"
|
||||
Color="Color.Primary"
|
||||
Href="/commission/dashboard">
|
||||
جزئیات بیشتر
|
||||
</MudButton>
|
||||
</CardHeaderActions>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="3">
|
||||
<MudCard Outlined="true">
|
||||
<MudCardContent>
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">هفته جاری</MudText>
|
||||
<MudText Typo="Typo.h5">@_currentWeekDisplayName</MudText>
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="3">
|
||||
<MudCard Outlined="true">
|
||||
<MudCardContent>
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">مبلغ کل استخر</MudText>
|
||||
<MudText Typo="Typo.h5" Color="Color.Primary">
|
||||
@(_currentPool?.TotalPoolAmount.ToString("N0") ?? "0") ریال
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="3">
|
||||
<MudCard Outlined="true">
|
||||
<MudCardContent>
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">مجموع موجودیها</MudText>
|
||||
<MudText Typo="Typo.h5">@(_currentPool?.TotalBalances.ToString("N0") ?? "0")</MudText>
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="3">
|
||||
<MudCard Outlined="true">
|
||||
<MudCardContent>
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">وضعیت</MudText>
|
||||
<MudChip T="string"
|
||||
Color="@(_currentPool?.IsCalculated == true ? Color.Success : Color.Warning)"
|
||||
Size="Size.Small">
|
||||
@(_currentPool?.IsCalculated == true ? "محاسبه شده" : "در انتظار")
|
||||
</MudChip>
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
|
||||
<!-- Club Membership Stats -->
|
||||
<MudCard Class="mb-4">
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
<MudText Typo="Typo.h6">
|
||||
<MudIcon Icon="@Icons.Material.Filled.CardMembership" Class="mr-2" />
|
||||
عضویت باشگاه
|
||||
</MudText>
|
||||
</CardHeaderContent>
|
||||
<CardHeaderActions>
|
||||
<MudButton Variant="Variant.Text"
|
||||
Color="Color.Primary"
|
||||
Href="/club/members">
|
||||
مشاهده اعضا
|
||||
</MudButton>
|
||||
</CardHeaderActions>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudCard Outlined="true">
|
||||
<MudCardContent>
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">کل اعضا</MudText>
|
||||
<MudText Typo="Typo.h5" Color="Color.Primary">@_totalClubMembers</MudText>
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudCard Outlined="true">
|
||||
<MudCardContent>
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">اعضای فعال</MudText>
|
||||
<MudText Typo="Typo.h5" Color="Color.Success">@_activeClubMembers</MudText>
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudCard Outlined="true">
|
||||
<MudCardContent>
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">اعضای غیرفعال</MudText>
|
||||
<MudText Typo="Typo.h5" Color="Color.Warning">@_inactiveClubMembers</MudText>
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
|
||||
<!-- Discount Shop Stats - TEMPORARILY DISABLED (needs DiscountOrder proto) -->
|
||||
@* <BackOffice.Pages.Dashboard.DiscountShopWidget /> *@
|
||||
|
||||
<MudCard Class="mb-4">
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
<MudText Typo="Typo.h6">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Discount" Class="mr-2" />
|
||||
آمار فروشگاه تخفیفی
|
||||
</MudText>
|
||||
</CardHeaderContent>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true">
|
||||
این بخش در حال توسعه است و بهزودی فعال میشود.
|
||||
</MudAlert>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<MudCard>
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
<MudText Typo="Typo.h6">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Speed" Class="mr-2" />
|
||||
دسترسی سریع
|
||||
</MudText>
|
||||
</CardHeaderContent>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
FullWidth="true"
|
||||
Href="/commission/dashboard"
|
||||
StartIcon="@Icons.Material.Filled.AccountBalanceWallet">
|
||||
داشبورد کمیسیون
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Secondary"
|
||||
FullWidth="true"
|
||||
Href="/commission/payouts"
|
||||
StartIcon="@Icons.Material.Filled.Payments">
|
||||
پرداختهای کاربران
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Tertiary"
|
||||
FullWidth="true"
|
||||
Href="/commission/withdrawals"
|
||||
StartIcon="@Icons.Material.Filled.RequestQuote">
|
||||
درخواستهای برداشت
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Info"
|
||||
FullWidth="true"
|
||||
Href="/network/tree"
|
||||
StartIcon="@Icons.Material.Filled.AccountTree">
|
||||
درخت شبکه
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Success"
|
||||
FullWidth="true"
|
||||
Href="/network/balances"
|
||||
StartIcon="@Icons.Material.Filled.AccountBalance">
|
||||
گزارش موجودیها
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Warning"
|
||||
FullWidth="true"
|
||||
Href="/club/members"
|
||||
StartIcon="@Icons.Material.Filled.Groups">
|
||||
مدیریت باشگاه
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
}
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
[Inject] public CommissionContract.CommissionContractClient CommissionClient { get; set; } = null!;
|
||||
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubClient { get; set; } = null!;
|
||||
|
||||
private bool _loading = false;
|
||||
private long _currentWeekDefinitionId = 0;
|
||||
private string _currentWeekDisplayName = string.Empty;
|
||||
private GetWeeklyCommissionPoolResponse? _currentPool;
|
||||
|
||||
private long _totalClubMembers = 0;
|
||||
private long _activeClubMembers = 0;
|
||||
private long _inactiveClubMembers = 0;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadDashboardData();
|
||||
}
|
||||
|
||||
private async Task LoadDashboardData()
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
// Get current week from available weeks API
|
||||
try
|
||||
{
|
||||
var availableWeeksRequest = new GetAvailableWeeksRequest
|
||||
{
|
||||
FutureWeeksCount = 0,
|
||||
PastWeeksCount = 0
|
||||
};
|
||||
var availableWeeksResponse = await CommissionClient.GetAvailableWeeksAsync(availableWeeksRequest);
|
||||
if (availableWeeksResponse?.CurrentWeek != null)
|
||||
{
|
||||
_currentWeekDefinitionId = availableWeeksResponse.CurrentWeek.WeekDefinitionId;
|
||||
_currentWeekDisplayName = availableWeeksResponse.CurrentWeek.DisplayName;
|
||||
}
|
||||
}
|
||||
catch { /* Ignore - will skip pool loading */ }
|
||||
|
||||
// Load Commission data
|
||||
if (_currentWeekDefinitionId > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
var commissionRequest = new GetWeeklyCommissionPoolRequest
|
||||
{
|
||||
WeekDefinitionId = _currentWeekDefinitionId
|
||||
};
|
||||
var commissionResponse = await CommissionClient.GetWeeklyCommissionPoolAsync(commissionRequest);
|
||||
_currentPool = commissionResponse;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری دادههای کمیسیون: {ex.Message}", Severity.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
// Load Club membership data
|
||||
try
|
||||
{
|
||||
// Get all members
|
||||
var allMembersRequest = new GetAllClubMembershipsRequest
|
||||
{
|
||||
PageIndex = 1,
|
||||
PageSize = 1
|
||||
};
|
||||
var allMembersResponse = await ClubClient.GetAllClubMembershipsAsync(allMembersRequest);
|
||||
_totalClubMembers = allMembersResponse.MetaData?.TotalCount ?? 0;
|
||||
|
||||
// Get active members
|
||||
var activeMembersRequest = new GetAllClubMembershipsRequest
|
||||
{
|
||||
PageIndex = 1,
|
||||
PageSize = 1,
|
||||
IsActive = true
|
||||
};
|
||||
var activeMembersResponse = await ClubClient.GetAllClubMembershipsAsync(activeMembersRequest);
|
||||
_activeClubMembers = activeMembersResponse.MetaData?.TotalCount ?? 0;
|
||||
|
||||
_inactiveClubMembers = _totalClubMembers - _activeClubMembers;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری دادههای باشگاه: {ex.Message}", Severity.Warning);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری داشبورد: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetCurrentWeekNumber()
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
var jan1 = new DateTime(now.Year, 1, 1);
|
||||
var daysOffset = DayOfWeek.Monday - jan1.DayOfWeek;
|
||||
var firstMonday = jan1.AddDays(daysOffset);
|
||||
var cal = System.Globalization.CultureInfo.CurrentCulture.Calendar;
|
||||
var weekNum = cal.GetWeekOfYear(now, System.Globalization.CalendarWeekRule.FirstDay, DayOfWeek.Saturday);
|
||||
return $"{now.Year}-W{weekNum:D2}";
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@
|
||||
HelperText="برای دسته اصلی خالی بگذارید">
|
||||
@foreach (var category in _availableParents)
|
||||
{
|
||||
<MudSelectItem Value="@((long?)category.Id)">@GetCategoryPath(category)</MudSelectItem>
|
||||
<MudSelectItem T="long?" Value="@((long?)category.Id)">@GetCategoryPath(category)</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
|
||||
@@ -21,13 +21,13 @@
|
||||
Label="وضعیت جدید *"
|
||||
Required="true"
|
||||
Variant="Variant.Outlined">
|
||||
<MudSelectItem Value="@OrderStatus.Pending">در انتظار پرداخت</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Paid">پرداخت شده</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Processing">در حال آمادهسازی</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Pending">در انتظار پرداخت</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Paid">پرداخت شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Processing">در حال آمادهسازی</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
|
||||
|
||||
@@ -26,6 +26,24 @@
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<!-- Timeline وضعیت -->
|
||||
<MudItem xs="12">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.subtitle2" GutterBottom="true">Timeline وضعیت</MudText>
|
||||
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center" Justify="Justify.Center">
|
||||
@foreach (var step in _timelineSteps)
|
||||
{
|
||||
<MudStack Spacing="0" AlignItems="AlignItems.Center">
|
||||
<MudAvatar Color="@GetStepColor(step.State)" Size="Size.Small">
|
||||
@step.Icon
|
||||
</MudAvatar>
|
||||
<MudText Typo="Typo.caption">@step.Title</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<!-- وضعیت سفارش -->
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
@@ -36,7 +54,7 @@
|
||||
@if (Order.Created.HasValue)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mt-2">
|
||||
تاریخ ثبت: @Order.Created.Value.ToString("yyyy/MM/dd HH:mm")
|
||||
تاریخ ثبت: @Order.Created.Value.MiladiToJalaliWithTime()
|
||||
</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
@@ -101,7 +119,14 @@
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>
|
||||
<MudText>@context.ProductTitle</MudText>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
@if (!string.IsNullOrWhiteSpace(context.ThumbnailPath))
|
||||
{
|
||||
<MudImage Src="@context.ThumbnailPath" Width="40" Height="40"
|
||||
ObjectFit="ObjectFit.Cover" Class="rounded" />
|
||||
}
|
||||
<MudText>@context.ProductTitle</MudText>
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
<MudTd>@context.Count</MudTd>
|
||||
<MudTd>@context.UnitPrice.ToString("N0") ریال</MudTd>
|
||||
@@ -147,6 +172,60 @@
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<!-- ویرایش وضعیت ارسال -->
|
||||
<MudItem xs="12">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.subtitle2" GutterBottom="true">مدیریت وضعیت ارسال</MudText>
|
||||
<MudDivider Class="mb-3" />
|
||||
<MudGrid>
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudSelect T="OrderStatus"
|
||||
Label="وضعیت ارسال"
|
||||
Dense="true"
|
||||
Variant="Variant.Outlined"
|
||||
@bind-Value="_newStatus">
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Pending">در انتظار پرداخت</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Paid">پرداخت شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Processing">در حال آمادهسازی</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudTextField T="string"
|
||||
Label="کد رهگیری"
|
||||
Variant="Variant.Outlined"
|
||||
Dense="true"
|
||||
@bind-Value="_trackingCode" />
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudTextField T="string"
|
||||
Label="یادداشت ادمین"
|
||||
Variant="Variant.Outlined"
|
||||
Dense="true"
|
||||
Lines="3"
|
||||
HelperText="دلیل تغییر وضعیت یا توضیحات اضافی"
|
||||
@bind-Value="_adminNotes" />
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
@if (_newStatus == OrderStatus.Cancelled || _newStatus == OrderStatus.Returned)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Class="mt-2">
|
||||
توجه: در صورت لغو یا مرجوعی سفارش، موجودی محصولات به انبار بازگردانده میشود.
|
||||
</MudAlert>
|
||||
}
|
||||
@if (_newStatus == OrderStatus.Shipped)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Class="mt-2">
|
||||
پس از تغییر وضعیت به "ارسال شده"، اطلاعرسانی به کاربر ارسال خواهد شد.
|
||||
</MudAlert>
|
||||
}
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<!-- یادداشتها -->
|
||||
@if (!string.IsNullOrEmpty(Order.Notes) || !string.IsNullOrEmpty(Order.AdminNotes))
|
||||
{
|
||||
@@ -174,46 +253,15 @@
|
||||
</MudGrid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Close" Variant="Variant.Filled" Color="Color.Primary">بستن</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Default" OnClick="Close">
|
||||
بستن
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveAsync" Disabled="_isSaving">
|
||||
@if (_isSaving)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="ml-2" />
|
||||
}
|
||||
ثبت تغییرات
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
[Parameter] public DiscountOrderDetailsDto Order { get; set; } = null!;
|
||||
|
||||
private void Close()
|
||||
{
|
||||
MudDialog.Close();
|
||||
}
|
||||
|
||||
private Color GetStatusColor(OrderStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
OrderStatus.Pending => Color.Warning,
|
||||
OrderStatus.Paid => Color.Info,
|
||||
OrderStatus.Processing => Color.Primary,
|
||||
OrderStatus.Shipped => Color.Secondary,
|
||||
OrderStatus.Delivered => Color.Success,
|
||||
OrderStatus.Cancelled => Color.Error,
|
||||
OrderStatus.Returned => Color.Dark,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private string GetStatusText(OrderStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
OrderStatus.Pending => "در انتظار پرداخت",
|
||||
OrderStatus.Paid => "پرداخت شده",
|
||||
OrderStatus.Processing => "در حال آمادهسازی",
|
||||
OrderStatus.Shipped => "ارسال شده",
|
||||
OrderStatus.Delivered => "تحویل داده شده",
|
||||
OrderStatus.Cancelled => "لغو شده",
|
||||
OrderStatus.Returned => "مرجوع شده",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
using BackOffice.Services.DiscountOrder;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.DiscountShop.Components;
|
||||
|
||||
public partial class OrderDetailsDialog
|
||||
{
|
||||
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
[Inject] public IDiscountOrderService OrderService { get; set; } = default!;
|
||||
|
||||
[Parameter] public DiscountOrderDetailsDto Order { get; set; } = null!;
|
||||
|
||||
private OrderStatus _newStatus;
|
||||
private string? _trackingCode;
|
||||
private string? _adminNotes;
|
||||
private bool _isSaving;
|
||||
private List<TimelineStep> _timelineSteps = new();
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_newStatus = Order.Status;
|
||||
_trackingCode = Order.TrackingCode;
|
||||
_adminNotes = Order.AdminNotes;
|
||||
BuildTimeline();
|
||||
}
|
||||
|
||||
private void BuildTimeline()
|
||||
{
|
||||
var status = Order.Status;
|
||||
var steps = new List<TimelineStep>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Title = "ثبت سفارش",
|
||||
State = status == OrderStatus.Pending ? StepState.Active : StepState.Completed,
|
||||
Icon = "1"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Title = "پرداخت",
|
||||
State = status switch
|
||||
{
|
||||
OrderStatus.Pending => StepState.Inactive,
|
||||
OrderStatus.Paid => StepState.Active,
|
||||
OrderStatus.Cancelled => StepState.Inactive,
|
||||
_ => Order.PaymentCompleted ? StepState.Completed : StepState.Inactive
|
||||
},
|
||||
Icon = "2"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Title = "آمادهسازی",
|
||||
State = status switch
|
||||
{
|
||||
OrderStatus.Processing => StepState.Active,
|
||||
OrderStatus.Shipped or OrderStatus.Delivered => StepState.Completed,
|
||||
_ => StepState.Inactive
|
||||
},
|
||||
Icon = "3"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Title = "ارسال",
|
||||
State = status switch
|
||||
{
|
||||
OrderStatus.Shipped => StepState.Active,
|
||||
OrderStatus.Delivered => StepState.Completed,
|
||||
_ => StepState.Inactive
|
||||
},
|
||||
Icon = "4"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Title = "تحویل / مرجوعی",
|
||||
State = status switch
|
||||
{
|
||||
OrderStatus.Delivered => StepState.Completed,
|
||||
OrderStatus.Returned => StepState.Completed,
|
||||
OrderStatus.Cancelled => StepState.Completed,
|
||||
_ => StepState.Inactive
|
||||
},
|
||||
Icon = "5"
|
||||
}
|
||||
};
|
||||
|
||||
_timelineSteps = steps;
|
||||
}
|
||||
|
||||
private Color GetStepColor(StepState state)
|
||||
{
|
||||
return state switch
|
||||
{
|
||||
StepState.Completed => Color.Success,
|
||||
StepState.Active => Color.Info,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
_isSaving = true;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
await OrderService.UpdateStatusAsync(Order.Id, new UpdateOrderStatusDto
|
||||
{
|
||||
Status = _newStatus,
|
||||
TrackingCode = _trackingCode,
|
||||
AdminNotes = _adminNotes
|
||||
});
|
||||
|
||||
Snackbar.Add("تغییرات سفارش با موفقیت ذخیره شد.", Severity.Success);
|
||||
MudDialog.Close(DialogResult.Ok(true));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ذخیره تغییرات: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSaving = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void Close() => MudDialog.Close();
|
||||
|
||||
private Color GetStatusColor(OrderStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
OrderStatus.Pending => Color.Warning,
|
||||
OrderStatus.Paid => Color.Info,
|
||||
OrderStatus.Processing => Color.Primary,
|
||||
OrderStatus.Shipped => Color.Secondary,
|
||||
OrderStatus.Delivered => Color.Success,
|
||||
OrderStatus.Cancelled => Color.Error,
|
||||
OrderStatus.Returned => Color.Dark,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private string GetStatusText(OrderStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
OrderStatus.Pending => "در انتظار پرداخت",
|
||||
OrderStatus.Paid => "پرداخت شده",
|
||||
OrderStatus.Processing => "در حال آمادهسازی",
|
||||
OrderStatus.Shipped => "ارسال شده",
|
||||
OrderStatus.Delivered => "تحویل داده شده",
|
||||
OrderStatus.Cancelled => "لغو شده",
|
||||
OrderStatus.Returned => "مرجوع شده",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
}
|
||||
|
||||
private enum StepState
|
||||
{
|
||||
Inactive = 0,
|
||||
Active = 1,
|
||||
Completed = 2
|
||||
}
|
||||
|
||||
private class TimelineStep
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public StepState State { get; set; }
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@
|
||||
Color="Color.Primary"
|
||||
ButtonType="ButtonType.Button"
|
||||
StartIcon="@Icons.Material.Filled.Image"
|
||||
Style="cursor:pointer;">
|
||||
>
|
||||
انتخاب تصویر اصلی
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
@@ -106,7 +106,7 @@
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
|
||||
<MudHtmlEditor @bind-Html="Model.FullInformation">
|
||||
<MudHtmlEditor @bind-Html="Model.FullInformation" MinHeight="300px">
|
||||
<MudHtmlToolbarOptions InsertImage="false" />
|
||||
</MudHtmlEditor>
|
||||
|
||||
@@ -125,6 +125,14 @@
|
||||
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.subtitle2">تصویر بندانگشتی</MudText>
|
||||
@if (!string.IsNullOrWhiteSpace(_thumbnailPreview))
|
||||
{
|
||||
<Image Src="@(_thumbnailPreview)" Width="120" Height="80" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Model.ThumbnailPath))
|
||||
{
|
||||
<Image Src="@Model.ThumbnailPath" Width="120" Height="80" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
<MudFileUpload T="IBrowserFile" Accept="image/*" FilesChanged="OnThumbnailSelected">
|
||||
<ActivatorContent>
|
||||
<MudButton HtmlTag="label"
|
||||
@@ -132,7 +140,7 @@
|
||||
Color="Color.Secondary"
|
||||
ButtonType="ButtonType.Button"
|
||||
StartIcon="@Icons.Material.Filled.Image"
|
||||
Style="cursor:pointer;">
|
||||
>
|
||||
انتخاب تصویر بندانگشتی (اختیاری)
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
@@ -165,6 +173,9 @@
|
||||
private IBrowserFile? _thumbnailImageFile;
|
||||
private readonly long _maxAllowedSize = (1024 * 1024) * 5;
|
||||
private string? _mainImagePreview;
|
||||
private string? _thumbnailPreview;
|
||||
private byte[]? _mainImageBuffer;
|
||||
private byte[]? _thumbnailImageBuffer;
|
||||
private List<long> _selectedCategoryIds = new();
|
||||
|
||||
protected override Task OnInitializedAsync()
|
||||
@@ -181,9 +192,10 @@
|
||||
_mainImageFile = file;
|
||||
if (file != null)
|
||||
{
|
||||
var buffer = new byte[file.Size];
|
||||
await file.OpenReadStream(_maxAllowedSize).ReadAsync(buffer);
|
||||
_mainImagePreview = $"data:{file.ContentType};base64," + Convert.ToBase64String(buffer);
|
||||
using var ms = new MemoryStream();
|
||||
await file.OpenReadStream(_maxAllowedSize).CopyToAsync(ms);
|
||||
_mainImageBuffer = ms.ToArray();
|
||||
_mainImagePreview = $"data:{file.ContentType};base64," + Convert.ToBase64String(_mainImageBuffer);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
@@ -191,6 +203,14 @@
|
||||
private async Task OnThumbnailSelected(IBrowserFile? file)
|
||||
{
|
||||
_thumbnailImageFile = file;
|
||||
if (file != null)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await file.OpenReadStream(_maxAllowedSize).CopyToAsync(ms);
|
||||
_thumbnailImageBuffer = ms.ToArray();
|
||||
_thumbnailPreview = $"data:{file.ContentType};base64," + Convert.ToBase64String(_thumbnailImageBuffer);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Submit()
|
||||
@@ -207,21 +227,17 @@
|
||||
Model.CategoryIds = _selectedCategoryIds.ToList();
|
||||
|
||||
// Handle main image upload
|
||||
if (_mainImageFile != null)
|
||||
if (_mainImageFile != null && _mainImageBuffer != null)
|
||||
{
|
||||
var buffer = new byte[_mainImageFile.Size];
|
||||
await _mainImageFile.OpenReadStream(_maxAllowedSize).ReadAsync(buffer);
|
||||
Model.ImageFile = buffer;
|
||||
Model.ImageFile = _mainImageBuffer;
|
||||
Model.ImageMime = _mainImageFile.ContentType;
|
||||
Model.ImageFileName = Path.GetFileNameWithoutExtension(_mainImageFile.Name);
|
||||
}
|
||||
|
||||
// Handle thumbnail upload
|
||||
if (_thumbnailImageFile != null)
|
||||
if (_thumbnailImageFile != null && _thumbnailImageBuffer != null)
|
||||
{
|
||||
var buffer = new byte[_thumbnailImageFile.Size];
|
||||
await _thumbnailImageFile.OpenReadStream(_maxAllowedSize).ReadAsync(buffer);
|
||||
Model.ThumbnailFile = buffer;
|
||||
Model.ThumbnailFile = _thumbnailImageBuffer;
|
||||
Model.ThumbnailMime = _thumbnailImageFile.ContentType;
|
||||
Model.ThumbnailFileName = Path.GetFileNameWithoutExtension(_thumbnailImageFile.Name);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Collections"
|
||||
Style="cursor:pointer;">
|
||||
>
|
||||
انتخاب تصاویر (چندتایی)
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
@@ -89,8 +89,9 @@
|
||||
{
|
||||
if (file == null) continue;
|
||||
|
||||
var buffer = new byte[file.Size];
|
||||
await file.OpenReadStream(_maxAllowedSize).ReadAsync(buffer);
|
||||
using var ms = new MemoryStream();
|
||||
await file.OpenReadStream(_maxAllowedSize).CopyToAsync(ms);
|
||||
var buffer = ms.ToArray();
|
||||
var preview = $"data:{file.ContentType};base64," + Convert.ToBase64String(buffer);
|
||||
|
||||
_items.Add(new ProductImageItem
|
||||
|
||||
@@ -1,343 +1,157 @@
|
||||
@page "/discount-categories"
|
||||
@using BackOffice.Services.DiscountCategory
|
||||
@using BackOffice.Pages.DiscountShop.Components
|
||||
@using BackOffice.Common.BaseComponents
|
||||
@using static BackOffice.Pages.DiscountShop.Components.CategoryFormDialog
|
||||
@inject IDiscountCategoryService DiscountCategoryService
|
||||
@inject IDialogService DialogService
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<PageTitle>مدیریت دستهبندی محصولات تخفیفی</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Category" Class="ml-2" />
|
||||
مدیریت دستهبندیهای فروشگاه تخفیفی
|
||||
</MudText>
|
||||
|
||||
<MudPaper Class="pa-4 mt-4">
|
||||
<BasePageComponent @ref="_basePage" OnClearFilterClick="OnFilterCleared" OnSubmitClick="OnFilterSubmit">
|
||||
<Filters>
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudTextField T="string" Clearable="true" Label="جستجو در دستهبندیها"
|
||||
@bind-Value="@_searchQuery" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
</MudItem>
|
||||
</Filters>
|
||||
<Content>
|
||||
<MudGrid>
|
||||
<MudItem xs="12" sm="8">
|
||||
<MudTextField @bind-Value="_searchQuery"
|
||||
Label="جستجو در دستهبندیها"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||
OnKeyUp="@(() => FilterCategories())" />
|
||||
@* ────── Tree sidebar (same as regular store) ────── *@
|
||||
<MudItem xs="12" md="3">
|
||||
<MudPaper Class="pa-2" Style="height:calc(100vh - 240px); overflow:auto;">
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2">درخت دستهبندیها</MudText>
|
||||
<MudList T="string" Dense="true">
|
||||
<MudListItem T="string"
|
||||
OnClick="@(() => OnTreeNodeSelected(null))"
|
||||
Selected="@(_selectedParentId == null)">
|
||||
<MudText Typo="Typo.body2">همه دستهبندیها</MudText>
|
||||
</MudListItem>
|
||||
@foreach (var node in _treeNodes)
|
||||
{
|
||||
<MudListItem T="string"
|
||||
OnClick="@(() => OnTreeNodeSelected(node.Id))"
|
||||
Selected="@(_selectedParentId == node.Id)">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
<div style="width:@(node.Depth * 16)px;"></div>
|
||||
<MudText Typo="Typo.body2">@node.Title</MudText>
|
||||
</MudStack>
|
||||
</MudListItem>
|
||||
}
|
||||
</MudList>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
FullWidth="true"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="@(() => OpenCreateDialog())">
|
||||
دستهبندی جدید
|
||||
</MudButton>
|
||||
|
||||
@* ────── Grid (9 cols) ────── *@
|
||||
<MudItem xs="12" md="9">
|
||||
<MudDataGrid T="DiscountCategoryDto"
|
||||
Items="@_filteredCategories"
|
||||
Hover="true"
|
||||
Dense="true"
|
||||
Height="calc(100vh - 240px)">
|
||||
<ColGroup>
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
<col style="width: 80px;" />
|
||||
</ColGroup>
|
||||
<ToolBarContent>
|
||||
<MudText>مدیریت دستهبندیها</MudText>
|
||||
<MudSpacer />
|
||||
<MudStack Spacing="2" Row="true" Justify="Justify.Center" AlignItems="AlignItems.Center">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Large"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => OpenCreateDialog())">
|
||||
افزودن
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</ToolBarContent>
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
<PropertyColumn Property="x => x.Name" Title="نام لاتین" />
|
||||
<PropertyColumn Property="x => x.Title" Title="عنوان" />
|
||||
|
||||
<TemplateColumn Title="دستهبندی والد">
|
||||
<CellTemplate>
|
||||
@{
|
||||
var parentTitle = "-";
|
||||
if (context.Item.ParentCategoryId.HasValue &&
|
||||
context.Item.ParentCategoryId.Value > 0)
|
||||
{
|
||||
var parent = _allCategories.FirstOrDefault(c => c.Id == context.Item.ParentCategoryId.Value);
|
||||
if (parent != null)
|
||||
{
|
||||
parentTitle = parent.Title;
|
||||
}
|
||||
}
|
||||
}
|
||||
<MudText Typo="Typo.body2">@parentTitle</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="تعداد محصولات">
|
||||
<CellTemplate>
|
||||
<MudChip T="string" Color="Color.Info" Size="Size.Small">@context.Item.ProductCount</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.SortOrder" Title="ترتیب" />
|
||||
|
||||
<TemplateColumn Title="فعال؟">
|
||||
<CellTemplate>
|
||||
<MudChip T="string"
|
||||
Color="@(context.Item.IsActive ? Color.Success : Color.Error)"
|
||||
Variant="Variant.Filled"
|
||||
Size="Size.Small">
|
||||
@(context.Item.IsActive ? "فعال" : "غیرفعال")
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn StickyLeft="true" Title="عملیات" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
|
||||
<CellTemplate>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
<MudTooltip Text="افزودن زیردسته">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Add"
|
||||
Color="Color.Success"
|
||||
Size="Size.Small"
|
||||
OnClick="@(() => OpenCreateDialog(context.Item.Id))" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="ویرایش">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.EditNote"
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => OpenEditDialog(context.Item.Id))" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="حذف">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.DeleteOutline"
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
Disabled="@(context.Item.Children.Any())"
|
||||
OnClick="@(() => DeleteCategory(context.Item.Id))" />
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="مدیریت محصولات این دسته (درگ و دراپ)">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ShoppingCart"
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => OpenCategoryProducts(context.Item))" />
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="DiscountCategoryDto"
|
||||
PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mt-4" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudDataGrid T="DiscountCategoryDto"
|
||||
Items="@_filteredCategories"
|
||||
Hover="true"
|
||||
Dense="true"
|
||||
Class="mt-4">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
|
||||
<TemplateColumn Title="عنوان">
|
||||
<CellTemplate>
|
||||
@{
|
||||
var indent = GetIndentLevel(context.Item);
|
||||
}
|
||||
<div style="padding-right: @(indent * 20)px;">
|
||||
@if (context.Item.Children.Any())
|
||||
{
|
||||
<MudIcon Icon="@Icons.Material.Filled.Folder" Size="Size.Small" Class="ml-1" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudIcon Icon="@Icons.Material.Filled.FolderOpen" Size="Size.Small" Class="ml-1" />
|
||||
}
|
||||
<strong>@context.Item.Title</strong>
|
||||
@if (!string.IsNullOrEmpty(context.Item.Name))
|
||||
{
|
||||
<MudText Typo="Typo.caption" Class="mr-2">(@context.Item.Name)</MudText>
|
||||
}
|
||||
</div>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="تعداد محصولات">
|
||||
<CellTemplate>
|
||||
<MudChip T="string" Color="Color.Info" Size="Size.Small">@context.Item.ProductCount</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.SortOrder" Title="ترتیب" />
|
||||
|
||||
<TemplateColumn Title="وضعیت">
|
||||
<CellTemplate>
|
||||
<MudChip T="string" Color="@(context.Item.IsActive ? Color.Success : Color.Default)"
|
||||
Size="Size.Small">
|
||||
@(context.Item.IsActive ? "فعال" : "غیرفعال")
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="عملیات" Sortable="false">
|
||||
<CellTemplate>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Add"
|
||||
Color="Color.Success"
|
||||
Size="Size.Small"
|
||||
Title="افزودن زیردسته"
|
||||
OnClick="@(() => OpenCreateDialog(context.Item.Id))" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
Title="ویرایش"
|
||||
OnClick="@(() => OpenEditDialog(context.Item.Id))" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Color="Color.Error"
|
||||
Size="Size.Small"
|
||||
Title="حذف"
|
||||
Disabled="@(context.Item.Children.Any())"
|
||||
OnClick="@(() => DeleteCategory(context.Item.Id))" />
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="DiscountCategoryDto" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
|
||||
@if (!_filteredCategories.Any())
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Class="mt-4">
|
||||
@if (string.IsNullOrEmpty(_searchQuery))
|
||||
{
|
||||
<text>هنوز دستهبندیای ایجاد نشده است.</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>دستهبندی با عبارت "@_searchQuery" یافت نشد.</text>
|
||||
}
|
||||
</MudAlert>
|
||||
}
|
||||
}
|
||||
</MudPaper>
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
private List<DiscountCategoryDto> _allCategories = new();
|
||||
private List<DiscountCategoryDto> _filteredCategories = new();
|
||||
private bool _loading = false;
|
||||
private string? _searchQuery;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadCategories();
|
||||
}
|
||||
|
||||
private async Task LoadCategories()
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var rootCategories = await DiscountCategoryService.GetCategoriesAsync();
|
||||
_allCategories = FlattenCategoriesWithDepth(rootCategories, 0);
|
||||
FilterCategories();
|
||||
Snackbar.Add("دستهبندیها بارگذاری شدند", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری دستهبندیها: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private List<DiscountCategoryDto> FlattenCategoriesWithDepth(List<DiscountCategoryDto> categories, int depth)
|
||||
{
|
||||
var result = new List<DiscountCategoryDto>();
|
||||
foreach (var category in categories.OrderBy(c => c.SortOrder))
|
||||
{
|
||||
category.IsExpanded = false; // Use IsExpanded to store depth temporarily
|
||||
result.Add(category);
|
||||
if (category.Children.Any())
|
||||
{
|
||||
result.AddRange(FlattenCategoriesWithDepth(category.Children, depth + 1));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private int GetIndentLevel(DiscountCategoryDto category)
|
||||
{
|
||||
// Calculate depth by traversing parents
|
||||
int depth = 0;
|
||||
var current = category;
|
||||
while (current.ParentCategoryId.HasValue)
|
||||
{
|
||||
var parent = _allCategories.FirstOrDefault(c => c.Id == current.ParentCategoryId.Value);
|
||||
if (parent == null) break;
|
||||
current = parent;
|
||||
depth++;
|
||||
}
|
||||
return depth;
|
||||
}
|
||||
|
||||
private void FilterCategories()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_searchQuery))
|
||||
{
|
||||
_filteredCategories = _allCategories;
|
||||
}
|
||||
else
|
||||
{
|
||||
var query = _searchQuery.ToLower();
|
||||
_filteredCategories = _allCategories
|
||||
.Where(c => c.Title.ToLower().Contains(query) ||
|
||||
c.Name.ToLower().Contains(query) ||
|
||||
(c.Description?.ToLower().Contains(query) ?? false))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenCreateDialog(long? parentId = null)
|
||||
{
|
||||
var model = new CategoryFormModel { ParentCategoryId = parentId };
|
||||
var parameters = new DialogParameters<CategoryFormDialog>
|
||||
{
|
||||
{ x => x.Model, model },
|
||||
{ x => x.IsEditMode, false },
|
||||
{ x => x.ExcludeCategoryId, (long?)null }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<CategoryFormDialog>(
|
||||
parentId.HasValue ? "ایجاد زیردسته" : "ایجاد دستهبندی جدید",
|
||||
parameters,
|
||||
options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false, Data: CategoryFormModel formData })
|
||||
{
|
||||
try
|
||||
{
|
||||
var dto = new CreateDiscountCategoryDto
|
||||
{
|
||||
ParentCategoryId = formData.ParentCategoryId,
|
||||
Name = formData.Name,
|
||||
Title = formData.Title,
|
||||
Description = formData.Description,
|
||||
ImagePath = formData.ImagePath,
|
||||
SortOrder = formData.SortOrder,
|
||||
IsActive = formData.IsActive
|
||||
};
|
||||
|
||||
await DiscountCategoryService.CreateAsync(dto);
|
||||
Snackbar.Add("دستهبندی با موفقیت ایجاد شد", Severity.Success);
|
||||
await LoadCategories();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ایجاد دستهبندی: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenEditDialog(long categoryId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var category = await DiscountCategoryService.GetByIdAsync(categoryId);
|
||||
if (category == null)
|
||||
{
|
||||
Snackbar.Add("دستهبندی یافت نشد", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var model = new CategoryFormModel
|
||||
{
|
||||
ParentCategoryId = category.ParentCategoryId,
|
||||
Name = category.Name,
|
||||
Title = category.Title,
|
||||
Description = category.Description,
|
||||
ImagePath = category.ImagePath,
|
||||
SortOrder = category.SortOrder,
|
||||
IsActive = category.IsActive
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters<CategoryFormDialog>
|
||||
{
|
||||
{ x => x.Model, model },
|
||||
{ x => x.IsEditMode, true },
|
||||
{ x => x.ExcludeCategoryId, categoryId }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<CategoryFormDialog>("ویرایش دستهبندی", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false, Data: CategoryFormModel formData })
|
||||
{
|
||||
var dto = new UpdateDiscountCategoryDto
|
||||
{
|
||||
ParentCategoryId = formData.ParentCategoryId,
|
||||
Name = formData.Name,
|
||||
Title = formData.Title,
|
||||
Description = formData.Description,
|
||||
ImagePath = formData.ImagePath,
|
||||
SortOrder = formData.SortOrder,
|
||||
IsActive = formData.IsActive
|
||||
};
|
||||
|
||||
await DiscountCategoryService.UpdateAsync(categoryId, dto);
|
||||
Snackbar.Add("دستهبندی با موفقیت ویرایش شد", Severity.Success);
|
||||
await LoadCategories();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ویرایش دستهبندی: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteCategory(long categoryId)
|
||||
{
|
||||
var category = _allCategories.FirstOrDefault(c => c.Id == categoryId);
|
||||
if (category?.Children.Any() == true)
|
||||
{
|
||||
Snackbar.Add("ابتدا زیردستههای این دستهبندی را حذف کنید", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await DialogService.ShowMessageBox(
|
||||
"تأیید حذف",
|
||||
"آیا از حذف این دستهبندی اطمینان دارید؟",
|
||||
yesText: "بله", cancelText: "خیر");
|
||||
|
||||
if (result == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DiscountCategoryService.DeleteAsync(categoryId);
|
||||
Snackbar.Add("دستهبندی با موفقیت حذف شد", Severity.Success);
|
||||
await LoadCategories();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در حذف دستهبندی: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</Content>
|
||||
</BasePageComponent>
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
using BackOffice.Common.BaseComponents;
|
||||
using BackOffice.Common.Utilities;
|
||||
using BackOffice.Services.DiscountCategory;
|
||||
using BackOffice.Pages.DiscountShop.Components;
|
||||
using static BackOffice.Pages.DiscountShop.Components.CategoryFormDialog;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.DiscountShop;
|
||||
|
||||
public partial class DiscountCategoriesMainPage
|
||||
{
|
||||
[Inject] public IDiscountCategoryService DiscountCategoryService { get; set; } = default!;
|
||||
|
||||
private List<DiscountCategoryDto> _allCategories = new();
|
||||
private List<DiscountCategoryDto> _filteredCategories = new();
|
||||
private List<TreeNodeModel> _treeNodes = new();
|
||||
private BasePageComponent _basePage = default!;
|
||||
private bool _loading;
|
||||
private string? _searchQuery;
|
||||
private long? _selectedParentId;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadCategories();
|
||||
}
|
||||
|
||||
private async Task LoadCategories()
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var rootCategories = await DiscountCategoryService.GetCategoriesAsync();
|
||||
_allCategories = FlattenCategoriesWithDepth(rootCategories, 0);
|
||||
BuildTreeNodes(rootCategories);
|
||||
FilterCategories();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری دستهبندیها: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildTreeNodes(List<DiscountCategoryDto> categories, int depth = 0)
|
||||
{
|
||||
_treeNodes.Clear();
|
||||
BuildTreeNodesRecursive(categories, depth);
|
||||
}
|
||||
|
||||
private void BuildTreeNodesRecursive(List<DiscountCategoryDto> categories, int depth)
|
||||
{
|
||||
foreach (var cat in categories.OrderBy(c => c.SortOrder))
|
||||
{
|
||||
_treeNodes.Add(new TreeNodeModel
|
||||
{
|
||||
Id = cat.Id,
|
||||
Title = cat.Title,
|
||||
Depth = depth,
|
||||
HasChildren = cat.Children.Any()
|
||||
});
|
||||
if (cat.Children.Any())
|
||||
{
|
||||
BuildTreeNodesRecursive(cat.Children, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<DiscountCategoryDto> FlattenCategoriesWithDepth(List<DiscountCategoryDto> categories, int depth)
|
||||
{
|
||||
var result = new List<DiscountCategoryDto>();
|
||||
foreach (var category in categories.OrderBy(c => c.SortOrder))
|
||||
{
|
||||
result.Add(category);
|
||||
if (category.Children.Any())
|
||||
{
|
||||
result.AddRange(FlattenCategoriesWithDepth(category.Children, depth + 1));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void OnTreeNodeSelected(long? parentId)
|
||||
{
|
||||
_selectedParentId = parentId;
|
||||
FilterCategories();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void FilterCategories()
|
||||
{
|
||||
var items = _allCategories.AsEnumerable();
|
||||
|
||||
if (_selectedParentId.HasValue)
|
||||
{
|
||||
items = items.Where(c =>
|
||||
c.Id == _selectedParentId.Value ||
|
||||
c.ParentCategoryId == _selectedParentId.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_searchQuery))
|
||||
{
|
||||
var query = _searchQuery.ToLower();
|
||||
items = items.Where(c =>
|
||||
c.Title.ToLower().Contains(query) ||
|
||||
c.Name.ToLower().Contains(query) ||
|
||||
(c.Description?.ToLower().Contains(query) ?? false));
|
||||
}
|
||||
|
||||
_filteredCategories = items.ToList();
|
||||
}
|
||||
|
||||
private async Task OnFilterSubmit()
|
||||
{
|
||||
_basePage.IsFiltered = true;
|
||||
FilterCategories();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task OnFilterCleared()
|
||||
{
|
||||
_basePage.IsFiltered = false;
|
||||
_searchQuery = null;
|
||||
_selectedParentId = null;
|
||||
FilterCategories();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task OpenCreateDialog(long? parentId = null)
|
||||
{
|
||||
var model = new CategoryFormModel { ParentCategoryId = parentId };
|
||||
var parameters = new DialogParameters<CategoryFormDialog>
|
||||
{
|
||||
{ x => x.Model, model },
|
||||
{ x => x.IsEditMode, false },
|
||||
{ x => x.ExcludeCategoryId, (long?)null }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<CategoryFormDialog>(
|
||||
parentId.HasValue ? "ایجاد زیردسته" : "ایجاد دستهبندی جدید",
|
||||
parameters,
|
||||
options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false, Data: CategoryFormModel formData })
|
||||
{
|
||||
try
|
||||
{
|
||||
var dto = new CreateDiscountCategoryDto
|
||||
{
|
||||
ParentCategoryId = formData.ParentCategoryId,
|
||||
Name = formData.Name,
|
||||
Title = formData.Title,
|
||||
Description = formData.Description,
|
||||
ImagePath = formData.ImagePath,
|
||||
SortOrder = formData.SortOrder,
|
||||
IsActive = formData.IsActive
|
||||
};
|
||||
|
||||
await DiscountCategoryService.CreateAsync(dto);
|
||||
Snackbar.Add("دستهبندی با موفقیت ایجاد شد", Severity.Success);
|
||||
await LoadCategories();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ایجاد دستهبندی: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenEditDialog(long categoryId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var category = await DiscountCategoryService.GetByIdAsync(categoryId);
|
||||
if (category == null)
|
||||
{
|
||||
Snackbar.Add("دستهبندی یافت نشد", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var model = new CategoryFormModel
|
||||
{
|
||||
ParentCategoryId = category.ParentCategoryId,
|
||||
Name = category.Name,
|
||||
Title = category.Title,
|
||||
Description = category.Description,
|
||||
ImagePath = category.ImagePath,
|
||||
SortOrder = category.SortOrder,
|
||||
IsActive = category.IsActive
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters<CategoryFormDialog>
|
||||
{
|
||||
{ x => x.Model, model },
|
||||
{ x => x.IsEditMode, true },
|
||||
{ x => x.ExcludeCategoryId, categoryId }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<CategoryFormDialog>("ویرایش دستهبندی", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false, Data: CategoryFormModel formData })
|
||||
{
|
||||
var dto = new UpdateDiscountCategoryDto
|
||||
{
|
||||
ParentCategoryId = formData.ParentCategoryId,
|
||||
Name = formData.Name,
|
||||
Title = formData.Title,
|
||||
Description = formData.Description,
|
||||
ImagePath = formData.ImagePath,
|
||||
SortOrder = formData.SortOrder,
|
||||
IsActive = formData.IsActive
|
||||
};
|
||||
|
||||
await DiscountCategoryService.UpdateAsync(categoryId, dto);
|
||||
Snackbar.Add("دستهبندی با موفقیت ویرایش شد", Severity.Success);
|
||||
await LoadCategories();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ویرایش دستهبندی: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteCategory(long categoryId)
|
||||
{
|
||||
var category = _allCategories.FirstOrDefault(c => c.Id == categoryId);
|
||||
if (category?.Children.Any() == true)
|
||||
{
|
||||
Snackbar.Add("ابتدا زیردستههای این دستهبندی را حذف کنید", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small };
|
||||
bool? result = await DialogService.ShowMessageBox(
|
||||
"اخطار",
|
||||
$"آیا از حذف دستهبندی «{category?.Title}» مطمئن هستید؟",
|
||||
yesText: "حذف", cancelText: "لغو",
|
||||
options: options);
|
||||
|
||||
if (result == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DiscountCategoryService.DeleteAsync(categoryId);
|
||||
Snackbar.Add("دستهبندی با موفقیت حذف شد", Severity.Success);
|
||||
await LoadCategories();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در حذف دستهبندی: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TreeNodeModel
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public int Depth { get; set; }
|
||||
public bool HasChildren { get; set; }
|
||||
}
|
||||
|
||||
public void OpenCategoryProducts(DiscountCategoryDto model)
|
||||
{
|
||||
Navigation.NavigateTo($"{RouteConstance.CategoryProducts}{model.Id}");
|
||||
}
|
||||
}
|
||||
@@ -1,283 +1,213 @@
|
||||
@page "/discount-orders"
|
||||
@* Merged into /discount-shop *@
|
||||
@using BackOffice.Services.DiscountOrder
|
||||
@using BackOffice.Pages.DiscountShop.Components
|
||||
@inject IDiscountOrderService DiscountOrderService
|
||||
@inject IDialogService DialogService
|
||||
@inject ISnackbar Snackbar
|
||||
@using BackOffice.Common.BaseComponents
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
|
||||
<PageTitle>مدیریت سفارشات فروشگاه تخفیفی</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-4">
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ShoppingCart" Class="ml-2" />
|
||||
مدیریت سفارشات فروشگاه تخفیفی
|
||||
</MudText>
|
||||
|
||||
<MudPaper Class="pa-4 mt-4">
|
||||
<MudGrid>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField @bind-Value="_searchQuery"
|
||||
Label="جستجو (شماره سفارش)"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||
OnKeyUp="@(() => OnSearch())" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect @bind-Value="_statusFilter"
|
||||
Label="وضعیت سفارش"
|
||||
Variant="Variant.Outlined"
|
||||
T="OrderStatus?"
|
||||
Clearable="true"
|
||||
OnClearButtonClick="@(() => OnSearch())">
|
||||
<MudSelectItem Value="@((OrderStatus?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Pending">در انتظار پرداخت</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Paid">پرداخت شده</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Processing">در حال آمادهسازی</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect @bind-Value="_paymentFilter"
|
||||
Label="وضعیت پرداخت"
|
||||
Variant="Variant.Outlined"
|
||||
T="bool?"
|
||||
Clearable="true">
|
||||
<MudSelectItem Value="@((bool?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem Value="@((bool?)true)">پرداخت شده</MudSelectItem>
|
||||
<MudSelectItem Value="@((bool?)false)">پرداخت نشده</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
FullWidth="true"
|
||||
StartIcon="@Icons.Material.Filled.Search"
|
||||
OnClick="@(() => OnSearch())">
|
||||
جستجو
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudDataGrid @ref="_dataGrid" T="DiscountOrderDto"
|
||||
Items="@_orders"
|
||||
<BackOffice.Common.BaseComponents.BasePageComponent @ref="_basePage" OnClearFilterClick="OnFilterCleared"
|
||||
OnSubmitClick="OnFilterSubmit">
|
||||
<Filters>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudNumericField HideSpinButtons="true" T="long?" Clearable="true" Label="شناسه سفارش"
|
||||
@bind-Value="@_orderIdFilter" Variant="Variant.Outlined" Margin="Margin.Dense"/>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<UserAutoComplete Label="جستجوی کاربر" @bind-SelectedUserId="@_userIdFilter" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField T="string" Clearable="true" Label="موبایل کاربر"
|
||||
@bind-Value="@_userMobileFilter" Variant="Variant.Outlined" Margin="Margin.Dense"/>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField T="string" Clearable="true" Label="کد رهگیری"
|
||||
@bind-Value="@_trackingCodeFilter" Variant="Variant.Outlined" Margin="Margin.Dense"/>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudDatePicker T="DateTime?" Label="از تاریخ" @bind-Date="_fromDateFilter"
|
||||
Variant="Variant.Outlined" Margin="Margin.Dense"/>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudDatePicker T="DateTime?" Label="تا تاریخ" @bind-Date="_toDateFilter"
|
||||
Variant="Variant.Outlined" Margin="Margin.Dense"/>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect T="int?" Clearable="true" Label="وضعیت پرداخت" Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" @bind-Value="_paymentStatusFilter">
|
||||
<MudSelectItem T="int?" Value="@((int?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(0)">در انتظار پرداخت</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(1)">پرداخت شده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(2)">ناموفق</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(3)">بازپرداخت</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect T="OrderStatus?" Clearable="true" Label="وضعیت ارسال" Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" @bind-Value="_statusFilter">
|
||||
<MudSelectItem T="OrderStatus?" Value="@((OrderStatus?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Pending">در انتظار ارسال</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Processing">در حال آمادهسازی</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
</Filters>
|
||||
<Content>
|
||||
<MudDataGrid T="DiscountOrderDto"
|
||||
Items="@_orders"
|
||||
Loading="@_loading"
|
||||
Hover="true"
|
||||
Dense="true"
|
||||
Class="mt-4">
|
||||
Hover="true" @ref="_dataGrid" Height="calc(100vh - 240px)">
|
||||
<ColGroup>
|
||||
<col/>
|
||||
<col/>
|
||||
<col/>
|
||||
<col style="width: 58px;"/>
|
||||
</ColGroup>
|
||||
<ToolBarContent>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Style="width:100%">
|
||||
<MudText>لیست سفارشات</MudText>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Info"
|
||||
OnClick="ExportToExcel"
|
||||
StartIcon="@Icons.Material.Filled.FileDownload"
|
||||
Size="Size.Small">
|
||||
خروجی Excel
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</ToolBarContent>
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.OrderNumber" Title="شماره سفارش" />
|
||||
|
||||
<PropertyColumn Property="x => x.Created" Title="تاریخ ثبت" Format="yyyy/MM/dd HH:mm" />
|
||||
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه"/>
|
||||
|
||||
<TemplateColumn Title="نام کاربر">
|
||||
<CellTemplate>
|
||||
@if (context.Item.UserId > 0)
|
||||
{
|
||||
<MudLink Href="@($"/network/user-info/{context.Item.UserId}")" Underline="Underline.Hover" Color="Color.Primary">
|
||||
@context.Item.UserFullName
|
||||
</MudLink>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText>@context.Item.UserFullName</MudText>
|
||||
}
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.UserMobile" Title="موبایل"/>
|
||||
|
||||
<TemplateColumn Title="مبلغ کل">
|
||||
<CellTemplate>
|
||||
<MudText>@context.Item.TotalPrice.ToString("N0") ریال</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="تخفیف">
|
||||
|
||||
<TemplateColumn Title="تخفیف کیفپول">
|
||||
<CellTemplate>
|
||||
<MudText Color="Color.Success">@context.Item.DiscountBalanceUsed.ToString("N0") ریال</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="قابل پرداخت">
|
||||
|
||||
<TemplateColumn Title="پرداخت درگاه">
|
||||
<CellTemplate>
|
||||
<MudText Typo="Typo.body2">
|
||||
<strong>@context.Item.GatewayAmount.ToString("N0") ریال</strong>
|
||||
</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="تعداد">
|
||||
|
||||
<TemplateColumn Title="تعداد آیتم">
|
||||
<CellTemplate>
|
||||
<MudChip T="string" Color="Color.Info" Size="Size.Small">@context.Item.ItemsCount آیتم</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="وضعیت">
|
||||
|
||||
<TemplateColumn Title="وضعیت پرداخت">
|
||||
<CellTemplate>
|
||||
<MudChip T="string" Color="@GetStatusColor(context.Item.Status)" Size="Size.Small">
|
||||
<MudChip T="string"
|
||||
Size="Size.Small"
|
||||
Color="@GetPaymentStatusColor(context.Item.PaymentStatusValue)">
|
||||
@GetPaymentStatusText(context.Item.PaymentStatusValue)
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="تاریخ پرداخت" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
|
||||
<CellTemplate>
|
||||
@(context.Item.PaymentDate?.MiladiToJalaliWithTime() ?? "-")
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="آدرس">
|
||||
<CellTemplate>
|
||||
<MudTooltip Text="@context.Item.ShippingAddress">
|
||||
<MudText Typo="Typo.body2">
|
||||
@(string.IsNullOrWhiteSpace(context.Item.ShippingAddress)
|
||||
? "-"
|
||||
: (context.Item.ShippingAddress.Length > 30
|
||||
? context.Item.ShippingAddress.Substring(0, 30) + "..."
|
||||
: context.Item.ShippingAddress))
|
||||
</MudText>
|
||||
</MudTooltip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="وضعیت ارسال">
|
||||
<CellTemplate>
|
||||
<MudChip T="string"
|
||||
Size="Size.Small"
|
||||
Color="@GetStatusColor(context.Item.Status)">
|
||||
@GetStatusText(context.Item.Status)
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="پرداخت">
|
||||
|
||||
<TemplateColumn Title="تاریخ ثبت" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
|
||||
<CellTemplate>
|
||||
@if (context.Item.PaymentCompleted)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Success" Size="Size.Small" Icon="@Icons.Material.Filled.CheckCircle">
|
||||
پرداخت شده
|
||||
</MudChip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChip T="string" Color="Color.Warning" Size="Size.Small" Icon="@Icons.Material.Filled.Schedule">
|
||||
در انتظار
|
||||
</MudChip>
|
||||
}
|
||||
@(context.Item.Created?.MiladiToJalaliWithTime() ?? "—")
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="عملیات" Sortable="false">
|
||||
|
||||
<TemplateColumn StickyLeft="true" Title="عملیات" CellStyle="text-wrap: nowrap;"
|
||||
HeaderStyle="text-wrap: nowrap;">
|
||||
<CellTemplate>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Visibility"
|
||||
Color="Color.Info"
|
||||
Size="Size.Small"
|
||||
Title="مشاهده جزئیات"
|
||||
OnClick="@(() => OpenOrderDetails(context.Item.Id))" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
Title="تغییر وضعیت"
|
||||
OnClick="@(() => OpenChangeStatusDialog(context.Item.Id))" />
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
|
||||
<MudTooltip Text="جزئیات سفارش">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Info"
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => OpenOrderDetails(context.Item.Id))"
|
||||
/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="تغییر وضعیت ارسال">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.LocalShipping"
|
||||
Size="Size.Small"
|
||||
Color="Color.Primary"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => OpenChangeStatusDialog(context.Item))"
|
||||
/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="لغو سفارش">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Cancel" Size="Size.Small"
|
||||
Color="Color.Error"
|
||||
ButtonType="ButtonType.Button" OnClick="@(() => OnDelete(context.Item))"
|
||||
/>
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="DiscountOrderDto" />
|
||||
<MudDataGridPager T="DiscountOrderDto" PageSizeOptions=@(new int[] { 20, 50, 100 })
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه"/>
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudPaper>
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
private MudDataGrid<DiscountOrderDto>? _dataGrid;
|
||||
private List<DiscountOrderDto> _orders = new();
|
||||
private bool _loading = false;
|
||||
private int _totalCount;
|
||||
private int _totalPages;
|
||||
|
||||
private string? _searchQuery;
|
||||
private OrderStatus? _statusFilter;
|
||||
private bool? _paymentFilter;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadOrders();
|
||||
}
|
||||
|
||||
private async Task LoadOrders()
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var filter = new OrderFilterDto
|
||||
{
|
||||
Status = _statusFilter,
|
||||
PaymentCompleted = _paymentFilter
|
||||
};
|
||||
|
||||
var (orders, totalCount, totalPages) = await DiscountOrderService.GetOrdersAsync(filter);
|
||||
_orders = orders;
|
||||
_totalCount = totalCount;
|
||||
_totalPages = totalPages;
|
||||
Snackbar.Add("سفارشات بارگذاری شدند", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری سفارشات: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnSearch()
|
||||
{
|
||||
await LoadOrders();
|
||||
}
|
||||
|
||||
private async Task OpenOrderDetails(long orderId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var order = await DiscountOrderService.GetByIdAsync(orderId);
|
||||
if (order == null)
|
||||
{
|
||||
Snackbar.Add("سفارش یافت نشد", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var parameters = new DialogParameters<OrderDetailsDialog> { { x => x.Order, order } };
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Large, FullWidth = true };
|
||||
await DialogService.ShowAsync<OrderDetailsDialog>("جزئیات سفارش", parameters, options);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری جزئیات: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenChangeStatusDialog(long orderId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var order = _orders.FirstOrDefault(o => o.Id == orderId);
|
||||
if (order == null)
|
||||
{
|
||||
Snackbar.Add("سفارش یافت نشد", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var parameters = new DialogParameters<ChangeOrderStatusDialog>
|
||||
{
|
||||
{ x => x.OrderId, orderId },
|
||||
{ x => x.CurrentStatus, order.Status }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<ChangeOrderStatusDialog>("تغییر وضعیت سفارش", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false, Data: UpdateOrderStatusDto dto })
|
||||
{
|
||||
await DiscountOrderService.UpdateStatusAsync(orderId, dto);
|
||||
Snackbar.Add("وضعیت سفارش با موفقیت تغییر یافت", Severity.Success);
|
||||
await LoadOrders();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در تغییر وضعیت: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private Color GetStatusColor(OrderStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
OrderStatus.Pending => Color.Warning,
|
||||
OrderStatus.Paid => Color.Info,
|
||||
OrderStatus.Processing => Color.Primary,
|
||||
OrderStatus.Shipped => Color.Secondary,
|
||||
OrderStatus.Delivered => Color.Success,
|
||||
OrderStatus.Cancelled => Color.Error,
|
||||
OrderStatus.Returned => Color.Dark,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private string GetStatusText(OrderStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
OrderStatus.Pending => "در انتظار پرداخت",
|
||||
OrderStatus.Paid => "پرداخت شده",
|
||||
OrderStatus.Processing => "در حال آمادهسازی",
|
||||
OrderStatus.Shipped => "ارسال شده",
|
||||
OrderStatus.Delivered => "تحویل داده شده",
|
||||
OrderStatus.Cancelled => "لغو شده",
|
||||
OrderStatus.Returned => "مرجوع شده",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
</Content>
|
||||
</BackOffice.Common.BaseComponents.BasePageComponent>
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
using System.Text;
|
||||
using BackOffice.Common.BaseComponents;
|
||||
using BackOffice.Services.DiscountOrder;
|
||||
using BackOffice.Pages.DiscountShop.Components;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using MudBlazor;
|
||||
using DateTimeConverterCL;
|
||||
|
||||
namespace BackOffice.Pages.DiscountShop;
|
||||
|
||||
public partial class DiscountOrdersMainPage
|
||||
{
|
||||
[Inject] public IDiscountOrderService DiscountOrderService { get; set; } = default!;
|
||||
[Inject] public IJSRuntime JsRuntime { get; set; } = default!;
|
||||
|
||||
private MudDataGrid<DiscountOrderDto>? _dataGrid;
|
||||
private BasePageComponent _basePage = default!;
|
||||
private List<DiscountOrderDto> _orders = new();
|
||||
private bool _loading;
|
||||
|
||||
// فیلترها - یکسان با فروشگاه عادی
|
||||
private long? _orderIdFilter;
|
||||
private long? _userIdFilter;
|
||||
private string? _userMobileFilter;
|
||||
private string? _trackingCodeFilter;
|
||||
private DateTime? _fromDateFilter;
|
||||
private DateTime? _toDateFilter;
|
||||
private int? _paymentStatusFilter;
|
||||
private OrderStatus? _statusFilter;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadOrders();
|
||||
}
|
||||
|
||||
private async Task LoadOrders()
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var filter = new OrderFilterDto
|
||||
{
|
||||
UserId = _userIdFilter ?? _orderIdFilter, // orderId → search by userId too
|
||||
Status = _statusFilter,
|
||||
PaymentStatus = _paymentStatusFilter,
|
||||
UserMobile = _userMobileFilter,
|
||||
TrackingCode = _trackingCodeFilter,
|
||||
FromDate = _fromDateFilter,
|
||||
ToDate = _toDateFilter
|
||||
};
|
||||
|
||||
var (orders, totalCount, totalPages) = await DiscountOrderService.GetOrdersAsync(filter);
|
||||
_orders = orders;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری سفارشات: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenOrderDetails(long orderId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var order = await DiscountOrderService.GetByIdAsync(orderId);
|
||||
if (order == null)
|
||||
{
|
||||
Snackbar.Add("سفارش یافت نشد", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var parameters = new DialogParameters<OrderDetailsDialog> { { x => x.Order, order } };
|
||||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Large, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<OrderDetailsDialog>("جزئیات سفارش", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false })
|
||||
{
|
||||
await LoadOrders();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری جزئیات: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenChangeStatusDialog(DiscountOrderDto order)
|
||||
{
|
||||
var parameters = new DialogParameters<ChangeOrderStatusDialog>
|
||||
{
|
||||
{ x => x.OrderId, order.Id },
|
||||
{ x => x.CurrentStatus, order.Status }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<ChangeOrderStatusDialog>(
|
||||
"تغییر وضعیت سفارش", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false, Data: UpdateOrderStatusDto dto })
|
||||
{
|
||||
try
|
||||
{
|
||||
await DiscountOrderService.UpdateStatusAsync(order.Id, dto);
|
||||
Snackbar.Add("وضعیت سفارش با موفقیت تغییر یافت", Severity.Success);
|
||||
await LoadOrders();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در تغییر وضعیت: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnDelete(DiscountOrderDto model)
|
||||
{
|
||||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small };
|
||||
bool? result = await DialogService.ShowMessageBox(
|
||||
"اخطار",
|
||||
$"آیا از لغو سفارش شماره «{model.Id}» مطمئن هستید؟ این عملیات وضعیت سفارش را به «لغو شده» تغییر میدهد.",
|
||||
yesText: "لغو سفارش", cancelText: "انصراف",
|
||||
options: options);
|
||||
if (result == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DiscountOrderService.UpdateStatusAsync(model.Id, new UpdateOrderStatusDto
|
||||
{
|
||||
Status = OrderStatus.Cancelled,
|
||||
AdminNotes = "لغو شده توسط ادمین"
|
||||
});
|
||||
Snackbar.Add("سفارش با موفقیت لغو شد.", Severity.Success);
|
||||
await LoadOrders();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در لغو سفارش: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public async Task OnFilterSubmit()
|
||||
{
|
||||
_basePage.IsFiltered = true;
|
||||
StateHasChanged();
|
||||
await LoadOrders();
|
||||
}
|
||||
|
||||
public async Task OnFilterCleared()
|
||||
{
|
||||
_basePage.IsFiltered = false;
|
||||
StateHasChanged();
|
||||
_orderIdFilter = null;
|
||||
_userIdFilter = null;
|
||||
_userMobileFilter = null;
|
||||
_trackingCodeFilter = null;
|
||||
_fromDateFilter = null;
|
||||
_toDateFilter = null;
|
||||
_paymentStatusFilter = null;
|
||||
_statusFilter = null;
|
||||
await LoadOrders();
|
||||
}
|
||||
|
||||
private Color GetStatusColor(OrderStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
OrderStatus.Pending => Color.Warning,
|
||||
OrderStatus.Paid => Color.Info,
|
||||
OrderStatus.Processing => Color.Primary,
|
||||
OrderStatus.Shipped => Color.Secondary,
|
||||
OrderStatus.Delivered => Color.Success,
|
||||
OrderStatus.Cancelled => Color.Error,
|
||||
OrderStatus.Returned => Color.Dark,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private string GetStatusText(OrderStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
OrderStatus.Pending => "در انتظار ارسال",
|
||||
OrderStatus.Paid => "پرداخت شده",
|
||||
OrderStatus.Processing => "در حال آمادهسازی",
|
||||
OrderStatus.Shipped => "ارسال شده",
|
||||
OrderStatus.Delivered => "تحویل داده شده",
|
||||
OrderStatus.Cancelled => "لغو شده",
|
||||
OrderStatus.Returned => "مرجوع شده",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
}
|
||||
|
||||
private Color GetPaymentStatusColor(int status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
0 => Color.Warning, // Pending
|
||||
1 => Color.Success, // Completed
|
||||
2 => Color.Error, // Failed
|
||||
3 => Color.Info, // Refunded
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private string GetPaymentStatusText(int status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
0 => "در انتظار پرداخت",
|
||||
1 => "پرداخت شده",
|
||||
2 => "ناموفق",
|
||||
3 => "بازپرداخت شده",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task ExportToExcel()
|
||||
{
|
||||
if (_orders == null || !_orders.Any())
|
||||
{
|
||||
Snackbar.Add("دادهای برای خروجی وجود ندارد.", Severity.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("شناسه,نام کاربر,موبایل,مبلغ کل,تخفیف کیفپول,پرداخت درگاه,تعداد آیتم,وضعیت پرداخت,وضعیت ارسال,تاریخ پرداخت,تاریخ ثبت,آدرس");
|
||||
|
||||
foreach (var o in _orders)
|
||||
{
|
||||
sb.AppendLine(string.Join(",",
|
||||
o.Id,
|
||||
EscapeCsv(o.UserFullName),
|
||||
EscapeCsv(o.UserMobile),
|
||||
o.TotalPrice.ToString("N0"),
|
||||
o.DiscountBalanceUsed.ToString("N0"),
|
||||
o.GatewayAmount.ToString("N0"),
|
||||
o.ItemsCount,
|
||||
EscapeCsv(GetPaymentStatusText(o.PaymentStatusValue)),
|
||||
EscapeCsv(GetStatusText(o.Status)),
|
||||
o.PaymentDate?.MiladiToJalaliWithTime() ?? "-",
|
||||
o.Created?.MiladiToJalaliWithTime() ?? "-",
|
||||
EscapeCsv(o.ShippingAddress ?? "-")));
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
var filename = $"discount-orders-{DateTime.Now:yyyyMMddHHmmss}.csv";
|
||||
|
||||
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
|
||||
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private static string EscapeCsv(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return "";
|
||||
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
|
||||
return $"\"{value.Replace("\"", "\"\"")}\"";
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -1,359 +1,208 @@
|
||||
@page "/discount-products"
|
||||
@using BackOffice.Services.DiscountProduct
|
||||
@using BackOffice.Services.DiscountCategory
|
||||
@using BackOffice.Pages.DiscountShop.Components
|
||||
@using BackOffice.Common.BaseComponents
|
||||
@using static BackOffice.Pages.DiscountShop.Components.ProductFormDialog
|
||||
@inject IDiscountProductService DiscountProductService
|
||||
@inject IDialogService DialogService
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<PageTitle>مدیریت محصولات تخفیفی</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-4">
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Discount" Class="ml-2" />
|
||||
مدیریت محصولات تخفیفی
|
||||
</MudText>
|
||||
|
||||
<MudPaper Class="pa-4 mt-4">
|
||||
<MudGrid>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField @bind-Value="_searchQuery"
|
||||
Label="جستجو"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||
OnKeyUp="@(() => OnSearch())" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect @bind-Value="_categoryFilter"
|
||||
Label="دستهبندی"
|
||||
Variant="Variant.Outlined"
|
||||
T="long?"
|
||||
Clearable="true"
|
||||
OnClearButtonClick="@(() => OnCategoryFilterChanged())">
|
||||
<MudSelectItem Value="@((long?)null)">همه</MudSelectItem>
|
||||
@* TODO: Load categories *@
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="2">
|
||||
<MudSelect @bind-Value="_statusFilter"
|
||||
Label="وضعیت"
|
||||
Variant="Variant.Outlined"
|
||||
T="bool?"
|
||||
Clearable="true">
|
||||
<MudSelectItem Value="@((bool?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem Value="@((bool?)true)">فعال</MudSelectItem>
|
||||
<MudSelectItem Value="@((bool?)false)">غیرفعال</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="2">
|
||||
<MudSelect @bind-Value="_stockFilter"
|
||||
Label="موجودی"
|
||||
Variant="Variant.Outlined"
|
||||
T="bool?"
|
||||
Clearable="true">
|
||||
<MudSelectItem Value="@((bool?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem Value="@((bool?)true)">موجود</MudSelectItem>
|
||||
<MudSelectItem Value="@((bool?)false)">ناموجود</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="12" md="2">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
FullWidth="true"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="@(() => OpenCreateDialog())">
|
||||
محصول جدید
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudDataGrid @ref="_dataGrid" T="DiscountProductDto"
|
||||
Items="@_products"
|
||||
<BasePageComponent @ref="_basePage" OnClearFilterClick="OnFilterCleared" OnSubmitClick="OnFilterSubmit">
|
||||
<Filters>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField T="string" Clearable="true" Label="جستجو"
|
||||
@bind-Value="@_searchQuery" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect @bind-Value="_categoryFilter"
|
||||
Label="دستهبندی"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
T="long?"
|
||||
Clearable="true">
|
||||
<MudSelectItem T="long?" Value="@((long?)null)">همه</MudSelectItem>
|
||||
@foreach (var cat in _categories)
|
||||
{
|
||||
<MudSelectItem T="long?" Value="@((long?)cat.Id)">@cat.Title</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect @bind-Value="_statusFilter"
|
||||
Label="وضعیت"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
T="bool?"
|
||||
Clearable="true">
|
||||
<MudSelectItem T="bool?" Value="@((bool?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem T="bool?" Value="@((bool?)true)">فعال</MudSelectItem>
|
||||
<MudSelectItem T="bool?" Value="@((bool?)false)">غیرفعال</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect @bind-Value="_stockFilter"
|
||||
Label="موجودی"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
T="bool?"
|
||||
Clearable="true">
|
||||
<MudSelectItem T="bool?" Value="@((bool?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem T="bool?" Value="@((bool?)true)">موجود</MudSelectItem>
|
||||
<MudSelectItem T="bool?" Value="@((bool?)false)">ناموجود</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
</Filters>
|
||||
<Content>
|
||||
<MudDataGrid @ref="_dataGrid" T="DiscountProductDto"
|
||||
ServerData="LoadServerData"
|
||||
Loading="@_loading"
|
||||
Hover="true"
|
||||
Dense="true"
|
||||
Class="mt-4">
|
||||
Hover="true"
|
||||
Height="calc(100vh - 240px)">
|
||||
<ColGroup>
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
<col style="width: 58px;" />
|
||||
</ColGroup>
|
||||
<ToolBarContent>
|
||||
<MudText>مدیریت محصولات</MudText>
|
||||
<MudSpacer />
|
||||
<MudStack Spacing="2" Row="true" Justify="Justify.Center" AlignItems="AlignItems.Center">
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Secondary"
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
StartIcon="@Icons.Material.Filled.Download"
|
||||
OnClick="ExportToExcel">
|
||||
خروجی Excel
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Large"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => OpenCreateDialog())">
|
||||
افزودن
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</ToolBarContent>
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
|
||||
<TemplateColumn Title="تصویر">
|
||||
|
||||
<PropertyColumn Property="x => x.Title" Title="عنوان" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
|
||||
<CellTemplate>
|
||||
@if (!string.IsNullOrEmpty(context.Item.ThumbnailPath))
|
||||
{
|
||||
<MudAvatar Image="@context.Item.ThumbnailPath" Size="Size.Medium" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudAvatar Color="Color.Secondary" Size="Size.Medium">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Image" />
|
||||
</MudAvatar>
|
||||
}
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
<MudTooltip Text="پیشنمایش تصویر">
|
||||
<div @onclick="@(() => OpenImagePreview(context.Item.ImagePath, context.Item.Title))">
|
||||
<Image Src="@context.Item.ImagePath"
|
||||
Width="25"
|
||||
Height="25"
|
||||
ObjectPosition="ObjectPosition.Center"
|
||||
ObjectFit="ObjectFit.Fill" />
|
||||
</div>
|
||||
</MudTooltip>
|
||||
@if (string.IsNullOrWhiteSpace(context.Item.Title))
|
||||
{
|
||||
<MudText Typo="Typo.inherit">-</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTooltip Text="@(context.Item.Title)" Arrow="true">
|
||||
<MudText Typo="Typo.inherit" Style="text-wrap: nowrap;">
|
||||
@(context.Item.Title.Truncate(20, true))
|
||||
</MudText>
|
||||
</MudTooltip>
|
||||
}
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.Title" Title="عنوان" />
|
||||
|
||||
<TemplateColumn Title="قیمت">
|
||||
</PropertyColumn>
|
||||
|
||||
<TemplateColumn Title="قیمت" CellStyle="text-wrap: nowrap;">
|
||||
<CellTemplate>
|
||||
<MudText>@context.Item.Price.ToString("N0") ریال</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="حداکثر تخفیف">
|
||||
|
||||
<TemplateColumn Title="حداکثر تخفیف" CellStyle="text-wrap: nowrap;">
|
||||
<CellTemplate>
|
||||
<MudChip T="string" Color="Color.Success" Size="Size.Small">@context.Item.MaxDiscountPercent%</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="موجودی">
|
||||
|
||||
<TemplateColumn Title="موجودی" CellStyle="text-wrap: nowrap;">
|
||||
<CellTemplate>
|
||||
<MudChip T="string" Color="@(context.Item.RemainingCount > 0 ? Color.Info : Color.Error)" Size="Size.Small">
|
||||
@context.Item.RemainingCount
|
||||
</MudChip>
|
||||
@if (context.Item.RemainingCount <= 0)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Error" Size="Size.Small">ناموجود</MudChip>
|
||||
}
|
||||
else if (context.Item.RemainingCount < 10)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Warning" Size="Size.Small">@context.Item.RemainingCount</MudChip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChip T="string" Color="Color.Success" Size="Size.Small">@context.Item.RemainingCount</MudChip>
|
||||
}
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="بازدید">
|
||||
<CellTemplate>
|
||||
<MudText>@context.Item.ViewCount</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
|
||||
<TemplateColumn Title="وضعیت">
|
||||
<CellTemplate>
|
||||
<MudChip T="string" Color="@(context.Item.IsActive ? Color.Success : Color.Default)"
|
||||
<MudChip T="string"
|
||||
Color="@(context.Item.IsActive ? Color.Success : Color.Error)"
|
||||
Variant="Variant.Filled"
|
||||
Size="Size.Small">
|
||||
@(context.Item.IsActive ? "فعال" : "غیرفعال")
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="عملیات" Sortable="false">
|
||||
|
||||
<TemplateColumn StickyLeft="true" Title="عملیات" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
|
||||
<CellTemplate>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
OnClick="@(() => OpenEditDialog(context.Item.Id))" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Color="Color.Error"
|
||||
Size="Size.Small"
|
||||
OnClick="@(() => DeleteProduct(context.Item.Id))" />
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
<MudTooltip Text="ویرایش">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.EditNote"
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => OpenEditDialog(context.Item.Id))" />
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="حذف">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.DeleteOutline"
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => DeleteProduct(context.Item.Id))" />
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="گالری تصاویر">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Collections"
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => OpenGallery(context.Item))" />
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="مدیریت دستهبندی (درگ و دراپ)">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Category"
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => OpenCategoryMapping(context.Item))" />
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="تگهای محصول">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Label"
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => OpenTagAssignment(context.Item))" />
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="DiscountProductDto" />
|
||||
<MudDataGridPager T="DiscountProductDto" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudPaper>
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
private MudDataGrid<DiscountProductDto>? _dataGrid;
|
||||
private List<DiscountProductDto> _products = new();
|
||||
private bool _loading = false;
|
||||
private int _totalCount;
|
||||
private int _totalPages;
|
||||
|
||||
private string? _searchQuery;
|
||||
private long? _categoryFilter;
|
||||
private bool? _statusFilter;
|
||||
private bool? _stockFilter;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadProducts();
|
||||
}
|
||||
|
||||
private async Task LoadProducts()
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var filter = new ProductFilterDto
|
||||
{
|
||||
SearchQuery = _searchQuery,
|
||||
CategoryId = _categoryFilter,
|
||||
IsActive = _statusFilter,
|
||||
InStock = _stockFilter
|
||||
};
|
||||
|
||||
var (products, totalCount, totalPages) = await DiscountProductService.GetProductsAsync(filter);
|
||||
_products = products;
|
||||
_totalCount = totalCount;
|
||||
_totalPages = totalPages;
|
||||
|
||||
Console.WriteLine($"Loaded {_products.Count} products, Total: {_totalCount}, Pages: {_totalPages}");
|
||||
|
||||
if (_products.Count > 0)
|
||||
{
|
||||
Snackbar.Add($"{_products.Count} محصول بارگذاری شد", Severity.Success);
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add("هیچ محصولی یافت نشد", Severity.Info);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error loading products: {ex.Message}");
|
||||
Console.WriteLine($"StackTrace: {ex.StackTrace}");
|
||||
Snackbar.Add($"خطا در بارگذاری محصولات: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnSearch()
|
||||
{
|
||||
await LoadProducts();
|
||||
}
|
||||
|
||||
private async Task OnCategoryFilterChanged()
|
||||
{
|
||||
await LoadProducts();
|
||||
}
|
||||
|
||||
private async Task OpenCreateDialog()
|
||||
{
|
||||
var model = new ProductFormModel();
|
||||
var parameters = new DialogParameters<ProductFormDialog>
|
||||
{
|
||||
{ x => x.Model, model },
|
||||
{ x => x.IsEditMode, false }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<ProductFormDialog>("ایجاد محصول جدید", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false, Data: ProductFormModel formData })
|
||||
{
|
||||
try
|
||||
{
|
||||
var dto = new CreateDiscountProductDto
|
||||
{
|
||||
Title = formData.Title,
|
||||
ShortInformation = formData.ShortInformation,
|
||||
FullInformation = formData.FullInformation,
|
||||
ImagePath = formData.ImagePath,
|
||||
ThumbnailPath = formData.ThumbnailPath,
|
||||
Price = formData.Price,
|
||||
MaxDiscountPercent = formData.MaxDiscountPercent,
|
||||
InitialCount = 0, // Always 0 - must use inventory management
|
||||
SortOrder = formData.SortOrder,
|
||||
IsActive = formData.IsActive,
|
||||
CategoryIds = formData.CategoryIds,
|
||||
ImageFile = formData.ImageFile,
|
||||
ImageMime = formData.ImageMime,
|
||||
ImageFileName = formData.ImageFileName,
|
||||
ThumbnailFile = formData.ThumbnailFile,
|
||||
ThumbnailMime = formData.ThumbnailMime,
|
||||
ThumbnailFileName = formData.ThumbnailFileName
|
||||
};
|
||||
|
||||
await DiscountProductService.CreateAsync(dto);
|
||||
Snackbar.Add("محصول با موفقیت ایجاد شد", Severity.Success);
|
||||
await LoadProducts();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ایجاد محصول: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenEditDialog(long productId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var product = await DiscountProductService.GetByIdAsync(productId);
|
||||
if (product == null)
|
||||
{
|
||||
Snackbar.Add("محصول یافت نشد", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var model = new ProductFormModel
|
||||
{
|
||||
Title = product.Title,
|
||||
ShortInformation = product.ShortInformation,
|
||||
FullInformation = product.FullInformation,
|
||||
ImagePath = product.ImagePath,
|
||||
ThumbnailPath = product.ThumbnailPath,
|
||||
Price = product.Price,
|
||||
MaxDiscountPercent = product.MaxDiscountPercent,
|
||||
SortOrder = product.SortOrder,
|
||||
IsActive = product.IsActive,
|
||||
CategoryIds = product.Categories?.Select(c => c.Id).ToList() ?? new()
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters<ProductFormDialog>
|
||||
{
|
||||
{ x => x.Model, model },
|
||||
{ x => x.IsEditMode, true }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<ProductFormDialog>("ویرایش محصول", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false, Data: ProductFormModel formData })
|
||||
{
|
||||
var dto = new UpdateDiscountProductDto
|
||||
{
|
||||
Title = formData.Title,
|
||||
ShortInformation = formData.ShortInformation,
|
||||
FullInformation = formData.FullInformation,
|
||||
ImagePath = formData.ImagePath,
|
||||
ThumbnailPath = formData.ThumbnailPath,
|
||||
Price = formData.Price,
|
||||
MaxDiscountPercent = formData.MaxDiscountPercent,
|
||||
SortOrder = formData.SortOrder,
|
||||
IsActive = formData.IsActive,
|
||||
CategoryIds = formData.CategoryIds,
|
||||
ImageFile = formData.ImageFile,
|
||||
ImageMime = formData.ImageMime,
|
||||
ImageFileName = formData.ImageFileName,
|
||||
ThumbnailFile = formData.ThumbnailFile,
|
||||
ThumbnailMime = formData.ThumbnailMime,
|
||||
ThumbnailFileName = formData.ThumbnailFileName
|
||||
};
|
||||
|
||||
await DiscountProductService.UpdateAsync(productId, dto);
|
||||
Snackbar.Add("محصول با موفقیت ویرایش شد", Severity.Success);
|
||||
await LoadProducts();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ویرایش محصول: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteProduct(long productId)
|
||||
{
|
||||
var result = await DialogService.ShowMessageBox(
|
||||
"تأیید حذف",
|
||||
"آیا از حذف این محصول اطمینان دارید؟",
|
||||
yesText: "بله", cancelText: "خیر");
|
||||
|
||||
if (result == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DiscountProductService.DeleteAsync(productId);
|
||||
Snackbar.Add("محصول با موفقیت حذف شد", Severity.Success);
|
||||
await LoadProducts();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در حذف محصول: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</Content>
|
||||
</BasePageComponent>
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
using System.Text;
|
||||
using BackOffice.Common.BaseComponents;
|
||||
using BackOffice.Common.Utilities;
|
||||
using BackOffice.Services.DiscountProduct;
|
||||
using BackOffice.Services.DiscountCategory;
|
||||
using BackOffice.Pages.DiscountShop.Components;
|
||||
using BackOffice.Pages.Products.Components;
|
||||
using BackOffice.Pages.Tag.Components;
|
||||
using static BackOffice.Pages.DiscountShop.Components.ProductFormDialog;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.DiscountShop;
|
||||
|
||||
public partial class DiscountProductsMainPage
|
||||
{
|
||||
[Inject] public IDiscountProductService DiscountProductService { get; set; } = default!;
|
||||
[Inject] public IDiscountCategoryService DiscountCategoryService { get; set; } = default!;
|
||||
|
||||
private MudDataGrid<DiscountProductDto>? _dataGrid;
|
||||
private BasePageComponent _basePage = default!;
|
||||
private List<DiscountProductDto> _products = new();
|
||||
private List<DiscountCategoryDto> _categories = new();
|
||||
private bool _loading;
|
||||
|
||||
private string? _searchQuery;
|
||||
private long? _categoryFilter;
|
||||
private bool? _statusFilter;
|
||||
private bool? _stockFilter;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadCategories();
|
||||
}
|
||||
|
||||
private async Task LoadCategories()
|
||||
{
|
||||
try
|
||||
{
|
||||
_categories = await DiscountCategoryService.GetCategoriesAsync(isActive: true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_categories = new();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<GridData<DiscountProductDto>> LoadServerData(GridState<DiscountProductDto> state)
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var filter = new ProductFilterDto
|
||||
{
|
||||
SearchQuery = _searchQuery,
|
||||
CategoryId = _categoryFilter,
|
||||
IsActive = _statusFilter,
|
||||
InStock = _stockFilter,
|
||||
PageNumber = state.Page + 1,
|
||||
PageSize = state.PageSize
|
||||
};
|
||||
|
||||
var (products, totalCount, _) = await DiscountProductService.GetProductsAsync(filter);
|
||||
_products = products;
|
||||
|
||||
return new GridData<DiscountProductDto>
|
||||
{
|
||||
Items = products,
|
||||
TotalItems = totalCount
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری محصولات: {ex.Message}", Severity.Error);
|
||||
return new GridData<DiscountProductDto> { Items = new List<DiscountProductDto>(), TotalItems = 0 };
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ReLoadData()
|
||||
{
|
||||
_dataGrid?.ReloadServerData();
|
||||
}
|
||||
|
||||
private async Task OnFilterSubmit()
|
||||
{
|
||||
_basePage.IsFiltered = true;
|
||||
StateHasChanged();
|
||||
ReLoadData();
|
||||
}
|
||||
|
||||
private async Task OnFilterCleared()
|
||||
{
|
||||
_basePage.IsFiltered = false;
|
||||
_searchQuery = null;
|
||||
_categoryFilter = null;
|
||||
_statusFilter = null;
|
||||
_stockFilter = null;
|
||||
StateHasChanged();
|
||||
ReLoadData();
|
||||
}
|
||||
|
||||
private async Task OpenCreateDialog()
|
||||
{
|
||||
var model = new ProductFormModel();
|
||||
var parameters = new DialogParameters<ProductFormDialog>
|
||||
{
|
||||
{ x => x.Model, model },
|
||||
{ x => x.IsEditMode, false }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<ProductFormDialog>("ایجاد محصول جدید", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false, Data: ProductFormModel formData })
|
||||
{
|
||||
try
|
||||
{
|
||||
var dto = new CreateDiscountProductDto
|
||||
{
|
||||
Title = formData.Title,
|
||||
ShortInformation = formData.ShortInformation,
|
||||
FullInformation = formData.FullInformation,
|
||||
ImagePath = formData.ImagePath,
|
||||
ThumbnailPath = formData.ThumbnailPath,
|
||||
Price = formData.Price,
|
||||
MaxDiscountPercent = formData.MaxDiscountPercent,
|
||||
InitialCount = 0,
|
||||
SortOrder = formData.SortOrder,
|
||||
IsActive = formData.IsActive,
|
||||
CategoryIds = formData.CategoryIds,
|
||||
ImageFile = formData.ImageFile,
|
||||
ImageMime = formData.ImageMime,
|
||||
ImageFileName = formData.ImageFileName,
|
||||
ThumbnailFile = formData.ThumbnailFile,
|
||||
ThumbnailMime = formData.ThumbnailMime,
|
||||
ThumbnailFileName = formData.ThumbnailFileName
|
||||
};
|
||||
|
||||
await DiscountProductService.CreateAsync(dto);
|
||||
Snackbar.Add("محصول با موفقیت ایجاد شد", Severity.Success);
|
||||
ReLoadData();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ایجاد محصول: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenEditDialog(long productId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var product = await DiscountProductService.GetByIdAsync(productId);
|
||||
if (product == null)
|
||||
{
|
||||
Snackbar.Add("محصول یافت نشد", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var model = new ProductFormModel
|
||||
{
|
||||
Title = product.Title,
|
||||
ShortInformation = product.ShortInformation,
|
||||
FullInformation = product.FullInformation,
|
||||
ImagePath = product.ImagePath,
|
||||
ThumbnailPath = product.ThumbnailPath,
|
||||
Price = product.Price,
|
||||
MaxDiscountPercent = product.MaxDiscountPercent,
|
||||
SortOrder = product.SortOrder,
|
||||
IsActive = product.IsActive,
|
||||
CategoryIds = product.Categories?.Select(c => c.Id).ToList() ?? new()
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters<ProductFormDialog>
|
||||
{
|
||||
{ x => x.Model, model },
|
||||
{ x => x.IsEditMode, true }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<ProductFormDialog>("ویرایش محصول", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false, Data: ProductFormModel formData })
|
||||
{
|
||||
var dto = new UpdateDiscountProductDto
|
||||
{
|
||||
Title = formData.Title,
|
||||
ShortInformation = formData.ShortInformation,
|
||||
FullInformation = formData.FullInformation,
|
||||
ImagePath = formData.ImagePath,
|
||||
ThumbnailPath = formData.ThumbnailPath,
|
||||
Price = formData.Price,
|
||||
MaxDiscountPercent = formData.MaxDiscountPercent,
|
||||
SortOrder = formData.SortOrder,
|
||||
IsActive = formData.IsActive,
|
||||
CategoryIds = formData.CategoryIds,
|
||||
ImageFile = formData.ImageFile,
|
||||
ImageMime = formData.ImageMime,
|
||||
ImageFileName = formData.ImageFileName,
|
||||
ThumbnailFile = formData.ThumbnailFile,
|
||||
ThumbnailMime = formData.ThumbnailMime,
|
||||
ThumbnailFileName = formData.ThumbnailFileName
|
||||
};
|
||||
|
||||
await DiscountProductService.UpdateAsync(productId, dto);
|
||||
Snackbar.Add("محصول با موفقیت ویرایش شد", Severity.Success);
|
||||
ReLoadData();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ویرایش محصول: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteProduct(long productId)
|
||||
{
|
||||
var product = _products.FirstOrDefault(p => p.Id == productId);
|
||||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small };
|
||||
bool? result = await DialogService.ShowMessageBox(
|
||||
"اخطار",
|
||||
$"آیا از حذف محصول «{product?.Title}» مطمئن هستید؟",
|
||||
yesText: "حذف", cancelText: "لغو",
|
||||
options: options);
|
||||
|
||||
if (result == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DiscountProductService.DeleteAsync(productId);
|
||||
Snackbar.Add("محصول با موفقیت حذف شد", Severity.Success);
|
||||
ReLoadData();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در حذف محصول: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenGallery(DiscountProductDto product)
|
||||
{
|
||||
var parameters = new DialogParameters<GalleryDialog>
|
||||
{
|
||||
{ x => x.ProductId, product.Id },
|
||||
{ x => x.ProductTitle, product.Title }
|
||||
};
|
||||
await DialogService.ShowAsync<GalleryDialog>("گالری تصاویر", parameters,
|
||||
new DialogOptions { CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.Medium });
|
||||
}
|
||||
|
||||
public void OpenCategoryMapping(DiscountProductDto product)
|
||||
{
|
||||
Navigation.NavigateTo($"{RouteConstance.ProductCategories}{product.Id}");
|
||||
}
|
||||
|
||||
private async Task OpenImagePreview(string? imagePath, string? title)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(imagePath))
|
||||
return;
|
||||
|
||||
var parameters = new DialogParameters<ImagePreviewDialog>
|
||||
{
|
||||
{ x => x.ImageUrl, imagePath },
|
||||
{ x => x.Title, title }
|
||||
};
|
||||
|
||||
await DialogService.ShowAsync<ImagePreviewDialog>("پیشنمایش تصویر محصول", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true });
|
||||
}
|
||||
|
||||
private async Task ExportToExcel()
|
||||
{
|
||||
try
|
||||
{
|
||||
var filter = new ProductFilterDto
|
||||
{
|
||||
SearchQuery = _searchQuery,
|
||||
CategoryId = _categoryFilter,
|
||||
IsActive = _statusFilter,
|
||||
InStock = _stockFilter,
|
||||
PageNumber = 1,
|
||||
PageSize = 1000
|
||||
};
|
||||
|
||||
var (products, _, _) = await DiscountProductService.GetProductsAsync(filter);
|
||||
|
||||
if (products == null || products.Count == 0)
|
||||
{
|
||||
Snackbar.Add("دادهای برای خروجی وجود ندارد.", Severity.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("شناسه,عنوان,قیمت,حداکثر تخفیف,موجودی,بازدید,وضعیت");
|
||||
|
||||
foreach (var p in products)
|
||||
{
|
||||
sb.AppendLine(string.Join(",",
|
||||
p.Id,
|
||||
EscapeCsv(p.Title),
|
||||
p.Price,
|
||||
p.MaxDiscountPercent,
|
||||
p.RemainingCount,
|
||||
p.ViewCount,
|
||||
p.IsActive ? "فعال" : "غیرفعال"));
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
var filename = $"discount-products-{DateTime.Now:yyyyMMddHHmmss}.csv";
|
||||
|
||||
await jsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
|
||||
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task OpenTagAssignment(DiscountProductDto model)
|
||||
{
|
||||
var parameters = new DialogParameters<AssignTagsDialog>
|
||||
{
|
||||
{ x => x.ProductId, model.Id },
|
||||
{ x => x.ProductTitle, model.Title }
|
||||
};
|
||||
|
||||
await DialogService.ShowAsync<AssignTagsDialog>("مدیریت تگهای محصول", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true });
|
||||
}
|
||||
|
||||
private static string EscapeCsv(string? value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return "";
|
||||
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
|
||||
return "\"" + value.Replace("\"", "\"\"") + "\"";
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
@page "/discount-shop"
|
||||
@page "/discount-orders"
|
||||
@page "/discount-sales-reports"
|
||||
@attribute [Authorize(Roles = "Administrator")]
|
||||
|
||||
<div>
|
||||
<MudText Typo="Typo.h5" Class="mb-3">سفارشات فروشگاه تخفیفی</MudText>
|
||||
|
||||
<MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true"
|
||||
PanelClass="pt-4" @bind-ActivePanelIndex="_activeTab">
|
||||
<MudTabPanel Text="سفارشات" Icon="@Icons.Material.Filled.ReceiptLong">
|
||||
<DiscountOrdersMainPage />
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="گزارش فروش" Icon="@Icons.Material.Filled.BarChart">
|
||||
<SalesReports />
|
||||
</MudTabPanel>
|
||||
</MudTabs>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private int _activeTab;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
var uri = Navigation.ToBaseRelativePath(Navigation.Uri).ToLowerInvariant().TrimEnd('/');
|
||||
_activeTab = uri switch
|
||||
{
|
||||
"discount-sales-reports" => 1,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
@page "/discount-sales-reports"
|
||||
@* Merged into /discount-shop *@
|
||||
@attribute [Authorize(Roles = "Administrator")]
|
||||
|
||||
@using MudBlazor
|
||||
@@ -10,13 +10,13 @@
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IJSRuntime JsRuntime
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-4">
|
||||
<div>
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">
|
||||
<MudIcon Icon="@Icons.Material.Filled.BarChart" Class="ml-2" />
|
||||
گزارش فروش فروشگاه تخفیفی
|
||||
گزارش فروش فروشگاه
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary" Class="mb-4">
|
||||
آمار فروش، تخفیف و وضعیت سفارشهای فروشگاه تخفیفی بر اساس بازه تاریخ و وضعیت سفارش
|
||||
آمار فروش و وضعیت سفارشها بر اساس بازه تاریخ و وضعیت سفارش
|
||||
</MudText>
|
||||
|
||||
<MudPaper Class="pa-4 mb-4">
|
||||
@@ -42,14 +42,14 @@
|
||||
Clearable="true"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense">
|
||||
<MudSelectItem Value="@(null as OrderStatus?)">همه وضعیتها</MudSelectItem>
|
||||
<MudSelectItem Value="OrderStatus.Pending">در انتظار</MudSelectItem>
|
||||
<MudSelectItem Value="OrderStatus.Paid">پرداخت شده</MudSelectItem>
|
||||
<MudSelectItem Value="OrderStatus.Processing">در حال پردازش</MudSelectItem>
|
||||
<MudSelectItem Value="OrderStatus.Shipped">ارسال شده</MudSelectItem>
|
||||
<MudSelectItem Value="OrderStatus.Delivered">تحویل شده</MudSelectItem>
|
||||
<MudSelectItem Value="OrderStatus.Cancelled">لغو شده</MudSelectItem>
|
||||
<MudSelectItem Value="OrderStatus.Returned">مرجوع شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="@(null as OrderStatus?)">همه وضعیتها</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Pending">در انتظار</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Paid">پرداخت شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Processing">در حال پردازش</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Shipped">ارسال شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Delivered">تحویل شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Cancelled">لغو شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Returned">مرجوع شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
@@ -137,14 +137,49 @@
|
||||
<MudText Typo="Typo.h6">روند فروش</MudText>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudChart ChartType="ChartType.Line"
|
||||
ChartSeries="@_salesSeries"
|
||||
XAxisLabels="@_salesLabels"
|
||||
Width="100%"
|
||||
Height="300px" />
|
||||
@if (_salesLabels.Length == 0)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">
|
||||
داده کافی برای نمایش نمودار وجود ندارد.
|
||||
</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChart ChartType="ChartType.Line"
|
||||
ChartSeries="@_salesSeries"
|
||||
XAxisLabels="@_salesLabels"
|
||||
Width="100%"
|
||||
Height="300px" />
|
||||
}
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
<MudCard>
|
||||
<MudCardHeader>
|
||||
<MudText Typo="Typo.h6">وضعیت سفارشها</MudText>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
@if (_statusChartLabels.Length == 0)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">
|
||||
داده کافی برای نمایش نمودار وجود ندارد.
|
||||
</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChart ChartType="ChartType.Bar"
|
||||
ChartSeries="@_statusChartSeries"
|
||||
XAxisLabels="@_statusChartLabels"
|
||||
Width="100%"
|
||||
Height="300px" />
|
||||
}
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudGrid Class="mb-4">
|
||||
<MudItem xs="12" md="6">
|
||||
<MudCard>
|
||||
<MudCardHeader>
|
||||
@@ -180,10 +215,24 @@
|
||||
Hover="true"
|
||||
Dense="true">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.OrderNumber" Title="شماره سفارش" />
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
<TemplateColumn Title="نام کاربر">
|
||||
<CellTemplate>
|
||||
@if (context.Item.UserId > 0)
|
||||
{
|
||||
<MudLink Href="@($"/network/user-info/{context.Item.UserId}")" Underline="Underline.Hover" Color="Color.Primary">
|
||||
@context.Item.UserFullName
|
||||
</MudLink>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText>@context.Item.UserFullName</MudText>
|
||||
}
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="تاریخ ثبت">
|
||||
<CellTemplate>
|
||||
@(context.Item.Created?.ToString("yyyy/MM/dd HH:mm") ?? "-")
|
||||
@(context.Item.Created?.MiladiToJalaliWithTime() ?? "-")
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="تعداد آیتم">
|
||||
@@ -211,11 +260,14 @@
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
</MudDataGrid>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
}
|
||||
</MudContainer>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private readonly List<DiscountOrderDto> _orders = new();
|
||||
@@ -230,6 +282,8 @@
|
||||
private long _totalSales;
|
||||
private long _totalDiscount;
|
||||
private long _averageOrder;
|
||||
private int _uniqueCustomers;
|
||||
private int _totalProductsSold;
|
||||
|
||||
private string[] _salesLabels = Array.Empty<string>();
|
||||
private List<ChartSeries> _salesSeries = new();
|
||||
@@ -237,6 +291,9 @@
|
||||
private string[] _topProductLabels = Array.Empty<string>();
|
||||
private List<ChartSeries> _topProductSeries = new();
|
||||
|
||||
private string[] _statusChartLabels = Array.Empty<string>();
|
||||
private List<ChartSeries> _statusChartSeries = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadReports();
|
||||
@@ -255,32 +312,82 @@
|
||||
|
||||
try
|
||||
{
|
||||
// ۱) گزارش سرور-ساید (خلاصه + نمودار روزانه + محصولات پرفروش)
|
||||
DiscountSalesReportDto? report = null;
|
||||
if (_fromDate.HasValue && _toDate.HasValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
report = await DiscountOrderService.GetSalesReportAsync(_fromDate.Value, _toDate.Value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// اگر RPC سرور ساید پیادهسازی نشده بود، fallback به کلاینتساید
|
||||
}
|
||||
}
|
||||
|
||||
// ۲) لیست سفارشات برای جدول و نمودار وضعیت
|
||||
var filter = new OrderFilterDto
|
||||
{
|
||||
Status = _statusFilter,
|
||||
FromDate = _fromDate,
|
||||
ToDate = _toDate,
|
||||
PageNumber = 1,
|
||||
PageSize = 200
|
||||
};
|
||||
|
||||
var (orders, _, _) = await DiscountOrderService.GetOrdersAsync(filter);
|
||||
_orders.AddRange(orders);
|
||||
|
||||
BuildSummary();
|
||||
BuildSalesChart();
|
||||
await BuildTopProductsChart();
|
||||
if (report?.Summary != null && report.Summary.TotalOrders > 0)
|
||||
{
|
||||
// استفاده از گزارش سرور-ساید
|
||||
_totalOrders = report.Summary.TotalOrders;
|
||||
_totalSales = report.Summary.TotalGatewayPaid;
|
||||
_totalDiscount = report.Summary.TotalDiscountUsed;
|
||||
_averageOrder = report.Summary.AverageOrderValue;
|
||||
_uniqueCustomers = report.Summary.UniqueCustomers;
|
||||
_totalProductsSold = report.Summary.TotalProductsSold;
|
||||
|
||||
// نمودار روند فروش از periods
|
||||
if (report.Periods.Count > 0)
|
||||
{
|
||||
_salesLabels = report.Periods.Select(p => p.PeriodLabel).ToArray();
|
||||
_salesSeries = new List<ChartSeries>
|
||||
{
|
||||
new ChartSeries { Name = "مبلغ نهایی", Data = report.Periods.Select(p => (double)p.GatewayPaid).ToArray() },
|
||||
new ChartSeries { Name = "تخفیف", Data = report.Periods.Select(p => (double)p.DiscountUsed).ToArray() }
|
||||
};
|
||||
}
|
||||
|
||||
// نمودار محصولات پرفروش
|
||||
if (report.TopProducts.Count > 0)
|
||||
{
|
||||
_topProductLabels = report.TopProducts.Take(5).Select(x => x.ProductTitle).ToArray();
|
||||
_topProductSeries = new List<ChartSeries>
|
||||
{
|
||||
new ChartSeries
|
||||
{
|
||||
Name = "مبلغ فروش",
|
||||
Data = report.TopProducts.Take(5).Select(x => (double)x.TotalRevenue).ToArray()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: محاسبه کلاینتساید
|
||||
BuildSummary();
|
||||
BuildSalesChart();
|
||||
BuildTopProductsChartFromOrders();
|
||||
}
|
||||
|
||||
// نمودار وضعیت همیشه از لیست سفارشات
|
||||
BuildStatusChart();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری گزارش فروش: {ex.Message}", Severity.Error);
|
||||
_orders.Clear();
|
||||
_totalOrders = 0;
|
||||
_totalSales = 0;
|
||||
_totalDiscount = 0;
|
||||
_averageOrder = 0;
|
||||
_salesLabels = Array.Empty<string>();
|
||||
_salesSeries = new List<ChartSeries>();
|
||||
_topProductLabels = Array.Empty<string>();
|
||||
_topProductSeries = new List<ChartSeries>();
|
||||
ClearData();
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -288,6 +395,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearData()
|
||||
{
|
||||
_orders.Clear();
|
||||
_totalOrders = 0;
|
||||
_totalSales = 0;
|
||||
_totalDiscount = 0;
|
||||
_averageOrder = 0;
|
||||
_uniqueCustomers = 0;
|
||||
_totalProductsSold = 0;
|
||||
_salesLabels = Array.Empty<string>();
|
||||
_salesSeries = new List<ChartSeries>();
|
||||
_topProductLabels = Array.Empty<string>();
|
||||
_topProductSeries = new List<ChartSeries>();
|
||||
_statusChartLabels = Array.Empty<string>();
|
||||
_statusChartSeries = new List<ChartSeries>();
|
||||
}
|
||||
|
||||
private void BuildSummary()
|
||||
{
|
||||
_totalOrders = _orders.Count;
|
||||
@@ -312,7 +436,7 @@
|
||||
.ToList();
|
||||
|
||||
_salesLabels = grouped
|
||||
.Select(g => g.Key.ToString("yyyy/MM/dd"))
|
||||
.Select(g => g.Key.MiladiToJalali())
|
||||
.ToArray();
|
||||
|
||||
var totalSeries = grouped
|
||||
@@ -330,73 +454,41 @@
|
||||
};
|
||||
}
|
||||
|
||||
private async Task BuildTopProductsChart()
|
||||
private void BuildStatusChart()
|
||||
{
|
||||
_topProductLabels = Array.Empty<string>();
|
||||
_topProductSeries = new List<ChartSeries>();
|
||||
|
||||
if (_orders.Count == 0)
|
||||
return;
|
||||
|
||||
// برای جلوگیری از فشار بیشازحد، فقط روی 50 سفارش اول کار میکنیم
|
||||
var sampleOrders = _orders
|
||||
.Where(o => o.Created.HasValue)
|
||||
.OrderByDescending(o => o.Created)
|
||||
.Take(50)
|
||||
.ToList();
|
||||
|
||||
var productTotals = new Dictionary<long, (string Name, long Amount)>();
|
||||
|
||||
foreach (var order in sampleOrders)
|
||||
{
|
||||
DiscountOrderDetailsDto? details;
|
||||
try
|
||||
{
|
||||
details = await DiscountOrderService.GetByIdAsync(order.Id);
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (details?.Items == null)
|
||||
continue;
|
||||
|
||||
foreach (var item in details.Items)
|
||||
{
|
||||
if (!productTotals.TryGetValue(item.ProductId, out var existing))
|
||||
{
|
||||
productTotals[item.ProductId] = (item.ProductTitle, item.TotalPrice);
|
||||
}
|
||||
else
|
||||
{
|
||||
productTotals[item.ProductId] = (existing.Name, existing.Amount + item.TotalPrice);
|
||||
}
|
||||
}
|
||||
_statusChartLabels = Array.Empty<string>();
|
||||
_statusChartSeries = new List<ChartSeries>();
|
||||
return;
|
||||
}
|
||||
|
||||
if (productTotals.Count == 0)
|
||||
return;
|
||||
|
||||
var top = productTotals.Values
|
||||
.OrderByDescending(x => x.Amount)
|
||||
.Take(5)
|
||||
var groups = _orders
|
||||
.GroupBy(x => x.Status)
|
||||
.OrderBy(g => (int)g.Key)
|
||||
.ToList();
|
||||
|
||||
_topProductLabels = top
|
||||
.Select(x => x.Name)
|
||||
_statusChartLabels = groups
|
||||
.Select(g => GetStatusText(g.Key))
|
||||
.ToArray();
|
||||
|
||||
_topProductSeries = new List<ChartSeries>
|
||||
_statusChartSeries = new List<ChartSeries>
|
||||
{
|
||||
new ChartSeries
|
||||
{
|
||||
Name = "مبلغ فروش",
|
||||
Data = top.Select(x => (double)x.Amount).ToArray()
|
||||
Name = "تعداد سفارشها",
|
||||
Data = groups.Select(g => (double)g.Count()).ToArray()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void BuildTopProductsChartFromOrders()
|
||||
{
|
||||
// Fallback ساده بدون N+1 — فقط از اطلاعات لیست سفارشات
|
||||
_topProductLabels = Array.Empty<string>();
|
||||
_topProductSeries = new List<ChartSeries>();
|
||||
}
|
||||
|
||||
private Color GetStatusColor(OrderStatus status)
|
||||
{
|
||||
return status switch
|
||||
@@ -436,20 +528,22 @@
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("OrderNumber,Created,ItemsCount,GatewayAmount,DiscountBalanceUsed,Status");
|
||||
sb.AppendLine("شناسه,نام کاربر,موبایل,تاریخ ثبت,تعداد آیتم,مبلغ نهایی,تخفیف,وضعیت");
|
||||
|
||||
foreach (var o in _orders.Where(o => o.Created.HasValue).OrderBy(o => o.Created))
|
||||
{
|
||||
sb.AppendLine(string.Join(",",
|
||||
EscapeCsv(o.OrderNumber),
|
||||
o.Created?.ToString("yyyy-MM-dd HH:mm") ?? "-",
|
||||
o.Id,
|
||||
EscapeCsv(o.UserFullName),
|
||||
EscapeCsv(o.UserMobile),
|
||||
o.Created?.MiladiToJalaliWithTime() ?? "-",
|
||||
o.ItemsCount,
|
||||
o.GatewayAmount,
|
||||
o.DiscountBalanceUsed,
|
||||
GetStatusText(o.Status)));
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(sb.ToString());
|
||||
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
var filename = $"discount-sales-{DateTime.Now:yyyyMMddHHmmss}.csv";
|
||||
|
||||
@@ -467,7 +561,7 @@
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("گزارش فروش فروشگاه تخفیفی");
|
||||
sb.AppendLine($"بازه: از {_fromDate:yyyy/MM/dd} تا {_toDate:yyyy/MM/dd}");
|
||||
sb.AppendLine($"بازه: از {_fromDate?.MiladiToJalali() ?? "-"} تا {_toDate?.MiladiToJalali() ?? "-"}");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("خلاصه:");
|
||||
@@ -478,15 +572,15 @@
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("جزئیات سفارشها:");
|
||||
sb.AppendLine("شماره سفارش | تاریخ | تعداد آیتم | مبلغ نهایی | تخفیف | وضعیت");
|
||||
sb.AppendLine("شناسه | نام کاربر | تاریخ | تعداد آیتم | مبلغ نهایی | تخفیف | وضعیت");
|
||||
|
||||
foreach (var o in _orders.Where(o => o.Created.HasValue).OrderBy(o => o.Created))
|
||||
{
|
||||
sb.AppendLine(
|
||||
$"{o.OrderNumber} | {o.Created?.ToString("yyyy/MM/dd HH:mm") ?? "-"} | {o.ItemsCount} | {o.GatewayAmount:N0} | {o.DiscountBalanceUsed:N0} | {GetStatusText(o.Status)}");
|
||||
$"{o.Id} | {o.UserFullName} | {o.Created?.MiladiToJalaliWithTime() ?? "-"} | {o.ItemsCount} | {o.GatewayAmount:N0} | {o.DiscountBalanceUsed:N0} | {GetStatusText(o.Status)}");
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(sb.ToString());
|
||||
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
var filename = $"discount-sales-{DateTime.Now:yyyyMMddHHmmss}.txt";
|
||||
|
||||
|
||||
+322
-117
@@ -1,4 +1,5 @@
|
||||
@attribute [Route(RouteConstance.HomePage)]
|
||||
@attribute [Route("/dashboard/overview")]
|
||||
|
||||
@using CMSMicroservice.Protobuf.Protos.User
|
||||
@using CMSMicroservice.Protobuf.Protos.UserOrder
|
||||
@@ -6,109 +7,237 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.Products
|
||||
@using CMSMicroservice.Protobuf.Protos.Package
|
||||
@using CMSMicroservice.Protobuf.Protos.Category
|
||||
@using CMSMicroservice.Protobuf.Protos.Commission
|
||||
@using CMSMicroservice.Protobuf.Protos.ClubMembership
|
||||
@inject UserContract.UserContractClient UserContract
|
||||
@inject UserOrderContract.UserOrderContractClient UserOrderContract
|
||||
@inject ProductsContract.ProductsContractClient ProductsContract
|
||||
@inject PackageContract.PackageContractClient PackageContract
|
||||
@inject CategoryContract.CategoryContractClient CategoryContract
|
||||
@inject CommissionContract.CommissionContractClient CommissionClient
|
||||
@inject ClubMembershipContract.ClubMembershipContractClient ClubClient
|
||||
|
||||
<MudStack Spacing="3">
|
||||
<MudText Typo="Typo.h5">داشبورد مدیریت</MudText>
|
||||
|
||||
<MudGrid GutterSize="3">
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4">
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.subtitle2">تعداد کاربران</MudText>
|
||||
<MudText Typo="Typo.h5">@_totalUsers.ToString("N0")</MudText>
|
||||
@* ── عنوان و وضعیت بارگذاری ── *@
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.h5">داشبورد مدیریت</MudText>
|
||||
@if (_loading)
|
||||
{
|
||||
<MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true" />
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
@* ── آمار کلی فروشگاه ── *@
|
||||
<MudGrid Spacing="2">
|
||||
<MudItem xs="6" sm="4" md="2">
|
||||
<MudPaper Class="pa-3 rounded-lg" Elevation="0" Outlined="true">
|
||||
<MudStack Spacing="1" AlignItems="AlignItems.Start">
|
||||
<MudIcon Icon="@Icons.Material.Filled.People" Color="Color.Primary" Size="Size.Small" />
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">کاربران</MudText>
|
||||
<MudText Typo="Typo.h6">@_totalUsers.ToString("N0")</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4">
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.subtitle2">تعداد محصولات</MudText>
|
||||
<MudText Typo="Typo.h5">@_totalProducts.ToString("N0")</MudText>
|
||||
<MudItem xs="6" sm="4" md="2">
|
||||
<MudPaper Class="pa-3 rounded-lg" Elevation="0" Outlined="true">
|
||||
<MudStack Spacing="1" AlignItems="AlignItems.Start">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ShoppingBag" Color="Color.Info" Size="Size.Small" />
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">محصولات</MudText>
|
||||
<MudText Typo="Typo.h6">@_totalProducts.ToString("N0")</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4">
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.subtitle2">تعداد پکیجها</MudText>
|
||||
<MudText Typo="Typo.h5">@_totalPackages.ToString("N0")</MudText>
|
||||
<MudItem xs="6" sm="4" md="2">
|
||||
<MudPaper Class="pa-3 rounded-lg" Elevation="0" Outlined="true">
|
||||
<MudStack Spacing="1" AlignItems="AlignItems.Start">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Inventory2" Color="Color.Secondary" Size="Size.Small" />
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">پکیجها</MudText>
|
||||
<MudText Typo="Typo.h6">@_totalPackages.ToString("N0")</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4">
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.subtitle2">تعداد دستهبندیها</MudText>
|
||||
<MudText Typo="Typo.h5">@_totalCategories.ToString("N0")</MudText>
|
||||
<MudItem xs="6" sm="4" md="2">
|
||||
<MudPaper Class="pa-3 rounded-lg" Elevation="0" Outlined="true">
|
||||
<MudStack Spacing="1" AlignItems="AlignItems.Start">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Category" Color="Color.Tertiary" Size="Size.Small" />
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">دستهبندیها</MudText>
|
||||
<MudText Typo="Typo.h6">@_totalCategories.ToString("N0")</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4">
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.subtitle2">تعداد سفارشها</MudText>
|
||||
<MudText Typo="Typo.h5">@_totalOrders.ToString("N0")</MudText>
|
||||
<MudItem xs="6" sm="4" md="2">
|
||||
<MudPaper Class="pa-3 rounded-lg" Elevation="0" Outlined="true">
|
||||
<MudStack Spacing="1" AlignItems="AlignItems.Start">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Receipt" Color="Color.Success" Size="Size.Small" />
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">سفارشها</MudText>
|
||||
<MudText Typo="Typo.h6">@_totalOrders.ToString("N0")</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4">
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.subtitle2">سفارشهای پرداختشده</MudText>
|
||||
<MudText Typo="Typo.h5">@_paidOrders.ToString("N0")</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4">
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.subtitle2">جمع مبلغ سفارشهای پرداختشده</MudText>
|
||||
<MudText Typo="Typo.h6">@_totalPaidAmount.ToString("N0") تومان</MudText>
|
||||
<MudItem xs="6" sm="4" md="2">
|
||||
<MudPaper Class="pa-3 rounded-lg" Elevation="0" Outlined="true">
|
||||
<MudStack Spacing="1" AlignItems="AlignItems.Start">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Paid" Color="Color.Warning" Size="Size.Small" />
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">مبلغ پرداختشده</MudText>
|
||||
<MudText Typo="Typo.h6">@_totalPaidAmount.ToString("N0")</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudPaper Class="pa-4">
|
||||
<MudStack Spacing="2">
|
||||
<MudText Typo="Typo.subtitle2">آخرین سفارشهای پرداختشده</MudText>
|
||||
@if (_lastOrders.Count == 0)
|
||||
{
|
||||
<MudText Typo="Typo.body2">سفارشی ثبت نشده است.</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTable Items="_lastOrders" Dense="true">
|
||||
<HeaderContent>
|
||||
<MudTh>شناسه سفارش</MudTh>
|
||||
<MudTh>کاربر</MudTh>
|
||||
<MudTh>مبلغ</MudTh>
|
||||
<MudTh>تاریخ پرداخت</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.Id</MudTd>
|
||||
<MudTd>@context.UserFullName (@context.UserNationalCode)</MudTd>
|
||||
<MudTd>@context.Amount.ToString("N0")</MudTd>
|
||||
<MudTd>@context.PaymentDate.ToDateTime().MiladiToJalali()</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
}
|
||||
@* ── کمیسیون هفته جاری ── *@
|
||||
<MudPaper Class="pa-4 rounded-lg" Outlined="true">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-3">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.subtitle1">کمیسیون هفته جاری</MudText>
|
||||
</MudStack>
|
||||
<MudButton Variant="Variant.Text" Color="Color.Primary" Href="/commission/dashboard" Size="Size.Small">
|
||||
جزئیات بیشتر
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
<MudGrid Spacing="2">
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">هفته جاری</MudText>
|
||||
<MudText Typo="Typo.h6">@(_currentWeekDisplayName ?? "—")</MudText>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">مبلغ کل استخر</MudText>
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary">
|
||||
@(_currentPool?.TotalPoolAmount.ToString("N0") ?? "0") ریال
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">مجموع موجودیها</MudText>
|
||||
<MudText Typo="Typo.h6">@(_currentPool?.TotalBalances.ToString("N0") ?? "0")</MudText>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">وضعیت</MudText>
|
||||
<MudChip T="string" Color="@(_currentPool?.IsCalculated == true ? Color.Success : Color.Warning)"
|
||||
Size="Size.Small">
|
||||
@(_currentPool?.IsCalculated == true ? "محاسبه شده" : "در انتظار")
|
||||
</MudChip>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
||||
@* ── آمار باشگاه ── *@
|
||||
<MudPaper Class="pa-4 rounded-lg" Outlined="true">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-3">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.CardMembership" Color="Color.Secondary" />
|
||||
<MudText Typo="Typo.subtitle1">عضویت باشگاه</MudText>
|
||||
</MudStack>
|
||||
<MudButton Variant="Variant.Text" Color="Color.Primary" Href="/club/members" Size="Size.Small">
|
||||
مشاهده اعضا
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
<MudGrid Spacing="2">
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">کل اعضا</MudText>
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary">@_totalClubMembers.ToString("N0")</MudText>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">فعال</MudText>
|
||||
<MudText Typo="Typo.h6" Color="Color.Success">@_activeClubMembers.ToString("N0")</MudText>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">غیرفعال</MudText>
|
||||
<MudText Typo="Typo.h6" Color="Color.Warning">@_inactiveClubMembers.ToString("N0")</MudText>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
||||
@* ── آخرین سفارشها ── *@
|
||||
<MudPaper Class="pa-4 rounded-lg" Outlined="true">
|
||||
<MudText Typo="Typo.subtitle1" Class="mb-2">آخرین سفارشهای پرداختشده</MudText>
|
||||
@if (_lastOrders.Count == 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text">سفارشی ثبت نشده است.</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTable Items="_lastOrders" Dense="true" Hover="true" Elevation="0">
|
||||
<HeaderContent>
|
||||
<MudTh>شناسه</MudTh>
|
||||
<MudTh>کاربر</MudTh>
|
||||
<MudTh>مبلغ</MudTh>
|
||||
<MudTh>تاریخ پرداخت</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.Id</MudTd>
|
||||
<MudTd>@context.UserFullName</MudTd>
|
||||
<MudTd>@context.Amount.ToString("N0")</MudTd>
|
||||
<MudTd>@(context.PaymentDate != null ? context.PaymentDate.ToDateTime().MiladiToJalali() : "—")</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
}
|
||||
</MudPaper>
|
||||
|
||||
@* ── دسترسی سریع ── *@
|
||||
<MudPaper Class="pa-4 rounded-lg" Outlined="true">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mb-3">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Speed" Color="Color.Info" />
|
||||
<MudText Typo="Typo.subtitle1">دسترسی سریع</MudText>
|
||||
</MudStack>
|
||||
<MudGrid Spacing="2">
|
||||
<MudItem xs="6" md="4">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary" FullWidth="true"
|
||||
Href="/commission/dashboard" StartIcon="@Icons.Material.Filled.AccountBalanceWallet">
|
||||
داشبورد کمیسیون
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
<MudItem xs="6" md="4">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" FullWidth="true"
|
||||
Href="/commission/payouts" StartIcon="@Icons.Material.Filled.Payments">
|
||||
واریزیها
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
<MudItem xs="6" md="4">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Warning" FullWidth="true"
|
||||
Href="/commission/withdrawals" StartIcon="@Icons.Material.Filled.RequestQuote">
|
||||
درخواستهای برداشت
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
<MudItem xs="6" md="4">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Info" FullWidth="true"
|
||||
Href="/network/tree" StartIcon="@Icons.Material.Filled.AccountTree">
|
||||
درخت شبکه
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
<MudItem xs="6" md="4">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Success" FullWidth="true"
|
||||
Href="/network/balances" StartIcon="@Icons.Material.Filled.AccountBalance">
|
||||
گزارش موجودیها
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
<MudItem xs="6" md="4">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Tertiary" FullWidth="true"
|
||||
Href="/club/members" StartIcon="@Icons.Material.Filled.Groups">
|
||||
مدیریت باشگاه
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
</MudStack>
|
||||
|
||||
@code {
|
||||
private bool _loading = true;
|
||||
private long _totalUsers;
|
||||
private long _totalOrders;
|
||||
private long _paidOrders;
|
||||
@@ -118,63 +247,139 @@
|
||||
private long _totalCategories;
|
||||
private readonly List<GetUserOrderResponse> _lastOrders = new();
|
||||
|
||||
// Commission
|
||||
private string? _currentWeekDisplayName;
|
||||
private long _currentWeekDefinitionId;
|
||||
private GetWeeklyCommissionPoolResponse? _currentPool;
|
||||
|
||||
// Club
|
||||
private long _totalClubMembers;
|
||||
private long _activeClubMembers;
|
||||
private long _inactiveClubMembers;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// کاربران
|
||||
var userRequest = new GetAllUserByFilterRequest
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
PaginationState = new() { PageNumber = 1, PageSize = 1 },
|
||||
Filter = new()
|
||||
};
|
||||
var userResult = await UserContract.GetAllUserByFilterAsync(userRequest);
|
||||
_totalUsers = userResult?.MetaData?.TotalCount ?? 0;
|
||||
|
||||
// سفارشها
|
||||
var orderRequest = new GetAllUserOrderByFilterRequest
|
||||
{
|
||||
PaginationState = new() { PageNumber = 1, PageSize = 10 },
|
||||
};
|
||||
var orderResult = await UserOrderContract.GetAllUserOrderByFilterAsync(orderRequest);
|
||||
if (orderResult != null && orderResult.Models != null)
|
||||
{
|
||||
_totalOrders = orderResult.MetaData?.TotalCount ?? 0;
|
||||
_paidOrders = orderResult.Models.Count(m => m.PaymentStatus==PaymentStatus.Success);
|
||||
_totalPaidAmount = orderResult.Models
|
||||
.Where(m => m.PaymentStatus==PaymentStatus.Success)
|
||||
.Aggregate(0L, (sum, m) => sum + m.Amount);
|
||||
|
||||
foreach (var item in orderResult.Models.OrderByDescending(m => m.PaymentDate).Take(5))
|
||||
// Load all data in parallel for speed
|
||||
var tasks = new List<Task>
|
||||
{
|
||||
var full = await UserOrderContract.GetUserOrderAsync(new GetUserOrderRequest { Id = item.Id });
|
||||
_lastOrders.Add(full);
|
||||
LoadCmsStats(),
|
||||
LoadCommissionStats(),
|
||||
LoadClubStats(),
|
||||
LoadRecentOrders()
|
||||
};
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
catch { /* Individual loaders handle their own errors */ }
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadCmsStats()
|
||||
{
|
||||
try
|
||||
{
|
||||
var userCall = UserContract.GetAllUserByFilterAsync(new GetAllUserByFilterRequest
|
||||
{
|
||||
PaginationState = new() { PageNumber = 1, PageSize = 1 },
|
||||
Filter = new()
|
||||
});
|
||||
var productCall = ProductsContract.GetAllProductsByFilterAsync(new GetAllProductsByFilterRequest
|
||||
{
|
||||
PaginationState = new() { PageNumber = 1, PageSize = 1 },
|
||||
Filter = new()
|
||||
});
|
||||
var packageCall = PackageContract.GetAllPackageByFilterAsync(new GetAllPackageByFilterRequest
|
||||
{
|
||||
PaginationState = new() { PageNumber = 1, PageSize = 1 },
|
||||
Filter = new()
|
||||
});
|
||||
var categoryCall = CategoryContract.GetAllCategoryByFilterAsync(new GetAllCategoryByFilterRequest
|
||||
{
|
||||
PaginationState = new() { PageNumber = 1, PageSize = 1 },
|
||||
Filter = new()
|
||||
});
|
||||
|
||||
await Task.WhenAll(userCall.ResponseAsync, productCall.ResponseAsync, packageCall.ResponseAsync, categoryCall.ResponseAsync);
|
||||
|
||||
_totalUsers = (await userCall)?.MetaData?.TotalCount ?? 0;
|
||||
_totalProducts = (await productCall)?.MetaData?.TotalCount ?? 0;
|
||||
_totalPackages = (await packageCall)?.MetaData?.TotalCount ?? 0;
|
||||
_totalCategories = (await categoryCall)?.MetaData?.TotalCount ?? 0;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private async Task LoadCommissionStats()
|
||||
{
|
||||
try
|
||||
{
|
||||
var availableWeeks = await CommissionClient.GetAvailableWeeksAsync(new GetAvailableWeeksRequest
|
||||
{
|
||||
FutureWeeksCount = 0,
|
||||
PastWeeksCount = 0
|
||||
});
|
||||
if (availableWeeks?.CurrentWeek != null)
|
||||
{
|
||||
_currentWeekDefinitionId = availableWeeks.CurrentWeek.WeekDefinitionId;
|
||||
_currentWeekDisplayName = availableWeeks.CurrentWeek.DisplayName;
|
||||
|
||||
_currentPool = await CommissionClient.GetWeeklyCommissionPoolAsync(new GetWeeklyCommissionPoolRequest
|
||||
{
|
||||
WeekDefinitionId = _currentWeekDefinitionId
|
||||
});
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// محصولات
|
||||
var productRequest = new GetAllProductsByFilterRequest
|
||||
private async Task LoadClubStats()
|
||||
{
|
||||
try
|
||||
{
|
||||
PaginationState = new() { PageNumber = 1, PageSize = 1 },
|
||||
Filter = new()
|
||||
};
|
||||
var productResult = await ProductsContract.GetAllProductsByFilterAsync(productRequest);
|
||||
_totalProducts = productResult?.MetaData?.TotalCount ?? 0;
|
||||
var allReq = ClubClient.GetAllClubMembershipsAsync(new GetAllClubMembershipsRequest { PageIndex = 1, PageSize = 1 });
|
||||
var activeReq = ClubClient.GetAllClubMembershipsAsync(new GetAllClubMembershipsRequest { PageIndex = 1, PageSize = 1, IsActive = true });
|
||||
|
||||
// پکیجها
|
||||
var packageRequest = new GetAllPackageByFilterRequest
|
||||
{
|
||||
PaginationState = new() { PageNumber = 1, PageSize = 1 },
|
||||
Filter = new()
|
||||
};
|
||||
var packageResult = await PackageContract.GetAllPackageByFilterAsync(packageRequest);
|
||||
_totalPackages = packageResult?.MetaData?.TotalCount ?? 0;
|
||||
await Task.WhenAll(allReq.ResponseAsync, activeReq.ResponseAsync);
|
||||
|
||||
// دستهبندیها
|
||||
var categoryRequest = new GetAllCategoryByFilterRequest
|
||||
_totalClubMembers = (await allReq).MetaData?.TotalCount ?? 0;
|
||||
_activeClubMembers = (await activeReq).MetaData?.TotalCount ?? 0;
|
||||
_inactiveClubMembers = _totalClubMembers - _activeClubMembers;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private async Task LoadRecentOrders()
|
||||
{
|
||||
try
|
||||
{
|
||||
PaginationState = new() { PageNumber = 1, PageSize = 1 },
|
||||
Filter = new()
|
||||
};
|
||||
var categoryResult = await CategoryContract.GetAllCategoryByFilterAsync(categoryRequest);
|
||||
_totalCategories = categoryResult?.MetaData?.TotalCount ?? 0;
|
||||
var orderResult = await UserOrderContract.GetAllUserOrderByFilterAsync(new GetAllUserOrderByFilterRequest
|
||||
{
|
||||
PaginationState = new() { PageNumber = 1, PageSize = 5 },
|
||||
});
|
||||
if (orderResult?.Models != null)
|
||||
{
|
||||
_totalOrders = orderResult.MetaData?.TotalCount ?? 0;
|
||||
_paidOrders = orderResult.Models.Count(m => m.PaymentStatus == PaymentStatus.Success);
|
||||
_totalPaidAmount = orderResult.Models
|
||||
.Where(m => m.PaymentStatus == PaymentStatus.Success)
|
||||
.Sum(m => m.Amount);
|
||||
|
||||
foreach (var item in orderResult.Models.OrderByDescending(m => m.PaymentDate).Take(5))
|
||||
{
|
||||
try
|
||||
{
|
||||
var full = await UserOrderContract.GetUserOrderAsync(new GetUserOrderRequest { Id = item.Id });
|
||||
_lastOrders.Add(full);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
Variant="Variant.Outlined"
|
||||
Required="true"
|
||||
RequiredError="نوع محصول الزامی است">
|
||||
<MudSelectItem Value="InventoryProductType.RegularProduct">محصول عادی</MudSelectItem>
|
||||
<MudSelectItem Value="InventoryProductType.DiscountProduct">محصول تخفیفی</MudSelectItem>
|
||||
<MudSelectItem T="InventoryProductType" Value="InventoryProductType.RegularProduct">محصول عادی</MudSelectItem>
|
||||
<MudSelectItem T="InventoryProductType" Value="InventoryProductType.DiscountProduct">محصول تخفیفی</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
@@ -44,7 +44,7 @@
|
||||
RequiredError="انبار الزامی است">
|
||||
@foreach (var wh in Warehouses)
|
||||
{
|
||||
<MudSelectItem Value="@((long?)wh.Id)">@wh.Name</MudSelectItem>
|
||||
<MudSelectItem T="long?" Value="@((long?)wh.Id)">@wh.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
|
||||
@@ -38,9 +38,9 @@
|
||||
Variant="Variant.Outlined"
|
||||
Required="true"
|
||||
RequiredError="نوع خسارت الزامی است">
|
||||
<MudSelectItem Value="StockMovementType.Loss">خسارت عمومی</MudSelectItem>
|
||||
<MudSelectItem Value="StockMovementType.Damaged">آسیبدیده</MudSelectItem>
|
||||
<MudSelectItem Value="StockMovementType.Expired">منقضی شده</MudSelectItem>
|
||||
<MudSelectItem T="StockMovementType" Value="StockMovementType.Loss">خسارت عمومی</MudSelectItem>
|
||||
<MudSelectItem T="StockMovementType" Value="StockMovementType.Damaged">آسیبدیده</MudSelectItem>
|
||||
<MudSelectItem T="StockMovementType" Value="StockMovementType.Expired">منقضی شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
|
||||
@if (_quantity > 0 && _quantity <= Item.Quantity)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.Inventory
|
||||
@using BackOffice.Pages.Inventory.Components
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<div>
|
||||
<MudText Typo="Typo.h4" Class="mb-4">مدیریت انبار</MudText>
|
||||
|
||||
@* Filters Section *@
|
||||
@@ -18,7 +18,7 @@
|
||||
Clearable="true">
|
||||
@foreach (var wh in _warehouses)
|
||||
{
|
||||
<MudSelectItem Value="@((long?)wh.Id)">@wh.Name</MudSelectItem>
|
||||
<MudSelectItem T="long?" Value="@((long?)wh.Id)">@wh.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
@@ -27,9 +27,9 @@
|
||||
Label="نوع محصول"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense">
|
||||
<MudSelectItem Value="ProductType.Unspecified">همه</MudSelectItem>
|
||||
<MudSelectItem Value="ProductType.RegularProduct">محصول عادی</MudSelectItem>
|
||||
<MudSelectItem Value="ProductType.DiscountProduct">محصول تخفیفی</MudSelectItem>
|
||||
<MudSelectItem T="ProductType" Value="ProductType.Unspecified">همه</MudSelectItem>
|
||||
<MudSelectItem T="ProductType" Value="ProductType.RegularProduct">محصول عادی</MudSelectItem>
|
||||
<MudSelectItem T="ProductType" Value="ProductType.DiscountProduct">محصول تخفیفی</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
@@ -94,11 +94,17 @@
|
||||
ServerData="@(new Func<GridState<InventoryItemDto>, Task<GridData<InventoryItemDto>>>(ServerReload))"
|
||||
Hover="true"
|
||||
@ref="_gridData"
|
||||
Height="60vh">
|
||||
Height="calc(100vh - 240px)">
|
||||
<ToolBarContent>
|
||||
<MudText Typo="Typo.h6">لیست موجودی انبار</MudText>
|
||||
<MudSpacer />
|
||||
<MudStack Row="true" Spacing="2">
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Info"
|
||||
OnClick="ExportToExcel"
|
||||
StartIcon="@Icons.Material.Filled.FileDownload">
|
||||
خروجی Excel
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
OnClick="OpenAddStockDialog"
|
||||
@@ -206,11 +212,14 @@
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="InventoryItemDto"
|
||||
PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد سطرهای صفحه" />
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudContainer>
|
||||
</div>
|
||||
|
||||
@@ -4,12 +4,15 @@ using Google.Protobuf.WellKnownTypes;
|
||||
using CMSMicroservice.Protobuf.Protos.Inventory;
|
||||
using BackOffice.Pages.Inventory.Components;
|
||||
using BackOffice.Common.Utilities;
|
||||
using Microsoft.JSInterop;
|
||||
using System.Text;
|
||||
|
||||
namespace BackOffice.Pages.Inventory;
|
||||
|
||||
public partial class InventoryMainPage
|
||||
{
|
||||
[Inject] public InventoryContract.InventoryContractClient InventoryContract { get; set; } = default!;
|
||||
[Inject] public IJSRuntime JsRuntime { get; set; } = default!;
|
||||
|
||||
private MudDataGrid<InventoryItemDto>? _gridData;
|
||||
private List<WarehouseDto> _warehouses = new();
|
||||
@@ -248,4 +251,66 @@ public partial class InventoryMainPage
|
||||
{
|
||||
Navigation.NavigateTo($"{RouteConstance.InventoryMovements}?itemId={item.Id}&productName={item.ProductTitle}");
|
||||
}
|
||||
|
||||
private async Task ExportToExcel()
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new GetAllInventoryItemsRequest
|
||||
{
|
||||
Page = 1,
|
||||
PageSize = 1000,
|
||||
ProductType = _filterProductType,
|
||||
Search = _searchTerm ?? string.Empty
|
||||
};
|
||||
|
||||
if (_filterWarehouseId.HasValue)
|
||||
{
|
||||
request.WarehouseId = _filterWarehouseId.Value;
|
||||
}
|
||||
|
||||
var result = await InventoryContract.GetAllInventoryItemsAsync(request);
|
||||
|
||||
if (result?.Items == null || !result.Items.Any())
|
||||
{
|
||||
Snackbar.Add("دادهای برای خروجی وجود ندارد.", Severity.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("شناسه,محصول,انبار,موجودی,رزرو,قابل فروش,حد هشدار,وضعیت");
|
||||
|
||||
foreach (var i in result.Items)
|
||||
{
|
||||
sb.AppendLine(string.Join(",",
|
||||
i.Id,
|
||||
EscapeCsv(i.ProductTitle),
|
||||
EscapeCsv(i.WarehouseName),
|
||||
i.Quantity,
|
||||
i.ReservedQuantity,
|
||||
i.AvailableQuantity,
|
||||
i.LowStockThreshold,
|
||||
i.IsLowStock ? "کمبود" : "عادی"));
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
var filename = $"inventory-{DateTime.Now:yyyyMMddHHmmss}.csv";
|
||||
|
||||
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
|
||||
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private static string EscapeCsv(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return "";
|
||||
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
|
||||
return $"\"{value.Replace("\"", "\"\"")}\"";
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
@using CMSMicroservice.Protobuf.Protos.Inventory
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<div>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-4">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.h4">
|
||||
@@ -33,7 +33,7 @@
|
||||
Clearable="true">
|
||||
@foreach (var wh in _warehouses)
|
||||
{
|
||||
<MudSelectItem Value="@((long?)wh.Id)">@wh.Name</MudSelectItem>
|
||||
<MudSelectItem T="long?" Value="@((long?)wh.Id)">@wh.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
@@ -42,9 +42,9 @@
|
||||
Label="نوع محصول"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense">
|
||||
<MudSelectItem Value="ProductType.Unspecified">همه</MudSelectItem>
|
||||
<MudSelectItem Value="ProductType.RegularProduct">محصول عادی</MudSelectItem>
|
||||
<MudSelectItem Value="ProductType.DiscountProduct">محصول تخفیفی</MudSelectItem>
|
||||
<MudSelectItem T="ProductType" Value="ProductType.Unspecified">همه</MudSelectItem>
|
||||
<MudSelectItem T="ProductType" Value="ProductType.RegularProduct">محصول عادی</MudSelectItem>
|
||||
<MudSelectItem T="ProductType" Value="ProductType.DiscountProduct">محصول تخفیفی</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="4" Class="d-flex align-end">
|
||||
@@ -73,7 +73,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudDataGrid T="InventoryItemDto" Items="_items" Hover="true" Height="60vh">
|
||||
<MudDataGrid T="InventoryItemDto" Items="_items" Hover="true" Height="calc(100vh - 240px)">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
|
||||
@@ -135,12 +135,15 @@
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="InventoryItemDto"
|
||||
PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد سطرهای صفحه" />
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
}
|
||||
</MudContainer>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.Inventory
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<div>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-4">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.h4">تاریخچه تغییرات موجودی</MudText>
|
||||
@@ -29,20 +29,20 @@
|
||||
Label="نوع تغییر"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense">
|
||||
<MudSelectItem Value="StockMovementType.MovementTypeUnspecified">همه</MudSelectItem>
|
||||
<MudSelectItem Value="StockMovementType.InitialStock">موجودی اولیه</MudSelectItem>
|
||||
<MudSelectItem Value="StockMovementType.Restock">ورود کالا</MudSelectItem>
|
||||
<MudSelectItem Value="StockMovementType.Return">برگشت از مشتری</MudSelectItem>
|
||||
<MudSelectItem Value="StockMovementType.Sale">فروش</MudSelectItem>
|
||||
<MudSelectItem Value="StockMovementType.AdjustmentIncrease">افزایش موجودی</MudSelectItem>
|
||||
<MudSelectItem Value="StockMovementType.AdjustmentDecrease">کاهش موجودی</MudSelectItem>
|
||||
<MudSelectItem Value="StockMovementType.Reserved">رزرو</MudSelectItem>
|
||||
<MudSelectItem Value="StockMovementType.Released">آزادسازی رزرو</MudSelectItem>
|
||||
<MudSelectItem Value="StockMovementType.Loss">خسارت</MudSelectItem>
|
||||
<MudSelectItem Value="StockMovementType.Damaged">آسیبدیده</MudSelectItem>
|
||||
<MudSelectItem Value="StockMovementType.Expired">منقضی</MudSelectItem>
|
||||
<MudSelectItem Value="StockMovementType.TransferOut">انتقال به بیرون</MudSelectItem>
|
||||
<MudSelectItem Value="StockMovementType.TransferIn">انتقال به داخل</MudSelectItem>
|
||||
<MudSelectItem T="StockMovementType" Value="StockMovementType.MovementTypeUnspecified">همه</MudSelectItem>
|
||||
<MudSelectItem T="StockMovementType" Value="StockMovementType.InitialStock">موجودی اولیه</MudSelectItem>
|
||||
<MudSelectItem T="StockMovementType" Value="StockMovementType.Restock">ورود کالا</MudSelectItem>
|
||||
<MudSelectItem T="StockMovementType" Value="StockMovementType.Return">برگشت از مشتری</MudSelectItem>
|
||||
<MudSelectItem T="StockMovementType" Value="StockMovementType.Sale">فروش</MudSelectItem>
|
||||
<MudSelectItem T="StockMovementType" Value="StockMovementType.AdjustmentIncrease">افزایش موجودی</MudSelectItem>
|
||||
<MudSelectItem T="StockMovementType" Value="StockMovementType.AdjustmentDecrease">کاهش موجودی</MudSelectItem>
|
||||
<MudSelectItem T="StockMovementType" Value="StockMovementType.Reserved">رزرو</MudSelectItem>
|
||||
<MudSelectItem T="StockMovementType" Value="StockMovementType.Released">آزادسازی رزرو</MudSelectItem>
|
||||
<MudSelectItem T="StockMovementType" Value="StockMovementType.Loss">خسارت</MudSelectItem>
|
||||
<MudSelectItem T="StockMovementType" Value="StockMovementType.Damaged">آسیبدیده</MudSelectItem>
|
||||
<MudSelectItem T="StockMovementType" Value="StockMovementType.Expired">منقضی</MudSelectItem>
|
||||
<MudSelectItem T="StockMovementType" Value="StockMovementType.TransferOut">انتقال به بیرون</MudSelectItem>
|
||||
<MudSelectItem T="StockMovementType" Value="StockMovementType.TransferIn">انتقال به داخل</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
@@ -50,9 +50,9 @@
|
||||
Label="نوع محصول"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense">
|
||||
<MudSelectItem Value="ProductType.Unspecified">همه</MudSelectItem>
|
||||
<MudSelectItem Value="ProductType.RegularProduct">محصول عادی</MudSelectItem>
|
||||
<MudSelectItem Value="ProductType.DiscountProduct">محصول تخفیفی</MudSelectItem>
|
||||
<MudSelectItem T="ProductType" Value="ProductType.Unspecified">همه</MudSelectItem>
|
||||
<MudSelectItem T="ProductType" Value="ProductType.RegularProduct">محصول عادی</MudSelectItem>
|
||||
<MudSelectItem T="ProductType" Value="ProductType.DiscountProduct">محصول تخفیفی</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
@@ -92,13 +92,20 @@
|
||||
ServerData="@(new Func<GridState<StockMovementDto>, Task<GridData<StockMovementDto>>>(ServerReload))"
|
||||
Hover="true"
|
||||
@ref="_gridData"
|
||||
Height="65vh">
|
||||
Height="calc(100vh - 240px)">
|
||||
<ToolBarContent>
|
||||
<MudText Typo="Typo.h6">تغییرات موجودی</MudText>
|
||||
<MudSpacer />
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary" Class="ml-4">
|
||||
کل: @_totalCount ردیف
|
||||
</MudText>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Info"
|
||||
OnClick="ExportToExcel"
|
||||
StartIcon="@Icons.Material.Filled.FileDownload"
|
||||
Size="Size.Small">
|
||||
خروجی Excel
|
||||
</MudButton>
|
||||
</ToolBarContent>
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
@@ -157,7 +164,7 @@
|
||||
@if (context.Item.Created != null)
|
||||
{
|
||||
<MudText Typo="Typo.caption">
|
||||
@context.Item.Created.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm")
|
||||
@context.Item.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()
|
||||
</MudText>
|
||||
}
|
||||
</CellTemplate>
|
||||
@@ -176,11 +183,14 @@
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="StockMovementDto"
|
||||
PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد سطرهای صفحه" />
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudContainer>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using MudBlazor;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using CMSMicroservice.Protobuf.Protos.Inventory;
|
||||
using BackOffice.Common.Utilities;
|
||||
using System.Text;
|
||||
using DateTimeConverterCL;
|
||||
|
||||
namespace BackOffice.Pages.Inventory;
|
||||
|
||||
public partial class MovementsPage
|
||||
{
|
||||
[Inject] public InventoryContract.InventoryContractClient InventoryContract { get; set; } = default!;
|
||||
[Inject] public IJSRuntime JsRuntime { get; set; } = default!;
|
||||
|
||||
[SupplyParameterFromQuery(Name = "itemId")]
|
||||
public long? ItemId { get; set; }
|
||||
@@ -150,4 +154,66 @@ public partial class MovementsPage
|
||||
{
|
||||
Navigation.NavigateTo(RouteConstance.Inventory);
|
||||
}
|
||||
|
||||
private async Task ExportToExcel()
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new GetStockMovementsRequest
|
||||
{
|
||||
Page = 1,
|
||||
PageSize = 1000,
|
||||
MovementType = _filterMovementType,
|
||||
ProductType = _filterProductType
|
||||
};
|
||||
|
||||
if (ItemId.HasValue) request.InventoryItemId = ItemId.Value;
|
||||
if (_filterFromDate.HasValue) request.FromDate = Timestamp.FromDateTime(_filterFromDate.Value.ToUniversalTime());
|
||||
if (_filterToDate.HasValue) request.ToDate = Timestamp.FromDateTime(_filterToDate.Value.ToUniversalTime());
|
||||
|
||||
var result = await InventoryContract.GetStockMovementsAsync(request);
|
||||
|
||||
if (result?.Movements == null || !result.Movements.Any())
|
||||
{
|
||||
Snackbar.Add("دادهای برای خروجی وجود ندارد.", Severity.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("شناسه,محصول,نوع تغییر,تعداد,قبل,بعد,شماره مرجع,یادداشت,تاریخ");
|
||||
|
||||
foreach (var m in result.Movements)
|
||||
{
|
||||
sb.AppendLine(string.Join(",",
|
||||
m.Id,
|
||||
EscapeCsv(m.ProductTitle),
|
||||
EscapeCsv(GetMovementTypeLabel(m.MovementType)),
|
||||
m.Quantity,
|
||||
m.QuantityBefore,
|
||||
m.QuantityAfter,
|
||||
EscapeCsv(m.ReferenceNumber),
|
||||
EscapeCsv(m.Note),
|
||||
m.Created?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-"));
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
var filename = $"stock-movements-{DateTime.Now:yyyyMMddHHmmss}.csv";
|
||||
|
||||
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
|
||||
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private static string EscapeCsv(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return "";
|
||||
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
|
||||
return $"\"{value.Replace("\"", "\"\"")}\"";
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.Inventory
|
||||
@using BackOffice.Pages.Inventory.Components
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<div>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-4">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.h4">
|
||||
@@ -92,7 +92,7 @@
|
||||
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.CalendarToday" Size="Size.Small" Color="Color.Secondary" />
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||
ایجاد: @(warehouse.Created?.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd") ?? "-")
|
||||
ایجاد: @(warehouse.Created?.ToDateTime().ToLocalTime().MiladiToJalali() ?? "-")
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
@@ -120,4 +120,4 @@
|
||||
}
|
||||
</MudGrid>
|
||||
}
|
||||
</MudContainer>
|
||||
</div>
|
||||
|
||||
@@ -4,18 +4,30 @@
|
||||
@attribute [AllowAnonymous]
|
||||
|
||||
|
||||
<MudStack Style="width:100%;height:80vh;" AlignItems="AlignItems.Center" Justify="Justify.Center">
|
||||
<MudPaper Elevation="4" Width="35%" Height="200px" Class="mx-auto my-auto pa-4">
|
||||
<MudForm @ref="@_form" Model="@_request" Validation="@(_requestValidator.ValidateValue)">
|
||||
|
||||
<MudStack>
|
||||
<MudTextField T="string" Variant="Variant.Outlined" Label="شماره موبایل" @bind-Value="@_request.Mobile" For="() => _request.Mobile" HelperText="لطفا شماره موبایل خود را وارد کنید"></MudTextField>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="_isLoading" OnClick="OnSubmitClick" ButtonType="ButtonType.Button" Style="cursor:pointer;">ارسال کد</MudButton>
|
||||
<MudRTLProvider RightToLeft="true">
|
||||
<MudThemeProvider Theme="CustomMudTheme.CustomMudBlazorTheme" />
|
||||
<MudStack Style="width:100%;height:100vh;background:var(--mud-palette-background);" AlignItems="AlignItems.Center" Justify="Justify.Center">
|
||||
<MudPaper Elevation="4" Class="pa-8" Style="width:100%;max-width:420px;border-radius:16px;">
|
||||
<MudStack Spacing="4" AlignItems="AlignItems.Center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.AdminPanelSettings" Color="Color.Primary" Size="Size.Large" />
|
||||
<MudText Typo="Typo.h5" Align="Align.Center">پنل مدیریت</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary" Align="Align.Center">کارا بازار سلامت</MudText>
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
|
||||
</MudPaper>
|
||||
</MudStack>
|
||||
<MudForm @ref="@_form" Model="@_request" Validation="@(_requestValidator.ValidateValue)" Class="mt-6">
|
||||
<MudStack Spacing="3">
|
||||
<MudTextField T="string" Variant="Variant.Outlined" Label="شماره موبایل" @bind-Value="@_request.Mobile" For="() => _request.Mobile" HelperText="لطفا شماره موبایل خود را وارد کنید" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Phone" />
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="_isLoading" OnClick="OnSubmitClick" ButtonType="ButtonType.Button" FullWidth="true" Size="Size.Large">
|
||||
@if (_isLoading)
|
||||
{
|
||||
<MudProgressCircular Color="Color.Inherit" Size="Size.Small" Indeterminate="true" Class="me-2" />
|
||||
}
|
||||
ارسال کد
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
</MudPaper>
|
||||
</MudStack>
|
||||
</MudRTLProvider>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,17 +4,32 @@
|
||||
@attribute [AllowAnonymous]
|
||||
|
||||
|
||||
<MudStack Style="width:100%;height:100vh;" AlignItems="AlignItems.Center" Justify="Justify.Center">
|
||||
<MudPaper Elevation="4" Width="35%" Height="230px" Class="mx-auto my-auto pa-4">
|
||||
<MudForm @ref="@_form" Model="@_request" Validation="@(_requestValidator.ValidateValue)">
|
||||
<MudStack Spacing="5">
|
||||
<MudTextField T="string" @bind-Value="@_request.Code" For="() => _request.Code" Variant="Variant.Outlined" Label="رمز پویا" HelperText="لطفا کد شش رقمی دریافتی را وارد کنید" InputType="InputType.Telephone" />
|
||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" Disabled="_isLoading" OnClick="OnSubmitClick">ورود به حساب کاربری</MudButton>
|
||||
<MudButton Color="Color.Primary" Variant="Variant.Text" Disabled="@(_currentCount > 1 || _isLoading)" OnClick="OnResendOtpClick">@(_currentCount < 1 ? "ارسال مجدد رمز پویا" : $"ارسال مجدد رمز پویا {_currentCount}")</MudButton>
|
||||
<MudRTLProvider RightToLeft="true">
|
||||
<MudThemeProvider Theme="CustomMudTheme.CustomMudBlazorTheme" />
|
||||
<MudStack Style="width:100%;height:100vh;background:var(--mud-palette-background);" AlignItems="AlignItems.Center" Justify="Justify.Center">
|
||||
<MudPaper Elevation="4" Class="pa-8" Style="width:100%;max-width:420px;border-radius:16px;">
|
||||
<MudStack Spacing="4" AlignItems="AlignItems.Center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.LockOpen" Color="Color.Primary" Size="Size.Large" />
|
||||
<MudText Typo="Typo.h5" Align="Align.Center">تأیید ورود</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary" Align="Align.Center">کد ارسالشده به @Mobile را وارد کنید</MudText>
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
|
||||
</MudPaper>
|
||||
</MudStack>
|
||||
<MudForm @ref="@_form" Model="@_request" Validation="@(_requestValidator.ValidateValue)" Class="mt-6">
|
||||
<MudStack Spacing="3">
|
||||
<MudTextField T="string" @bind-Value="@_request.Code" For="() => _request.Code" Variant="Variant.Outlined" Label="رمز پویا" HelperText="لطفا کد شش رقمی دریافتی را وارد کنید" InputType="InputType.Telephone" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Pin" />
|
||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" Disabled="_isLoading" OnClick="OnSubmitClick" FullWidth="true" Size="Size.Large">
|
||||
@if (_isLoading)
|
||||
{
|
||||
<MudProgressCircular Color="Color.Inherit" Size="Size.Small" Indeterminate="true" Class="me-2" />
|
||||
}
|
||||
ورود به حساب کاربری
|
||||
</MudButton>
|
||||
<MudButton Color="Color.Primary" Variant="Variant.Text" Disabled="@(_currentCount > 1 || _isLoading)" OnClick="OnResendOtpClick" FullWidth="true">
|
||||
@(_currentCount < 1 ? "ارسال مجدد رمز پویا" : $"ارسال مجدد رمز پویا {_currentCount}")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
</MudPaper>
|
||||
</MudStack>
|
||||
</MudRTLProvider>
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
@using MudBlazor
|
||||
@using CMSMicroservice.Protobuf.Protos.Commission
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<div>
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">گزارش موجودیهای هفتگی</MudText>
|
||||
<MudText Typo="Typo.body1" Color="Color.Secondary" Class="mb-4">
|
||||
مشاهده موجودیهای شبکه کاربران به تفکیک هفته
|
||||
@@ -151,6 +151,9 @@
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="UserWeeklyBalanceModel"
|
||||
PageSizeOptions="new int[] { 10, 25, 50, 100 }" />
|
||||
@@ -183,7 +186,7 @@
|
||||
</MudGrid>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudContainer>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Inject] public CommissionContract.CommissionContractClient CommissionClient { get; set; }
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.NetworkMembership
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-4">
|
||||
<div>
|
||||
<MudText Typo="Typo.h4" Class="mb-4">درخت شبکه</MudText>
|
||||
|
||||
@* Search & Filter Panel *@
|
||||
@@ -28,11 +28,11 @@
|
||||
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center">
|
||||
<MudSelect T="int" @bind-Value="_selectedDepth" Label="عمق نمایش" Variant="Variant.Outlined"
|
||||
Dense="true" Margin="Margin.Dense" Style="width: 120px;">
|
||||
<MudSelectItem Value="3">3 سطح</MudSelectItem>
|
||||
<MudSelectItem Value="5">5 سطح</MudSelectItem>
|
||||
<MudSelectItem Value="10">10 سطح</MudSelectItem>
|
||||
<MudSelectItem Value="15">15 سطح</MudSelectItem>
|
||||
<MudSelectItem Value="100">همه</MudSelectItem>
|
||||
<MudSelectItem T="int" Value="3">3 سطح</MudSelectItem>
|
||||
<MudSelectItem T="int" Value="5">5 سطح</MudSelectItem>
|
||||
<MudSelectItem T="int" Value="10">10 سطح</MudSelectItem>
|
||||
<MudSelectItem T="int" Value="15">15 سطح</MudSelectItem>
|
||||
<MudSelectItem T="int" Value="100">همه</MudSelectItem>
|
||||
</MudSelect>
|
||||
|
||||
<MudSelect T="bool?" Label="وضعیت باشگاه" @bind-Value="_clubActiveFilter" Variant="Variant.Outlined"
|
||||
@@ -145,7 +145,7 @@
|
||||
<CellTemplate>
|
||||
@if (context.Item.ClubActivatedAt != null)
|
||||
{
|
||||
@context.Item.ClubActivatedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd")
|
||||
@context.Item.ClubActivatedAt.ToDateTime().ToLocalTime().MiladiToJalali()
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -184,8 +184,13 @@
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="NetworkTreeNodeModel" />
|
||||
<MudDataGridPager T="NetworkTreeNodeModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudPaper>
|
||||
@@ -198,7 +203,7 @@
|
||||
</MudAlert>
|
||||
</MudPaper>
|
||||
}
|
||||
</MudContainer>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Inject] public NetworkMembershipContract.NetworkMembershipContractClient NetworkContract { get; set; }
|
||||
@@ -332,7 +337,7 @@
|
||||
isClubActive = n.IsClubActive,
|
||||
isActivatedInTargetWeek = n.IsActivatedInTargetWeek,
|
||||
activationWeekDefinitionId = n.ActivationWeekDefinitionId,
|
||||
clubActivatedAt = n.ClubActivatedAt?.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd") ?? ""
|
||||
clubActivatedAt = n.ClubActivatedAt?.ToDateTime().ToLocalTime().MiladiToJalali() ?? ""
|
||||
}).ToArray();
|
||||
|
||||
var options = new { filterWeek = _activationWeekFilter };
|
||||
@@ -341,7 +346,6 @@
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error rendering chart: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.NetworkMembership
|
||||
@using MudBlazor
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<div>
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">آمار شبکه</MudText>
|
||||
<MudText Typo="Typo.body1" Color="Color.Secondary" Class="mb-4">
|
||||
نمای کلی از وضعیت شبکه و آمار اعضا
|
||||
@@ -162,7 +162,7 @@
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
}
|
||||
</MudContainer>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Inject] public NetworkMembershipContract.NetworkMembershipContractClient NetworkClient { get; set; }
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
@attribute [Authorize]
|
||||
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<div>
|
||||
<MudBreadcrumbs Items="_breadcrumbs" Class="mb-4"></MudBreadcrumbs>
|
||||
|
||||
@if (_isLoading)
|
||||
@@ -346,6 +346,72 @@
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
|
||||
<!-- تاریخچه تغییرات عضویت -->
|
||||
<MudItem xs="12">
|
||||
<MudCard>
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
<MudText Typo="Typo.h6">
|
||||
<MudIcon Icon="@Icons.Material.Filled.History" Class="ml-1" />
|
||||
تاریخچه تغییرات شبکه
|
||||
</MudText>
|
||||
</CardHeaderContent>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
@if (_networkHistoryLoading)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" Color="Color.Primary" />
|
||||
}
|
||||
else if (_networkHistory.Any())
|
||||
{
|
||||
<MudTable T="NetworkMembershipHistoryModel" Items="_networkHistory" Dense="true" Hover="true">
|
||||
<HeaderContent>
|
||||
<MudTh>عملیات</MudTh>
|
||||
<MudTh>تاریخ</MudTh>
|
||||
<MudTh>والد قبلی → جدید</MudTh>
|
||||
<MudTh>ساق قبلی → جدید</MudTh>
|
||||
<MudTh>دلیل</MudTh>
|
||||
<MudTh>توسط</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="عملیات">
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Info">@context.Action</MudChip>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="تاریخ">@context.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</MudTd>
|
||||
<MudTd DataLabel="والد">
|
||||
@if (context.OldParentId > 0 || context.NewParentId > 0)
|
||||
{
|
||||
<span>@context.OldParentId → @context.NewParentId</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>-</span>
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd DataLabel="ساق">
|
||||
@{ var oldLeg = context.OldNetworkLeg == 0 ? "چپ" : "راست"; var newLeg = context.NewNetworkLeg == 0 ? "چپ" : "راست"; }
|
||||
@if (context.OldNetworkLeg != context.NewNetworkLeg)
|
||||
{
|
||||
<span>@oldLeg → @newLeg</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>-</span>
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd DataLabel="دلیل">@(string.IsNullOrEmpty(context.Reason) ? "-" : context.Reason)</MudTd>
|
||||
<MudTd DataLabel="توسط">@(string.IsNullOrEmpty(context.PerformedBy) ? "-" : context.PerformedBy)</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text">تاریخچهای یافت نشد.</MudAlert>
|
||||
}
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
|
||||
<!-- عملیات -->
|
||||
<MudItem xs="12">
|
||||
<MudCard>
|
||||
@@ -386,7 +452,7 @@
|
||||
کاربر یافت نشد یا خطایی رخ داده است.
|
||||
</MudAlert>
|
||||
}
|
||||
</MudContainer>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public long UserId { get; set; }
|
||||
@@ -397,10 +463,13 @@
|
||||
private GetUserNetworkResponse _userInfo;
|
||||
private bool _isLoading = true;
|
||||
private List<BreadcrumbItem> _breadcrumbs = new();
|
||||
private List<NetworkMembershipHistoryModel> _networkHistory = new();
|
||||
private bool _networkHistoryLoading;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
await LoadUserInfo();
|
||||
await LoadNetworkHistory();
|
||||
}
|
||||
|
||||
private async Task LoadUserInfo()
|
||||
@@ -433,6 +502,30 @@
|
||||
NavigationManager.NavigateTo($"/network/user-info/{parentIdValue.Value}");
|
||||
}
|
||||
|
||||
private async Task LoadNetworkHistory()
|
||||
{
|
||||
_networkHistoryLoading = true;
|
||||
try
|
||||
{
|
||||
var request = new GetNetworkMembershipHistoryRequest
|
||||
{
|
||||
UserId = UserId,
|
||||
PageIndex = 0,
|
||||
PageSize = 50
|
||||
};
|
||||
var response = await NetworkContract.GetNetworkMembershipHistoryAsync(request);
|
||||
_networkHistory = response.Models?.ToList() ?? new();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_networkHistory = new();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_networkHistoryLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetPackagePurchaseMethodText(int method)
|
||||
{
|
||||
return method switch
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
Color="Color.Primary"
|
||||
ButtonType="ButtonType.Button"
|
||||
StartIcon="@Icons.Material.Filled.Image"
|
||||
Style="cursor:pointer;">
|
||||
>
|
||||
انتخاب تصویر
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
@@ -45,7 +45,7 @@
|
||||
</MudItem>
|
||||
</MudStack>
|
||||
<MudItem xs="12">
|
||||
<MudHtmlEditor @bind-Html="Model.Description">
|
||||
<MudHtmlEditor @bind-Html="Model.Description" MinHeight="300px">
|
||||
<MudHtmlToolbarOptions InsertImage="false" /> <!-- This will exclude the "insert image" toolbar option -->
|
||||
</MudHtmlEditor>
|
||||
</MudItem>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Image"
|
||||
ButtonType="ButtonType.Button"
|
||||
Style="cursor:pointer;">
|
||||
>
|
||||
انتخاب تصویر
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
@@ -46,7 +46,7 @@
|
||||
</MudItem>
|
||||
</MudStack>
|
||||
<MudItem xs="12">
|
||||
<MudHtmlEditor @bind-Html="Model.Description">
|
||||
<MudHtmlEditor @bind-Html="Model.Description" MinHeight="300px">
|
||||
<MudHtmlToolbarOptions InsertImage="false" /> <!-- This will exclude the "insert image" toolbar option -->
|
||||
</MudHtmlEditor>
|
||||
</MudItem>
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
|
||||
<BackOffice.Common.BaseComponents.BasePageComponent @ref="_basePage" OnClearFilterClick="OnFilterCleared" OnSubmitClick="OnFilterSubmit">
|
||||
<Filters>
|
||||
<MudTextField T="string" Clearable="true" Label="عنوان" @bind-Value="@_request.Filter.Title" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField T="string" Clearable="true" Label="عنوان" @bind-Value="@_request.Filter.Title" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
</MudItem>
|
||||
</Filters>
|
||||
<Content>
|
||||
<MudDataGrid T="DataModel" ServerData="@(new Func<GridState<DataModel>, Task<GridData<DataModel>>>(ServerReload))"
|
||||
Hover="true" @ref="_gridData" Height="72vh">
|
||||
Hover="true" @ref="_gridData" Height="calc(100vh - 240px)">
|
||||
<ColGroup>
|
||||
<col />
|
||||
<col />
|
||||
@@ -22,7 +24,7 @@
|
||||
<MudText>مدیریت پکیج</MudText>
|
||||
<MudSpacer />
|
||||
<MudStack Spacing="2" Row="true" Justify="Justify.Center" AlignItems="AlignItems.Center">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Size="Size.Large" ButtonType="ButtonType.Button" OnClick="CreateNew" Style="cursor:pointer;">افزودن</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Size="Size.Large" ButtonType="ButtonType.Button" OnClick="CreateNew">افزودن</MudButton>
|
||||
</MudStack>
|
||||
</ToolBarContent>
|
||||
<Columns>
|
||||
@@ -71,18 +73,21 @@
|
||||
<CellTemplate>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
<MudTooltip Text="ویرایش">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.EditNote" Size="Size.Small" ButtonType="ButtonType.Button" OnClick="@(() => Update(context.Item.Adapt<DataModel>()))" Style="cursor:pointer;" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.EditNote" Size="Size.Small" ButtonType="ButtonType.Button" OnClick="@(() => Update(context.Item.Adapt<DataModel>()))" />
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="آرشیو">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.DeleteOutline" Size="Size.Small" ButtonType="ButtonType.Button" OnClick="@(() => OnDelete(context.Item))" Style="cursor:pointer;" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.DeleteOutline" Size="Size.Small" ButtonType="ButtonType.Button" OnClick="@(() => OnDelete(context.Item))" />
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="DataModel" PageSizeOptions=@(new int[] { 30, 60, 90 }) InfoFormat="سطر {first_item} تا {last_item} از {all_items}" RowsPerPageString="تعداد سطرهای صفحه" />
|
||||
<MudDataGridPager T="DataModel" PageSizeOptions=@(new int[] { 20, 50, 100 }) InfoFormat="سطر {first_item} تا {last_item} از {all_items}" RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</Content>
|
||||
|
||||
@@ -49,7 +49,7 @@ public partial class PackageMainPage
|
||||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small };
|
||||
bool? result = await DialogService.ShowMessageBox(
|
||||
"اخطار",
|
||||
"آیا از حذف این مورد مطمئن هستید؟",
|
||||
$"آیا از حذف پکیج «{model.Title}» مطمئن هستید؟",
|
||||
yesText: "حذف", cancelText: "لغو",
|
||||
options: options);
|
||||
if (result != null && result.Value)
|
||||
|
||||
@@ -25,13 +25,13 @@
|
||||
Label="نوع پرداخت"
|
||||
Variant="Variant.Outlined"
|
||||
@bind-Value="_selectedType">
|
||||
<MudSelectItem Value="1">واریز نقدی</MudSelectItem>
|
||||
<MudSelectItem Value="2">شارژ کیف پول تخفیف</MudSelectItem>
|
||||
<MudSelectItem Value="3">شارژ کیف پول شبکه</MudSelectItem>
|
||||
<MudSelectItem Value="4">تسویه حساب</MudSelectItem>
|
||||
<MudSelectItem Value="5">اصلاح خطا</MudSelectItem>
|
||||
<MudSelectItem Value="6">بازپرداخت</MudSelectItem>
|
||||
<MudSelectItem Value="99">سایر</MudSelectItem>
|
||||
<MudSelectItem T="int" Value="1">واریز نقدی</MudSelectItem>
|
||||
<MudSelectItem T="int" Value="2">شارژ کیف پول تخفیف</MudSelectItem>
|
||||
<MudSelectItem T="int" Value="3">شارژ کیف پول شبکه</MudSelectItem>
|
||||
<MudSelectItem T="int" Value="4">تسویه حساب</MudSelectItem>
|
||||
<MudSelectItem T="int" Value="5">اصلاح خطا</MudSelectItem>
|
||||
<MudSelectItem T="int" Value="6">بازپرداخت</MudSelectItem>
|
||||
<MudSelectItem T="int" Value="99">سایر</MudSelectItem>
|
||||
</MudSelect>
|
||||
|
||||
<MudTextField T="string"
|
||||
|
||||
@@ -6,58 +6,52 @@
|
||||
|
||||
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
|
||||
<Filters>
|
||||
<UserAutoComplete Label="جستجوی کاربر"
|
||||
@bind-SelectedUserId="_userIdFilter" />
|
||||
|
||||
<MudTextField T="string"
|
||||
Label="شماره مرجع"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
@bind-Value="_referenceFilter" />
|
||||
|
||||
<MudSelect T="int?"
|
||||
Clearable="true"
|
||||
Label="وضعیت"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
@bind-Value="_statusFilter">
|
||||
<MudSelectItem T="int?" Value="@(0)">در انتظار</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(1)">تایید شده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(2)">رد شده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(3)">لغو شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
|
||||
<MudSelect T="int?"
|
||||
Clearable="true"
|
||||
Label="نوع"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
@bind-Value="_typeFilter">
|
||||
<MudSelectItem T="int?" Value="@(1)">واریز نقدی</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(2)">شارژ کیفپول تخفیف</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(3)">شارژ کیفپول شبکه</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(4)">تسویه حساب</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(5)">اصلاح خطا</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(6)">بازگشت وجه</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(99)">سایر</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<UserAutoComplete Label="جستجوی کاربر" @bind-SelectedUserId="_userIdFilter" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField T="string" Label="شماره مرجع" Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" @bind-Value="_referenceFilter" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect T="int?" Clearable="true" Label="وضعیت" Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" @bind-Value="_statusFilter">
|
||||
<MudSelectItem T="int?" Value="@(0)">در انتظار</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(1)">تایید شده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(2)">رد شده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(3)">لغو شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect T="int?" Clearable="true" Label="نوع" Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" @bind-Value="_typeFilter">
|
||||
<MudSelectItem T="int?" Value="@(1)">واریز نقدی</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(2)">شارژ کیفپول تخفیف</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(3)">شارژ کیفپول شبکه</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(4)">تسویه حساب</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(5)">اصلاح خطا</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(6)">بازگشت وجه</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(99)">سایر</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
</Filters>
|
||||
<Content>
|
||||
<MudPaper Class="pa-2 mb-2">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.subtitle1">فعالسازی (پرداخت) دستی</MudText>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Primary"
|
||||
OnClick="OpenCreateDialog">
|
||||
ثبت فعالسازی دستی جدید
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<MudDataGrid @ref="_dataGrid" T="ManualPaymentModel"
|
||||
ServerData="LoadData"
|
||||
Hover="true"
|
||||
Dense="true">
|
||||
Hover="true" Dense="true"
|
||||
Height="calc(100vh - 240px)">
|
||||
<ToolBarContent>
|
||||
<MudText Typo="Typo.subtitle1">فعالسازی (پرداخت) دستی</MudText>
|
||||
<MudSpacer />
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Info" Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.FileDownload" OnClick="ExportToExcel" Class="mr-2">
|
||||
خروجی Excel
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.Add" OnClick="OpenCreateDialog">
|
||||
ثبت جدید
|
||||
</MudButton>
|
||||
</ToolBarContent>
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
<PropertyColumn Property="x => x.UserId" Title="شناسه کاربر" />
|
||||
@@ -94,10 +88,31 @@
|
||||
Size="Size.Small"
|
||||
OnClick="@(() => OpenDetails(context.Item))" />
|
||||
</MudTooltip>
|
||||
@if ((int)context.Item.Status == 0)
|
||||
{
|
||||
<MudTooltip Text="تایید">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.CheckCircle"
|
||||
Size="Size.Small" Color="Color.Success"
|
||||
OnClick="@(() => ApprovePayment(context.Item))" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="رد">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Cancel"
|
||||
Size="Size.Small" Color="Color.Error"
|
||||
OnClick="@(() => RejectPayment(context.Item))" />
|
||||
</MudTooltip>
|
||||
}
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="ManualPaymentModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</Content>
|
||||
</BasePageComponent>
|
||||
@@ -237,4 +252,46 @@
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private async Task ApprovePayment(ManualPaymentModel model)
|
||||
{
|
||||
bool? confirm = await DialogService.ShowMessageBox(
|
||||
"تایید پرداخت",
|
||||
$"آیا از تایید پرداخت دستی #{model.Id} به مبلغ {model.Amount:N0} ریال مطمئن هستید؟",
|
||||
yesText: "تایید", cancelText: "لغو");
|
||||
if (confirm == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ManualPaymentClient.ApproveManualPaymentAsync(new ApproveManualPaymentRequest { ManualPaymentId = model.Id });
|
||||
Snackbar.Add("پرداخت با موفقیت تایید شد", Severity.Success);
|
||||
if (_dataGrid != null) await _dataGrid.ReloadServerData();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RejectPayment(ManualPaymentModel model)
|
||||
{
|
||||
bool? confirm = await DialogService.ShowMessageBox(
|
||||
"رد پرداخت",
|
||||
$"آیا از رد پرداخت دستی #{model.Id} مطمئن هستید؟",
|
||||
yesText: "رد کردن", cancelText: "لغو");
|
||||
if (confirm == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ManualPaymentClient.RejectManualPaymentAsync(new RejectManualPaymentRequest { ManualPaymentId = model.Id });
|
||||
Snackbar.Add("پرداخت رد شد", Severity.Warning);
|
||||
if (_dataGrid != null) await _dataGrid.ReloadServerData();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using MudBlazor;
|
||||
using System.Text;
|
||||
using DateTimeConverterCL;
|
||||
using CMSMicroservice.Protobuf.Protos.ManualPayment;
|
||||
|
||||
namespace BackOffice.Pages.Payment;
|
||||
|
||||
public partial class ManualPayments
|
||||
{
|
||||
[Inject] public IJSRuntime JsRuntime { get; set; } = default!;
|
||||
|
||||
private async Task ExportToExcel()
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new GetAllManualPaymentsRequest
|
||||
{
|
||||
PageNumber = 1,
|
||||
PageSize = 1000
|
||||
};
|
||||
|
||||
if (_userIdFilter.HasValue)
|
||||
request.UserId = _userIdFilter.Value;
|
||||
if (_statusFilter.HasValue)
|
||||
request.Status = _statusFilter.Value;
|
||||
if (_typeFilter.HasValue)
|
||||
request.Type = _typeFilter.Value;
|
||||
|
||||
var response = await ManualPaymentClient.GetAllManualPaymentsAsync(request);
|
||||
|
||||
if (response?.Models == null || !response.Models.Any())
|
||||
{
|
||||
Snackbar.Add("دادهای برای خروجی وجود ندارد.", Severity.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("شناسه,شناسه کاربر,نام کاربر,مبلغ,نوع,وضعیت,تاریخ ایجاد");
|
||||
|
||||
foreach (var m in response.Models)
|
||||
{
|
||||
sb.AppendLine(string.Join(",",
|
||||
m.Id,
|
||||
m.UserId,
|
||||
EscapeCsv(m.UserFullName),
|
||||
m.Amount.ToString("N0"),
|
||||
EscapeCsv(GetTypeText((int)m.Type)),
|
||||
EscapeCsv(GetStatusText((int)m.Status)),
|
||||
m.Created?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-"));
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
var filename = $"manual-payments-{DateTime.Now:yyyyMMddHHmmss}.csv";
|
||||
|
||||
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
|
||||
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private static string EscapeCsv(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return "";
|
||||
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
|
||||
return $"\"{value.Replace("\"", "\"\"")}\"";
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -3,42 +3,34 @@
|
||||
|
||||
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
|
||||
<Filters>
|
||||
<BackOffice.Common.BaseComponents.DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه تاریخی" />
|
||||
|
||||
<MudSelect T="int?"
|
||||
Clearable="true"
|
||||
Label="وضعیت پرداخت"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
@bind-Value="_statusFilter">
|
||||
<MudSelectItem T="int?" Value="@(0)">در انتظار</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(1)">موفق</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(2)">ناموفق</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(3)">بازگشت وجه</MudSelectItem>
|
||||
</MudSelect>
|
||||
|
||||
<MudSelect T="int?"
|
||||
Clearable="true"
|
||||
Label="نوع تراکنش"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
@bind-Value="_typeFilter">
|
||||
<MudSelectItem T="int?" Value="@(0)">نامشخص</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(1)">خرید پکیج</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(2)">پرداخت دستی</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(3)">شارژ کیفپول</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(4)">بازگشت وجه</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<BackOffice.Common.BaseComponents.DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه تاریخی" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudSelect T="int?" Clearable="true" Label="وضعیت پرداخت" Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" @bind-Value="_statusFilter">
|
||||
<MudSelectItem T="int?" Value="@(0)">در انتظار</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(1)">موفق</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(2)">ناموفق</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(3)">بازگشت وجه</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudSelect T="int?" Clearable="true" Label="نوع تراکنش" Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" @bind-Value="_typeFilter">
|
||||
<MudSelectItem T="int?" Value="@(0)">نامشخص</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(1)">خرید پکیج</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(2)">پرداخت دستی</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(3)">شارژ کیفپول</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(4)">بازگشت وجه</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
</Filters>
|
||||
<Content>
|
||||
<MudPaper Class="pa-2 mb-2">
|
||||
<MudText Typo="Typo.subtitle1">مدیریت تراکنشها</MudText>
|
||||
</MudPaper>
|
||||
|
||||
<MudDataGrid @ref="_dataGrid" T="TransactionModel"
|
||||
ServerData="LoadData"
|
||||
Hover="true"
|
||||
Dense="true">
|
||||
Hover="true" Dense="true"
|
||||
Height="calc(100vh - 240px)">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
<PropertyColumn Property="x => x.RefId" Title="شماره مرجع" />
|
||||
@@ -76,6 +68,14 @@
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="TransactionModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</Content>
|
||||
</BasePageComponent>
|
||||
|
||||
@@ -8,36 +8,22 @@
|
||||
OnClearFilterClick="OnFilterCleared"
|
||||
OnSubmitClick="OnFilterSubmit">
|
||||
<Filters>
|
||||
<MudTextField T="string"
|
||||
Clearable="true"
|
||||
Label="عنوان"
|
||||
@bind-Value="@_request.Filter.Title"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
|
||||
<MudNumericField T="long?"
|
||||
HideSpinButtons="true"
|
||||
Clearable="true"
|
||||
Label="شناسه محصول"
|
||||
@bind-Value="@_request.Filter.Id"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
|
||||
<MudNumericField T="long?"
|
||||
HideSpinButtons="true"
|
||||
Clearable="true"
|
||||
Label="شناسه دستهبندی"
|
||||
@bind-Value="@_request.Filter.CategoryId"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
|
||||
<MudNumericField T="long?"
|
||||
HideSpinButtons="true"
|
||||
Clearable="true"
|
||||
Label="قیمت"
|
||||
@bind-Value="@_request.Filter.Price"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField T="string" Clearable="true" Label="عنوان"
|
||||
@bind-Value="@_request.Filter.Title" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudNumericField T="long?" HideSpinButtons="true" Clearable="true" Label="شناسه محصول"
|
||||
@bind-Value="@_request.Filter.Id" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudNumericField T="long?" HideSpinButtons="true" Clearable="true" Label="شناسه دستهبندی"
|
||||
@bind-Value="@_request.Filter.CategoryId" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudNumericField T="long?" HideSpinButtons="true" Clearable="true" Label="قیمت"
|
||||
@bind-Value="@_request.Filter.Price" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
</MudItem>
|
||||
</Filters>
|
||||
<Content>
|
||||
<MudGrid Spacing="3">
|
||||
@@ -60,7 +46,7 @@
|
||||
ServerData="@(new Func<GridState<DataModel>, Task<GridData<DataModel>>>(ServerReload))"
|
||||
Hover="true"
|
||||
@ref="_gridData"
|
||||
Height="72vh">
|
||||
Height="calc(100vh - 340px)">
|
||||
<ColGroup>
|
||||
<col />
|
||||
<col style="width: 80px;" />
|
||||
@@ -85,9 +71,13 @@
|
||||
<PropertyColumn Property="x => x.Discount" Title="تخفیف (%)" />
|
||||
<PropertyColumn Property="x => x.RemainingCount" Title="موجودی" />
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="DataModel"
|
||||
PageSizeOptions="@(new int[] { 30, 60, 90 })" />
|
||||
<MudDataGridPager T="DataModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudStack>
|
||||
@@ -134,8 +124,8 @@
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
@bind-Value="_statusEnable">
|
||||
<MudSelectItem Value="@(true)">فعالسازی</MudSelectItem>
|
||||
<MudSelectItem Value="@(false)">غیرفعالسازی</MudSelectItem>
|
||||
<MudSelectItem T="bool?" Value="@(true)">فعالسازی</MudSelectItem>
|
||||
<MudSelectItem T="bool?" Value="@(false)">غیرفعالسازی</MudSelectItem>
|
||||
</MudSelect>
|
||||
|
||||
<MudStack Row="true"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.Products
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Tizzani.MudBlazor.HtmlEditor
|
||||
@using BackOffice.Common.BaseComponents
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
|
||||
<MudDialog>
|
||||
@@ -10,7 +9,7 @@
|
||||
<MudStack Justify="Justify.Center" AlignItems="AlignItems.Center">
|
||||
@if (!string.IsNullOrWhiteSpace(_mainImagePreview))
|
||||
{
|
||||
<Image Src="@(_mainImagePreview)" Width="220" Height="140" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
<img src="@_mainImagePreview" alt="تصویر اصلی" style="width:220px; height:140px; object-fit:cover; border-radius:4px;" />
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -26,7 +25,7 @@
|
||||
Color="Color.Primary"
|
||||
ButtonType="ButtonType.Button"
|
||||
StartIcon="@Icons.Material.Filled.Image"
|
||||
Style="cursor:pointer;">
|
||||
>
|
||||
انتخاب تصویر اصلی
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
@@ -52,7 +51,7 @@
|
||||
<MudTextField T="string" @bind-Value="Model.Title" Disabled="_isLoading" Label="عنوان" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
</MudItem>
|
||||
<MudItem xs="6">
|
||||
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.Price" Format="N0" Immediate="true" Disabled="_isLoading" Label="قیمت" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.Price" Format="N0" Immediate="true" Disabled="_isLoading" Label="قیمت (ریال)" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
</MudItem>
|
||||
</MudStack>
|
||||
|
||||
@@ -64,7 +63,7 @@
|
||||
|
||||
<MudTextField T="string" @bind-Value="Model.ShortInfomation" Disabled="_isLoading" Label="خلاصه" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
|
||||
<MudHtmlEditor @bind-Html="Model.FullInformation">
|
||||
<MudHtmlEditor @bind-Html="Model.FullInformation" MinHeight="300px">
|
||||
<MudHtmlToolbarOptions InsertImage="false" />
|
||||
</MudHtmlEditor>
|
||||
|
||||
@@ -83,6 +82,10 @@
|
||||
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.subtitle2">تصویر بندانگشتی</MudText>
|
||||
@if (!string.IsNullOrWhiteSpace(_thumbnailPreview))
|
||||
{
|
||||
<img src="@_thumbnailPreview" alt="بندانگشتی" style="width:120px; height:80px; object-fit:cover; border-radius:4px;" />
|
||||
}
|
||||
<MudFileUpload T="IBrowserFile" Accept="image/*" FilesChanged="OnThumbnailSelected">
|
||||
<ActivatorContent>
|
||||
<MudButton HtmlTag="label"
|
||||
@@ -90,7 +93,7 @@
|
||||
Color="Color.Secondary"
|
||||
ButtonType="ButtonType.Button"
|
||||
StartIcon="@Icons.Material.Filled.Image"
|
||||
Style="cursor:pointer;">
|
||||
>
|
||||
انتخاب تصویر بندانگشتی (اختیاری)
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
|
||||
@@ -17,6 +17,9 @@ public partial class CreateDialog
|
||||
private readonly long _maxAllowedSize = (1024 * 1024) * 5;
|
||||
private bool _isLoading;
|
||||
private string? _mainImagePreview;
|
||||
private string? _thumbnailPreview;
|
||||
private byte[]? _mainImageBuffer;
|
||||
private byte[]? _thumbnailImageBuffer;
|
||||
private List<long> _selectedCategoryIds = new();
|
||||
|
||||
private async Task OnMainImageSelected(IBrowserFile? file)
|
||||
@@ -24,9 +27,10 @@ public partial class CreateDialog
|
||||
_mainImageFile = file;
|
||||
if (file != null)
|
||||
{
|
||||
var buffer = new byte[file.Size];
|
||||
await file.OpenReadStream(_maxAllowedSize).ReadAsync(buffer);
|
||||
_mainImagePreview = $"data:{file.ContentType};base64," + Convert.ToBase64String(buffer);
|
||||
using var ms = new MemoryStream();
|
||||
await file.OpenReadStream(_maxAllowedSize).CopyToAsync(ms);
|
||||
_mainImageBuffer = ms.ToArray();
|
||||
_mainImagePreview = $"data:{file.ContentType};base64," + Convert.ToBase64String(_mainImageBuffer);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
@@ -34,9 +38,17 @@ public partial class CreateDialog
|
||||
private async Task OnThumbnailSelected(IBrowserFile? file)
|
||||
{
|
||||
_thumbnailImageFile = file;
|
||||
if (file != null)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await file.OpenReadStream(_maxAllowedSize).CopyToAsync(ms);
|
||||
_thumbnailImageBuffer = ms.ToArray();
|
||||
_thumbnailPreview = $"data:{file.ContentType};base64," + Convert.ToBase64String(_thumbnailImageBuffer);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public async void CallCreateMethod()
|
||||
public async Task CallCreateMethod()
|
||||
{
|
||||
_isLoading = true;
|
||||
StateHasChanged();
|
||||
@@ -49,25 +61,21 @@ public partial class CreateDialog
|
||||
return;
|
||||
}
|
||||
|
||||
if (_mainImageFile != null)
|
||||
if (_mainImageFile != null && _mainImageBuffer != null)
|
||||
{
|
||||
var buffer = new byte[_mainImageFile.Size];
|
||||
await _mainImageFile.OpenReadStream(_maxAllowedSize).ReadAsync(buffer);
|
||||
Model.ImageFile = new ImageFileModel
|
||||
{
|
||||
File = ByteString.CopyFrom(buffer),
|
||||
File = ByteString.CopyFrom(_mainImageBuffer),
|
||||
Mime = _mainImageFile.ContentType,
|
||||
FileName = Path.GetFileNameWithoutExtension(_mainImageFile.Name)
|
||||
};
|
||||
}
|
||||
|
||||
if (_thumbnailImageFile != null)
|
||||
if (_thumbnailImageFile != null && _thumbnailImageBuffer != null)
|
||||
{
|
||||
var buffer = new byte[_thumbnailImageFile.Size];
|
||||
await _thumbnailImageFile.OpenReadStream(_maxAllowedSize).ReadAsync(buffer);
|
||||
Model.ThumbnailFile = new ImageFileModel
|
||||
{
|
||||
File = ByteString.CopyFrom(buffer),
|
||||
File = ByteString.CopyFrom(_thumbnailImageBuffer),
|
||||
Mime = _thumbnailImageFile.ContentType,
|
||||
FileName = Path.GetFileNameWithoutExtension(_thumbnailImageFile.Name)
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
Color="Color.Primary"
|
||||
ButtonType="ButtonType.Button"
|
||||
StartIcon="@Icons.Material.Filled.Image"
|
||||
Style="cursor:pointer;">
|
||||
>
|
||||
انتخاب تصویر
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
@@ -77,16 +77,24 @@
|
||||
{
|
||||
<MudItem xs="6" sm="4" md="3">
|
||||
<MudPaper Class="pa-2">
|
||||
<div style="position:relative; cursor:pointer;"
|
||||
<div style="position:relative;"
|
||||
@onclick="@(() => OpenPreview(item))">
|
||||
@* <img src="@item.ImageThumbnailPath" *@
|
||||
@* alt="@item.Title" *@
|
||||
@* style="width:100%; height:120px; object-fit:cover; border-radius:4px;" /> *@
|
||||
<Image Src="@item.ImageThumbnailPath"
|
||||
Width="25"
|
||||
Height="25"
|
||||
ObjectPosition="ObjectPosition.Center"
|
||||
ObjectFit="ObjectFit.Fill" />
|
||||
@if (!string.IsNullOrWhiteSpace(item.ImageThumbnailPath))
|
||||
{
|
||||
<img src="@GetFullImageUrl(item.ImageThumbnailPath)"
|
||||
alt="@item.Title"
|
||||
style="width:100%; height:120px; object-fit:cover; border-radius:4px;" />
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(item.ImagePath))
|
||||
{
|
||||
<img src="@GetFullImageUrl(item.ImagePath)"
|
||||
alt="@item.Title"
|
||||
style="width:100%; height:120px; object-fit:cover; border-radius:4px;" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Height="120px" Width="100%" />
|
||||
}
|
||||
<MudIconButton Icon="@Icons.Material.Filled.DeleteOutline"
|
||||
Color="Color.Error"
|
||||
Size="Size.Small"
|
||||
|
||||
@@ -2,6 +2,7 @@ using CMSMicroservice.Protobuf.Protos.Products;
|
||||
using Google.Protobuf;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Products.Components;
|
||||
@@ -10,6 +11,7 @@ public partial class GalleryDialog
|
||||
{
|
||||
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
[Inject] public ProductsContract.ProductsContractClient ProductsContract { get; set; } = default!;
|
||||
[Inject] private IConfiguration Configuration { get; set; } = default!;
|
||||
|
||||
[Parameter] public long ProductId { get; set; }
|
||||
[Parameter] public string ProductTitle { get; set; } = string.Empty;
|
||||
@@ -58,10 +60,10 @@ public partial class GalleryDialog
|
||||
|
||||
if (file != null)
|
||||
{
|
||||
var buffer = new byte[file.Size];
|
||||
await file.OpenReadStream(_maxAllowedSize).ReadAsync(buffer);
|
||||
_imageBuffer = buffer;
|
||||
_previewImage = $"data:{file.ContentType};base64," + Convert.ToBase64String(buffer);
|
||||
using var ms = new MemoryStream();
|
||||
await file.OpenReadStream(_maxAllowedSize).CopyToAsync(ms);
|
||||
_imageBuffer = ms.ToArray();
|
||||
_previewImage = $"data:{file.ContentType};base64," + Convert.ToBase64String(_imageBuffer);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
@@ -76,9 +78,9 @@ public partial class GalleryDialog
|
||||
|
||||
if (_imageBuffer == null)
|
||||
{
|
||||
var buffer = new byte[_file.Size];
|
||||
await _file.OpenReadStream(_maxAllowedSize).ReadAsync(buffer);
|
||||
_imageBuffer = buffer;
|
||||
using var ms = new MemoryStream();
|
||||
await _file.OpenReadStream(_maxAllowedSize).CopyToAsync(ms);
|
||||
_imageBuffer = ms.ToArray();
|
||||
}
|
||||
|
||||
var request = new AddProductImageRequest
|
||||
@@ -113,7 +115,7 @@ public partial class GalleryDialog
|
||||
|
||||
var parameters = new DialogParameters<ImagePreviewDialog>
|
||||
{
|
||||
{ x => x.ImageUrl, item.ImagePath },
|
||||
{ x => x.ImageUrl, GetFullImageUrl(item.ImagePath) },
|
||||
{ x => x.Title, item.Title }
|
||||
};
|
||||
|
||||
@@ -133,6 +135,14 @@ public partial class GalleryDialog
|
||||
|
||||
private void Close() => MudDialog.Close();
|
||||
|
||||
private string GetFullImageUrl(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path)) return string.Empty;
|
||||
if (path.StartsWith("data:") || path.StartsWith("http")) return path;
|
||||
var baseUrl = Configuration["GwUrl"]?.TrimEnd('/') ?? "";
|
||||
return baseUrl + path;
|
||||
}
|
||||
|
||||
private class GalleryItemViewModel
|
||||
{
|
||||
public long ProductGalleryId { get; set; }
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.Products
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Tizzani.MudBlazor.HtmlEditor
|
||||
@using BackOffice.Common.BaseComponents
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
|
||||
<MudDialog>
|
||||
@@ -10,7 +9,7 @@
|
||||
<MudStack Justify="Justify.Center" AlignItems="AlignItems.Center">
|
||||
@if (!string.IsNullOrWhiteSpace(_mainImagePreview))
|
||||
{
|
||||
<Image Src="@(_mainImagePreview)" Width="220" Height="140" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
<img src="@GetFullImageUrl(_mainImagePreview)" alt="تصویر اصلی" style="width:220px; height:140px; object-fit:cover; border-radius:4px;" />
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -26,7 +25,7 @@
|
||||
Color="Color.Primary"
|
||||
ButtonType="ButtonType.Button"
|
||||
StartIcon="@Icons.Material.Filled.Image"
|
||||
Style="cursor:pointer;">
|
||||
>
|
||||
تغییر تصویر اصلی
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
@@ -38,7 +37,7 @@
|
||||
<MudTextField T="string" @bind-Value="Model.Title" Disabled="_isLoading" Label="عنوان" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
</MudItem>
|
||||
<MudItem xs="6">
|
||||
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.Price" Format="N0" Immediate="true" Disabled="_isLoading" Label="قیمت" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.Price" Format="N0" Immediate="true" Disabled="_isLoading" Label="قیمت (ریال)" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
</MudItem>
|
||||
</MudStack>
|
||||
|
||||
@@ -50,7 +49,7 @@
|
||||
|
||||
<MudTextField T="string" @bind-Value="Model.ShortInfomation" Disabled="_isLoading" Label="خلاصه" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
|
||||
<MudHtmlEditor @bind-Html="Model.FullInformation">
|
||||
<MudHtmlEditor @bind-Html="Model.FullInformation" MinHeight="300px">
|
||||
<MudHtmlToolbarOptions InsertImage="false" />
|
||||
</MudHtmlEditor>
|
||||
|
||||
@@ -69,6 +68,14 @@
|
||||
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.subtitle2">تصویر بندانگشتی</MudText>
|
||||
@if (!string.IsNullOrWhiteSpace(_thumbnailPreview))
|
||||
{
|
||||
<img src="@GetFullImageUrl(_thumbnailPreview)" alt="بندانگشتی" style="width:120px; height:80px; object-fit:cover; border-radius:4px;" />
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(Model.ThumbnailPath))
|
||||
{
|
||||
<img src="@GetFullImageUrl(Model.ThumbnailPath)" alt="بندانگشتی" style="width:120px; height:80px; object-fit:cover; border-radius:4px;" />
|
||||
}
|
||||
<MudFileUpload T="IBrowserFile" Accept="image/*" FilesChanged="OnThumbnailSelected">
|
||||
<ActivatorContent>
|
||||
<MudButton HtmlTag="label"
|
||||
@@ -76,7 +83,7 @@
|
||||
Color="Color.Secondary"
|
||||
ButtonType="ButtonType.Button"
|
||||
StartIcon="@Icons.Material.Filled.Image"
|
||||
Style="cursor:pointer;">
|
||||
>
|
||||
تغییر تصویر بندانگشتی (اختیاری)
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user