From 6db83ccd90aca7080dafcc450f927b46b7561aa2 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Mon, 16 Feb 2026 00:51:39 +0330 Subject: [PATCH] feat: full FrontOffice updates - discount store, blog, payment gateway, UI improvements - Discount store pages (products, cart, orders, order detail) - Blog pages and services - Payment gateway callback page - AppImage component, EmptyState, LoadingState, PageHeader - DiscountCartService, DiscountOrderService, DiscountProductService - BlogCategoryService, BlogPostService, SitePageService, ImageCacheService - PhoneVerifyForm component - Profile Hub page - UI/UX improvements across all pages - landing.js for homepage - Config and routing updates --- docs/CHANGELOG.md | 240 ++++ docs/UI-UNIFICATION-PLAN.md | 542 ++++++++ src/FrontOffice.Main/ConfigureServices.cs | 28 + src/FrontOffice.Main/Pages/About.razor | 434 +++--- src/FrontOffice.Main/Pages/About.razor.cs | 33 +- src/FrontOffice.Main/Pages/Blog/Index.razor | 172 +++ .../Pages/Blog/Index.razor.cs | 91 ++ src/FrontOffice.Main/Pages/Blog/Post.razor | 122 ++ src/FrontOffice.Main/Pages/Blog/Post.razor.cs | 51 + src/FrontOffice.Main/Pages/Checkout.razor | 15 +- src/FrontOffice.Main/Pages/Checkout.razor.cs | 2 +- .../Pages/Club/FeaturesPage.razor | 18 +- .../Pages/Club/MembershipPage.razor | 7 +- .../Commission/CommissionDashboardPage.razor | 7 +- .../Pages/Commission/WeeklyBalancePage.razor | 17 +- src/FrontOffice.Main/Pages/Contact.razor | 46 +- src/FrontOffice.Main/Pages/Contact.razor.cs | 79 +- .../Pages/DiscountStore/Cart.razor | 190 +++ .../Pages/DiscountStore/Cart.razor.cs | 56 + .../Pages/DiscountStore/Checkout.razor | 193 +++ .../Pages/DiscountStore/Checkout.razor.cs | 146 ++ .../Pages/DiscountStore/OrderDetail.razor | 198 +++ .../Pages/DiscountStore/OrderDetail.razor.cs | 76 ++ .../Pages/DiscountStore/Orders.razor | 130 ++ .../Pages/DiscountStore/Orders.razor.cs | 65 + .../Pages/DiscountStore/ProductDetail.razor | 140 ++ .../DiscountStore/ProductDetail.razor.cs | 64 + .../Pages/DiscountStore/Products.razor | 133 ++ .../Pages/DiscountStore/Products.razor.cs | 87 ++ src/FrontOffice.Main/Pages/FAQ.razor | 2 +- .../Pages/Gateway/CartChooser.razor | 62 + .../Pages/Gateway/CartChooser.razor.cs | 47 + .../Pages/Gateway/OrdersChooser.razor | 49 + .../Pages/Gateway/StoreChooser.razor | 56 + .../Pages/Gateway/StoreChooser.razor.cs | 43 + src/FrontOffice.Main/Pages/Index.razor | 647 +++++---- src/FrontOffice.Main/Pages/Index.razor.cs | 188 +-- .../Pages/Network/NetworkStatisticsPage.razor | 7 +- .../Pages/Package/MyPackages.razor | 11 +- .../Pages/Package/Packages.razor | 8 +- .../Pages/PackageDetail.razor | 31 +- .../Pages/PackageDetail.razor.cs | 2 +- .../Pages/Profile/Addresses.razor | 5 +- .../Pages/Profile/ChangePassword.razor | 5 +- .../Components/OrganizationChart.razor | 12 +- .../Components/OrganizationChartLevel.razor | 6 +- src/FrontOffice.Main/Pages/Profile/Hub.razor | 64 + .../Pages/Profile/Hub.razor.cs | 34 + .../Pages/Profile/Index.razor | 428 +++--- .../Pages/Profile/Index.razor.cs | 60 +- .../Pages/Profile/PaymentCallback.razor | 7 +- .../Pages/Profile/Personal.razor | 7 +- .../Pages/Profile/Personal.razor.cs | 19 +- .../Pages/Profile/Settings.razor | 5 +- .../Pages/Profile/Settings.razor.cs | 15 +- src/FrontOffice.Main/Pages/Profile/Tree.razor | 5 +- .../Pages/Profile/Wallet.razor | 5 +- .../Pages/Profile/WithdrawalRequests.razor | 7 +- .../Pages/RegisterWizard.razor | 123 +- .../Pages/RegisterWizard.razor.cs | 10 +- src/FrontOffice.Main/Pages/Store/Cart.razor | 18 +- .../Pages/Store/Cart.razor.cs | 2 +- .../Pages/Store/Categories.razor | 2 +- .../Pages/Store/CheckoutSummary.razor | 11 +- .../Pages/Store/CheckoutSummary.razor.cs | 2 +- .../Pages/Store/OrderDetail.razor | 28 +- .../Pages/Store/OrderDetail.razor.cs | 2 +- .../Pages/Store/OrderTracking.razor | 3 +- .../Pages/Store/OrderTracking.razor.cs | 4 + src/FrontOffice.Main/Pages/Store/Orders.razor | 6 +- .../Pages/Store/ProductDetail.razor | 19 +- .../Pages/Store/Products.razor | 21 +- src/FrontOffice.Main/Pages/_Host.cshtml | 11 +- src/FrontOffice.Main/Shared/AppImage.razor | 101 ++ src/FrontOffice.Main/Shared/AuthDialog.razor | 249 ++-- .../Shared/AuthDialog.razor.cs | 21 +- .../Shared/ClubMembershipContractDialog.razor | 5 +- src/FrontOffice.Main/Shared/EmptyState.razor | 67 + .../Shared/LoadingState.razor | 18 + src/FrontOffice.Main/Shared/MainLayout.razor | 218 ++- .../Shared/MainLayout.razor.cs | 117 +- src/FrontOffice.Main/Shared/PageHeader.razor | 40 + .../Shared/PhoneVerifyForm.razor | 96 ++ .../Shared/PhoneVerifyForm.razor.cs | 42 + .../Utilities/AuthDialogService.cs | 14 +- src/FrontOffice.Main/Utilities/AuthService.cs | 2 +- .../Utilities/BlogCategoryService.cs | 60 + .../Utilities/BlogPostService.cs | 204 +++ src/FrontOffice.Main/Utilities/CartService.cs | 2 +- .../Utilities/CustomMudTheme.cs | 145 +- .../Utilities/DiscountCartService.cs | 151 ++ .../Utilities/DiscountOrderService.cs | 209 +++ .../Utilities/DiscountProductService.cs | 207 +++ .../Utilities/ImageCacheService.cs | 122 ++ .../Utilities/OrderService.cs | 4 +- .../Utilities/PackageService.cs | 4 +- .../Utilities/ProductService.cs | 4 +- .../Utilities/RouteConstants.cs | 27 + .../Utilities/SitePageService.cs | 114 ++ src/FrontOffice.Main/Utilities/UrlUtility.cs | 12 + src/FrontOffice.Main/appsettings.Staging.json | 2 +- src/FrontOffice.Main/appsettings.json | 2 +- src/FrontOffice.Main/wwwroot/css/site.css | 1211 ++++++++++++++++- src/FrontOffice.Main/wwwroot/js/landing.js | 87 ++ 104 files changed, 7668 insertions(+), 1370 deletions(-) create mode 100644 docs/CHANGELOG.md create mode 100644 docs/UI-UNIFICATION-PLAN.md create mode 100644 src/FrontOffice.Main/Pages/Blog/Index.razor create mode 100644 src/FrontOffice.Main/Pages/Blog/Index.razor.cs create mode 100644 src/FrontOffice.Main/Pages/Blog/Post.razor create mode 100644 src/FrontOffice.Main/Pages/Blog/Post.razor.cs create mode 100644 src/FrontOffice.Main/Pages/DiscountStore/Cart.razor create mode 100644 src/FrontOffice.Main/Pages/DiscountStore/Cart.razor.cs create mode 100644 src/FrontOffice.Main/Pages/DiscountStore/Checkout.razor create mode 100644 src/FrontOffice.Main/Pages/DiscountStore/Checkout.razor.cs create mode 100644 src/FrontOffice.Main/Pages/DiscountStore/OrderDetail.razor create mode 100644 src/FrontOffice.Main/Pages/DiscountStore/OrderDetail.razor.cs create mode 100644 src/FrontOffice.Main/Pages/DiscountStore/Orders.razor create mode 100644 src/FrontOffice.Main/Pages/DiscountStore/Orders.razor.cs create mode 100644 src/FrontOffice.Main/Pages/DiscountStore/ProductDetail.razor create mode 100644 src/FrontOffice.Main/Pages/DiscountStore/ProductDetail.razor.cs create mode 100644 src/FrontOffice.Main/Pages/DiscountStore/Products.razor create mode 100644 src/FrontOffice.Main/Pages/DiscountStore/Products.razor.cs create mode 100644 src/FrontOffice.Main/Pages/Gateway/CartChooser.razor create mode 100644 src/FrontOffice.Main/Pages/Gateway/CartChooser.razor.cs create mode 100644 src/FrontOffice.Main/Pages/Gateway/OrdersChooser.razor create mode 100644 src/FrontOffice.Main/Pages/Gateway/StoreChooser.razor create mode 100644 src/FrontOffice.Main/Pages/Gateway/StoreChooser.razor.cs create mode 100644 src/FrontOffice.Main/Pages/Profile/Hub.razor create mode 100644 src/FrontOffice.Main/Pages/Profile/Hub.razor.cs create mode 100644 src/FrontOffice.Main/Shared/AppImage.razor create mode 100644 src/FrontOffice.Main/Shared/EmptyState.razor create mode 100644 src/FrontOffice.Main/Shared/LoadingState.razor create mode 100644 src/FrontOffice.Main/Shared/PageHeader.razor create mode 100644 src/FrontOffice.Main/Shared/PhoneVerifyForm.razor create mode 100644 src/FrontOffice.Main/Shared/PhoneVerifyForm.razor.cs create mode 100644 src/FrontOffice.Main/Utilities/BlogCategoryService.cs create mode 100644 src/FrontOffice.Main/Utilities/BlogPostService.cs create mode 100644 src/FrontOffice.Main/Utilities/DiscountCartService.cs create mode 100644 src/FrontOffice.Main/Utilities/DiscountOrderService.cs create mode 100644 src/FrontOffice.Main/Utilities/DiscountProductService.cs create mode 100644 src/FrontOffice.Main/Utilities/ImageCacheService.cs create mode 100644 src/FrontOffice.Main/Utilities/SitePageService.cs create mode 100644 src/FrontOffice.Main/wwwroot/js/landing.js diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md new file mode 100644 index 0000000..a4af557 --- /dev/null +++ b/docs/CHANGELOG.md @@ -0,0 +1,240 @@ +# 📋 تاریخچه تغییرات FrontOffice — کارا بازار سلامت + +> **آخرین بروزرسانی**: بهمن ۱۴۰۴ (فوریه ۲۰۲۶) +> **فریمورک**: Blazor Server + MudBlazor 8.14 + .NET 9 + +--- + +## 🔖 نسخه ۲.۵.۰ — بهمن ۱۴۰۴ + +### ۱. محافظت صفحات نیازمند احراز هویت (Auth Guard) + +**فایل‌های تغییریافته:** +- `Shared/MainLayout.razor.cs` + +**شرح:** +قبلاً هیچ محافظتی در سطح مسیریابی وجود نداشت — کاربر غیرلاگین می‌توانست مستقیماً به `/profile/*`، `/commission/*`، `/cart` و... دسترسی پیدا کند. + +**تغییرات:** +- متد `EnforceAuthGuardAsync()` اضافه شد — در هر تغییر مسیر و اولین بار رندر اجرا می‌شود +- متد `IsProtectedRoute(path)` مسیرهای محافظت‌شده را تشخیص می‌دهد +- کاربر غیرلاگین → ریدایرکت به `/` (صفحه اصلی) + +**مسیرهای محافظت‌شده:** +| گروه | مسیرها | +|---|---| +| پروفایل | `/profile/*` | +| کمیسیون | `/commission/*` | +| شبکه | `/network/*` | +| باشگاه | `/club/*` | +| سبد خرید | `/cart`, `/checkout*`, `/orders`, `/order/*`, `/order-tracking/*` | +| پکیج‌ها | `/my-packages` | +| دروازه | `/my-orders`, `/my-cart` | +| فروشگاه تخفیفی | `/discount-store/cart`, `/discount-store/checkout`, `/discount-store/orders`, `/discount-store/order/*` | + +**مسیرهای عمومی:** +`/`, `/register`, `/about`, `/faq`, `/contact`, `/blog/*`, `/packages`, `/package/*`, `/products`, `/product/*`, `/categories`, `/stores`, `/discount-store`, `/discount-store/product/*` + +--- + +### ۲. استخراج کامپوننت PhoneVerifyForm + +**فایل‌های جدید:** +- `Shared/PhoneVerifyForm.razor` +- `Shared/PhoneVerifyForm.razor.cs` + +**فایل‌های تغییریافته:** +- `Shared/AuthDialog.razor` +- `Shared/AuthDialog.razor.cs` + +**شرح:** +فرم تلفن + تایید OTP + کپچا که قبلاً به‌صورت `RenderFragment` با `__builder` مستقیم در `AuthDialog` نوشته شده بود، به کامپوننت مستقل `PhoneVerifyForm` استخراج شد. + +**ساختار قبل:** +``` +AuthDialog.razor + └── @code { PhoneOrVerifyContent() => __builder => { ... } } ← RenderFragment پیچیده +``` + +**ساختار بعد:** +``` +AuthDialog.razor + └── ← کامپوننت مستقل +PhoneVerifyForm.razor ← مارکاپ فرم +PhoneVerifyForm.razor.cs ← پارامترها و فرم رف‌ها +``` + +**پارامترهای PhoneVerifyForm:** + +| پارامتر | نوع | توضیح | +|---|---|---| +| `CurrentStep` | `AuthStep` | مرحله فعلی (Phone / Verify) | +| `PhoneRequest` | `CreateNewOtpTokenRequest` | مدل فرم تلفن | +| `VerifyRequest` | `VerifyOtpTokenRequest` | مدل فرم تایید | +| `CaptchaCode` | `string?` | کد کپچا نمایش‌داده‌شده | +| `CaptchaInput` / `CaptchaInputChanged` | `string?` + `EventCallback` | ورودی کپچا (two-way) | +| `OnRefreshCaptcha` | `EventCallback` | رفرش کپچا | +| `IsBusy` | `bool` | وضعیت بارگذاری | +| `ErrorMessage` / `InfoMessage` | `string?` | پیام‌های خطا/اطلاع | +| `PhoneNumber` | `string?` | شماره تایید‌شده | +| `ResendRemaining` | `int` | ثانیه تا ارسال مجدد | +| `OnChangePhone` / `OnResendOtp` | `EventCallback` | اکشن‌های تایید | + +**نکته مهم — Two-way binding کپچا:** +فیلد کپچا با `Value` + `ValueChanged` بایند شده (نه `@bind-Value`) تا مقدار تایپ‌شده از فرزند به والد برگردد: +```razor + +``` + +**دسترسی به فرم‌ها از والد:** +```csharp +// AuthDialog.razor.cs +var phoneForm = _phoneVerifyForm?.GetPhoneForm(); +var verifyForm = _phoneVerifyForm?.GetVerifyForm(); +``` + +--- + +### ۳. بهبود لایوت مدال ورود (AuthDialog) + +**فایل‌های تغییریافته:** +- `Shared/AuthDialog.razor` +- `Utilities/AuthDialogService.cs` +- `wwwroot/css/site.css` + +**مشکلات قبلی:** +- دیالوگ روی موبایل `FullScreen` بود → فضای خالی بزرگ بین فرم و دکمه‌ها +- `TitleContent` و `DialogActions` جدا → گپ عمودی +- ردیف کپچا با `MudStack Row="true"` → آیتم‌ها عمودی رندر می‌شدند + +**تغییرات:** +1. **حذف FullScreen**: `AuthDialogService` حالا همیشه `MaxWidth.ExtraSmall, FullWidth=true, CloseButton=true` +2. **ادغام محتوا**: آواتار + عنوان + فرم + دکمه‌ها همه داخل `DialogContent` → بدون `TitleContent` و `DialogActions` +3. **کلاس `auth-dialog-wrapper`**: CSS با `.auth-dialog-wrapper .mud-dialog-title { display: none; }` عنوان پیش‌فرض دیالوگ رو مخفی می‌کنه +4. **ردیف کپچا**: از `MudStack Row` به `div.captcha-row` با CSS flex اختصاصی +5. **حالت Inline**: بدون تغییر ساختاری — فقط از `PhoneVerifyForm` استفاده می‌کنه + +**CSS جدید:** +```css +.auth-dialog-wrapper .mud-dialog-title { display: none; } +.auth-dialog-wrapper .mud-dialog-content { padding-bottom: 24px !important; } +.auth-dialog-content { max-width: 400px; margin: 0 auto; } + +.captcha-row { display: flex; align-items: center; gap: 10px; } +.captcha-row > .mud-input-control { flex: 1 1 0; min-width: 0; } +.captcha-row > .mud-paper { flex: 0 0 auto; } +.captcha-row > .mud-button-root { flex: 0 0 auto; } +``` + +--- + +### ۴. صفحه لندینگ — حذف /pricing و اضافه بنر بلاگ + +**فایل‌های تغییریافته:** +- `Pages/Index.razor` +- `wwwroot/css/site.css` + +**تغییرات:** +1. دکمه هیرو «مشاهده پکیج‌ها» → **«آخرین اخبار»** با لینک `/blog` +2. **بنر جدیدترین مطلب** بین هیرو و «سه گام تا شروع» اضافه شد + - تصویر بندانگشتی + عنوان + خلاصه + آیکون شیشه‌ای + - هاور: `translateY(-2px)` + سایه بزرگ‌تر + +**CSS جدید:** +```css +.landing-blog-banner { border: 1px solid var(--mud-palette-divider); transition: ... } +.landing-blog-banner:hover { box-shadow: var(--mud-elevation-4); transform: translateY(-2px); } +.landing-blog-thumb { width: 80px; height: 80px; border-radius: 12px; } +.landing-blog-title { -webkit-line-clamp: 1; font-weight: 600; } +.landing-blog-summary { -webkit-line-clamp: 1; } +``` + +--- + +### ۵. افزایش ارتفاع تکست‌باکس‌ها (Global) + +**فایل تغییریافته:** +- `wwwroot/css/site.css` + +**قبل:** `padding: 10px 14px` +**بعد:** `padding: 14px 14px` + `font-size: 1rem` + +```css +.mud-input-outlined .mud-input-slot { + padding: 14px 14px !important; + font-size: 1rem; +} +``` + +تمام فیلدهای Outlined در کل اپلیکیشن بزرگ‌تر شدند. + +--- + +### ۶. اصلاح RTL فیلدهای ورودی + +**فایل تغییریافته:** +- `wwwroot/css/site.css` + +**مشکل:** فیلدهای `type="tel"` به‌صورت پیش‌فرض مرورگر `direction: ltr` می‌گیرن — لیبل سمت راست ولی placeholder/cursor سمت چپ. + +**اصلاح:** +```css +.mud-input-slot input, +.mud-input-slot textarea { + direction: rtl !important; + text-align: right !important; +} +``` + +--- + +### ۷. اصلاح captcha-box CSS + +**فایل تغییریافته:** +- `wwwroot/css/site.css` + +**قبل:** +```css +.captcha-box { + min-width: 120px; min-height: 56px; + background: linear-gradient(135deg, rgba(123,97,255,.12), rgba(255,140,189,.12)); +} +``` + +**بعد:** +```css +.captcha-box { + min-width: 96px; min-height: 48px; + border-radius: var(--mud-default-borderradius); + background: rgba(99,102,241,.08); + border: 1px solid var(--mud-palette-divider); +} +``` + +--- + +## 📁 نقشه فایل‌ها + +``` +Shared/ +├── AuthDialog.razor ← بازنویسی (کامپوننت PhoneVerifyForm جایگزین RenderFragment) +├── AuthDialog.razor.cs ← بروزرسانی (استفاده از _phoneVerifyForm) +├── PhoneVerifyForm.razor ← ✨ جدید (فرم تلفن + تایید + کپچا) +├── PhoneVerifyForm.razor.cs ← ✨ جدید (پارامترها و فرم رف‌ها) +├── MainLayout.razor.cs ← Auth Guard اضافه شد +Pages/ +├── Index.razor ← /pricing → /blog + بنر بلاگ +Utilities/ +├── AuthDialogService.cs ← حذف FullScreen، ثابت‌سازی سایز +wwwroot/css/ +├── site.css ← captcha-row, auth-dialog, RTL fix, input height +``` + +--- + +## 🔍 وضعیت بیلد + +| تاریخ | خطا | هشدار | توضیح | +|---|---|---|---| +| بهمن ۱۴۰۴ | **۰** | ۱۰۵ | MUD0002 warnings (بی‌خطر — مربوط به MudBlazor analyzer) | diff --git a/docs/UI-UNIFICATION-PLAN.md b/docs/UI-UNIFICATION-PLAN.md new file mode 100644 index 0000000..daa7d19 --- /dev/null +++ b/docs/UI-UNIFICATION-PLAN.md @@ -0,0 +1,542 @@ +# 🎨 پلن جامع یکپارچه‌سازی UI/UX — کارا بازار سلامت + +> **تاریخ**: بهمن ۱۴۰۴ +> **وضعیت**: ✅ **تمام ۶ فاز + فاز ۷ (ناوبری و امنیت) تکمیل شد** +> **هدف**: یکپارچه‌سازی طراحی FrontOffice با ۲۰٪ تغییر UX و ۵۰٪ تغییر UI +> **فریمورک**: Blazor Server + MudBlazor 8.14 +> **📋 تاریخچه تغییرات جزئی**: [CHANGELOG.md](CHANGELOG.md) + +--- + +## 📊 خلاصه اجرایی + +| شاخص | وضعیت قبل | وضعیت بعد | +|---|---|---| +| **صفحات کل** | ۴۱ صفحه + ۶ کامپوننت مشترک | ۴۱ صفحه + ۸ کامپوننت مشترک | +| **الگوی PageHeader** | ۱۹ صفحه از ۴۱ | ۳۲+ صفحه ✅ | +| **Inline Style سنگین** | ۱۱ صفحه 🔴 | ≤۲ صفحه (فقط gradient‌های تزئینی) ✅ | +| **Loading State** | ۲ الگوی متفاوت (Circular vs Linear) | `` واحد ✅ | +| **Empty State** | ۴+ الگوی متناقض | `` واحد ✅ | +| **Container Spacing** | ۶ الگوی متناقض | `py-6` استاندارد ✅ | +| **رنگ‌بندی** | رنگ‌های hardcoded در ۸+ صفحه | CSS Variable ✅ | +| **تم پالت** | Primary `#0380C0` (آبی ساده) | Indigo `#6366f1` + Full PaletteDark ✅ | +| **Elevation** | مخلوط ۲/۳/۴ | استاندارد ۰–۲ ✅ | +| **Build** | ۰ خطا | ۰ خطا ✅ | + +### فازها +| فاز | عنوان | وضعیت | +|---|---|---| +| ۱ | زیرساخت دیزاین سیستم | ✅ تکمیل | +| ۲ | صفحات پروفایل | ✅ تکمیل | +| ۳ | صفحات تخصصی | ✅ تکمیل | +| ۴ | بهبود بصری فروشگاه | ✅ تکمیل | +| ۵ | صفحات عمومی | ✅ تکمیل | +| ۶ | پالیش و تست | ✅ تکمیل | +| ۷ | ناوبری، امنیت و بازسازی کامپوننت‌ها | ✅ تکمیل | + +--- + +## 🔍 بخش ۱: تحلیل ضعف‌های جاری + +### ۱.۱ ناسازگاری‌های ساختاری (Structural) + +#### ❌ ۱.۱.۱ — PageHeader دوگانه +**مشکل**: نیمی از صفحات از `` استفاده می‌کنند، نیم دیگر header دستی دارند. + +| از `` استفاده می‌کنند ✅ | Header دستی دارند ❌ | +|---|---| +| Store/* (۸ صفحه) | Profile/* (۷ صفحه) | +| DiscountStore/* (۶ صفحه) | Club/* (۲ صفحه) | +| Gateway/* (۳ صفحه) | Commission/* (۲ صفحه) | +| Package/* (۲ صفحه) | Network/* (۱ صفحه) | +| PackageDetail, Checkout | Blog/*, Index, About, Contact, FAQ | + +**تأثیر**: ظاهر متفاوت دکمه بازگشت، فاصله‌بندی ناهماهنگ + +#### ❌ ۱.۱.۲ — Container MaxWidth متناقض +``` +MaxWidth.Large → اکثر صفحات +MaxWidth.Medium → Personal, Settings, OrderTracking, Blog/Post, Gateway/* +MaxWidth.Small → ChangePassword +ترکیبی (loading≠content) → OrderDetail, ProductDetail, PackageDetail +``` + +**تأثیر**: برخی صفحات پهن‌تر از حد نیاز هستند (مثلاً فرم‌های ساده با Large) + +#### ❌ ۱.۱.۳ — Container Padding متناقض +``` +py-6 → اکثریت (استاندارد) +py-8 → Package/Packages, Package/MyPackages +pa-2 pa-md-6 → Store/Products, DiscountStore/Products +py-6 py-md-10 → Gateway/* +py-4 py-md-6 → DiscountStore/ProductDetail +py-16 سکشنی → Index, About, Contact, FAQ +``` + +**قاعده پیشنهادی**: `py-6` برای صفحات داخلی، section-based برای صفحات عمومی + +--- + +### ۱.۲ ناسازگاری‌های بصری (Visual) + +#### ❌ ۱.۲.۱ — Inline Style سنگین (۱۱ صفحه) + +| صفحه | نمونه مشکل‌دار | +|---|---| +| **Index.razor** | `Style="color:#fff; font-size:clamp(1.6rem,4.5vw,2.4rem);"` | +| **Profile/Index** | `Style="background:rgba(99,102,241,.12);"` | +| **Store/ProductDetail** | `style="width:100%;height:360px;background-image:url(...);"` | +| **PackageDetail** | `Style="background: radial-gradient(600px 280px..."` | +| **Checkout** | `Style="background: radial-gradient(..."` + `Elevation="4"` | +| **Blog/Index** | `Style="color:#fff; font-weight:700;"`, `Style="font-size:3.5rem;"` | +| **Blog/Post** | `Style="width:100%; height:100%;"`, `Style="font-size:4rem;"` | +| **About** | `Style="background: radial-gradient(...);"` | +| **WeeklyBalance** | `Style="background: linear-gradient(135deg, #e8f5e9..."` | +| **Gateway/**** | `Style="background:rgba(16,185,129,.12)..."`, `Style="color:#10b981;"` | +| **DiscountStore/ProductDetail** | `Style="background:rgba(16,185,129,.06)..."` | + +#### ❌ ۱.۲.۲ — رنگ‌های Hardcoded + +| رنگ | استفاده | باید باشد | +|---|---|---| +| `#10b981` | Gateway (سبز فروشگاه) | `var(--ds-color-store)` | +| `#ef4444` | Gateway (قرمز تخفیفی) | `var(--ds-color-discount)` | +| `#6366f1`, `#818cf8`, `#a78bfa` | Hero/Banner gradients | `var(--ds-gradient-primary)` | +| `rgba(99,102,241,.12)` | Dashboard backgrounds | `var(--ds-primary-soft)` | +| `rgba(16,185,129,.06)` | Discount product highlights | `var(--ds-success-soft)` | +| `#e8f5e9`, `#c8e6c9` | WeeklyBalance stat cards | `var(--ds-success-gradient)` | + +#### ❌ ۱.۲.۳ — Elevation ناهماهنگ +``` +Elevation="0" → Blog cards (با border) +Elevation="1" → برخی صفحات +Elevation="2" → اکثر صفحات (استاندارد) +Elevation="3" → WeeklyBalance stat cards +Elevation="4" → Checkout.razor +``` + +**قاعده پیشنهادی**: `Elevation="0"` با `border` = کارت‌های اطلاعاتی، `Elevation="2"` = default + +#### ❌ ۱.۲.۴ — Paper Rounding ناهماهنگ +``` +rounded-lg (16px) → اکثریت +rounded-xl (20px) → Index, Profile/Index, Gateway, DiscountStore/ProductDetail +بدون class → design system !important → 12px +``` + +--- + +### ۱.۳ ناسازگاری‌های UX (تجربه کاربری) + +#### ❌ ۱.۳.۱ — Loading State دوگانه +``` +MudProgressCircular → Store/*, DiscountStore/*, Package/*, About, Blog, Addresses +MudProgressLinear → Club/*, Commission/*, Network/*, RegisterWizard +``` + +**مشکل**: کاربر دو تجربه مختلف «در حال بارگذاری» می‌بیند + +#### ❌ ۱.۳.۲ — Empty State ناهماهنگ +``` +MudAlert Severity.Info → Store/Orders, Categories, DiscountStore/Orders +Icon + Text + Button → Addresses, Package/MyPackages, PackageDetail +MudAlert Severity.Warning → WithdrawalRequests +Custom dashed-border paper → Blog/Index +``` + +#### ❌ ۱.۳.۳ — Routing Directive ناهماهنگ +``` +@attribute [Route(RouteConstants...)] → ۳۹ صفحه ✅ +@page "/categories" → Categories.razor ❌ +@page "/blog/{Slug}" → Blog/Post.razor ❌ +``` + +#### ❌ ۱.۳.۴ — PackageDetail Loading/Error بدون Container +Loading و Error state در `PackageDetail.razor` بدون `MudContainer` رندر می‌شوند → محتوا تمام‌عرض نمایش می‌یابد. + +#### ❌ ۱.۳.۵ — تم فعلی کم‌رنگ +```csharp +// CustomMudTheme.cs فعلی +Primary = "#0380C0" // آبی ساده +// بدون Secondary، Tertiary، Info، Warning تعریف‌شده +// بدون PaletteDark +// بدون LayoutProperties +``` + +**مشکلات**: +- فقط Primary تعریف شده، بقیه رنگ‌ها default MudBlazor +- Dark mode بدون palette اختصاصی +- بدون `DefaultBorderRadius`، `AppbarHeight` و غیره +- تناقض بین Primary `#0380C0` و gradient‌های CSS با `#6366f1` + +--- + +## 🎯 بخش ۲: معماری دیزاین سیستم هدف + +### ۲.۱ سلسله‌مراتب صفحات + +``` +┌─────────────────────────────────────────────┐ +│ MainLayout │ +│ ├─ AppBar (fixed, transparent) │ +│ ├─ MudMainContent │ +│ │ ├─ [Public Pages] → Section-based │ +│ │ │ (Index, About, Contact, FAQ, Blog) │ +│ │ └─ [Internal Pages] → Container-based │ +│ │ ├─ │ +│ │ ├─ Content (MudStack/MudGrid) │ +│ │ └─ │ +│ ├─ Footer (hidden on mobile) │ +│ └─ BottomNav (mobile only) │ +└─────────────────────────────────────────────┘ +``` + +### ۲.۲ قواعد واحد (Single Source of Truth) + +| قاعده | مقدار | +|---|---| +| **Container MaxWidth** | `Large` = لیست/گرید, `Medium` = فرم/جزئیات, `Small` = تک‌فرم ساده | +| **Container Spacing** | `py-6` صفحات داخلی, section-based صفحات عمومی | +| **Paper Elevation** | `0` با border = کارت اطلاعاتی, `2` = default | +| **Paper Rounding** | `rounded-lg` = default, `rounded-xl` = hero/banner | +| **Loading State** | `` component واحد | +| **Empty State** | `` component واحد | +| **Page Header** | `` در تمام صفحات داخلی | +| **Content Wrapper** | `MudStack Spacing="3"` بعد از PageHeader | + +### ۲.۳ CSS Variables هدف + +```css +:root { + /* ── Brand Colors ── */ + --ds-brand-primary: #6366f1; /* Indigo — هویت اصلی */ + --ds-brand-secondary: #8b5cf6; /* Purple */ + --ds-brand-accent: #a78bfa; /* Light purple */ + + /* ── Semantic Colors ── */ + --ds-color-store: #10b981; /* فروشگاه عادی */ + --ds-color-discount: #ef4444; /* فروشگاه تخفیفی */ + --ds-color-success: #10b981; + --ds-color-warning: #f59e0b; + --ds-color-error: #ef4444; + --ds-color-info: #3b82f6; + + /* ── Soft Backgrounds ── */ + --ds-primary-soft: rgba(99,102,241,.08); + --ds-success-soft: rgba(16,185,129,.08); + --ds-error-soft: rgba(239,68,68,.08); + --ds-warning-soft: rgba(245,158,11,.08); + + /* ── Gradients ── */ + --ds-gradient-primary: linear-gradient(135deg, #6366f1 0%, #818cf8 50%, #a78bfa 100%); + --ds-gradient-store: linear-gradient(135deg, #10b981 0%, #34d399 100%); + --ds-gradient-discount: linear-gradient(135deg, #ef4444 0%, #f97316 50%, #f59e0b 100%); + --ds-gradient-success: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%); + + /* ── Spacing (existing) ── */ + --ds-radius-sm: 8px; + --ds-radius-md: 12px; + --ds-radius-lg: 16px; + --ds-radius-xl: 20px; + --ds-transition: 0.2s ease; + --ds-shadow-sm: 0 1px 3px rgba(0,0,0,.06); + --ds-shadow-md: 0 4px 12px rgba(0,0,0,.08); + --ds-shadow-lg: 0 8px 24px rgba(0,0,0,.10); +} +``` + +--- + +## 🚀 بخش ۳: فازبندی اجرا + +### 🔷 فاز ۱ — زیرساخت دیزاین سیستم (UI ~15%) +> **اولویت**: بالا | **ریسک**: پایین | **حجم**: ۶ فایل + +| # | تسک | فایل | نوع تغییر | +|---|---|---|---| +| 1.1 | ارتقاء `CustomMudTheme.cs` — اضافه کردن PaletteDark، LayoutProperties، رنگ‌های Secondary/Tertiary/Info، تغییر Primary به `#6366f1` | `CustomMudTheme.cs` | UI | +| 1.2 | توسعه CSS Variables — اضافه کردن brand colors، semantic colors، soft backgrounds، gradients | `site.css` | UI | +| 1.3 | ساخت `` component واحد | `Shared/LoadingState.razor` (جدید) | UX | +| 1.4 | ساخت `` component واحد | `Shared/EmptyState.razor` (جدید) | UX | +| 1.5 | بهبود `` — اضافه کردن آیکون، subtitle اختیاری | `Shared/PageHeader.razor` | UI | +| 1.6 | اضافه کردن `.page-container` CSS pattern | `site.css` | UI | + +--- + +### 🔷 فاز ۲ — یکپارچه‌سازی صفحات Profile (UI ~10%, UX ~5%) +> **اولویت**: بالا | **ریسک**: پایین | **حجم**: ۹ فایل + +| # | تسک | تغییرات | +|---|---|---| +| 2.1 | `Profile/Personal` → جایگزینی header دستی با `` | UX | +| 2.2 | `Profile/Addresses` → `` + `` + `` | UX | +| 2.3 | `Profile/Wallet` → `` | UX | +| 2.4 | `Profile/Settings` → `` | UX | +| 2.5 | `Profile/Tree` → `` | UX | +| 2.6 | `Profile/WithdrawalRequests` → `` + `` | UX | +| 2.7 | `Profile/ChangePassword` → `` | UX | +| 2.8 | `Profile/Index` (Dashboard) → حذف inline styles، استفاده از CSS Variables | UI | +| 2.9 | `Club/MembershipPage` + `Club/FeaturesPage` → `` + `` | UX | + +--- + +### 🔷 فاز ۳ — یکپارچه‌سازی صفحات تخصصی (UI ~5%, UX ~5%) +> **اولویت**: متوسط | **ریسک**: پایین | **حجم**: ۵ فایل + +| # | تسک | تغییرات | +|---|---|---| +| 3.1 | `Commission/Dashboard` → `` + `` | UX | +| 3.2 | `Commission/WeeklyBalance` → `` + حذف inline gradient styles | UI + UX | +| 3.3 | `Network/NetworkStatistics` → `` + `` | UX | +| 3.4 | `PackageDetail` → wrap loading/error در `MudContainer` | UX bug fix | +| 3.5 | `Checkout` → Elevation=4→2، حذف inline radial-gradient | UI | + +--- + +### 🔷 فاز ۴ — بهبود بصری فروشگاه‌ها (UI ~10%) +> **اولویت**: متوسط | **ریسک**: پایین | **حجم**: ۶ فایل + +| # | تسک | تغییرات | +|---|---|---| +| 4.1 | `Store/Products` → حذف inline hero styles، استفاده از CSS class | UI | +| 4.2 | `Store/ProductDetail` → حذف inline image styles، ساخت `.product-image-main` CSS | UI | +| 4.3 | `DiscountStore/ProductDetail` → حذف hardcoded rgba، استفاده از `--ds-success-soft` | UI | +| 4.4 | `Gateway/*` (۳ صفحه) → حذف hardcoded `#10b981`/`#ef4444`، استفاده از `--ds-color-store`/`--ds-color-discount` | UI | +| 4.5 | یکسان‌سازی Elevation → `0` با border یا `2` | UI | +| 4.6 | یکسان‌سازی Container spacing → `py-6` | UI | + +--- + +### 🔷 فاز ۵ — بهبود صفحات عمومی و بلاگ (UI ~10%) +> **اولویت**: پایین | **ریسک**: پایین | **حجم**: ۷ فایل + +| # | تسک | تغییرات | +|---|---|---| +| 5.1 | `Index.razor` → حذف inline styles از hero، استفاده از CSS class | UI | +| 5.2 | `About.razor` → حذف inline radial-gradient | UI | +| 5.3 | `Contact.razor` → cleanup minor inline styles | UI | +| 5.4 | `FAQ.razor` → cleanup minor inline styles | UI | +| 5.5 | `Blog/Index` → حذف inline hero styles، استفاده از CSS class | UI | +| 5.6 | `Blog/Post` → حذف inline styles از hero image و typography | UI | +| 5.7 | `RegisterWizard` → بهینه‌سازی wizard-section dark mode | UI | + +--- + +### 🔷 فاز ۶ — Polish نهایی و فرآیندی (UX ~10%) +> **اولویت**: پایین | **ریسک**: بسیار پایین | **حجم**: ۴ فایل + تست + +| # | تسک | تغییرات | +|---|---|---| +| 6.1 | Fix routing inconsistency — Categories + Blog/Post | UX | +| 6.2 | MudSnackbar notifications styling | UI | +| 6.3 | Dialog styling consistency (AuthDialog, AddressDialogs) | UI | +| 6.4 | Micro-interactions — button press, card hover, page transition | UI | +| 6.5 | تست کامل Dark Mode در تمام صفحات | UI + QA | +| 6.6 | تست Mobile Responsive در تمام صفحات | UX + QA | + +--- + +## 📈 بخش ۴: جدول تأثیرگذاری + +### تأثیر UI (هدف ~۵۰٪ تغییر) + +| حوزه | تعداد فایل | درصد تأثیر | +|---|---|---| +| Theme + CSS Variables | ۲ | ۱۵% (تأثیر سراسری) | +| حذف Inline Styles | ۱۱ | ۱۵% | +| یکسان‌سازی Elevation/Rounding | ۲۰+ | ۱۰% | +| بهبود رنگ‌بندی (CSS Variables) | ۸ | ۵% | +| Micro-interactions | سراسری | ۵% | +| **جمع** | | **~۵۰%** | + +### تأثیر UX (هدف ~۲۰٪ تغییر) + +| حوزه | تعداد فایل | درصد تأثیر | +|---|---|---| +| PageHeader یکپارچه | ۱۲ صفحه | ۸% | +| LoadingState واحد | ۱۵+ صفحه | ۴% | +| EmptyState واحد | ۸+ صفحه | ۳% | +| Container fixes (PackageDetail) | ۲ | ۲% | +| Routing consistency | ۲ | ۱% | +| Flow improvements | ۲ | ۲% | +| **جمع** | | **~۲۰%** | + +--- + +## 🔧 بخش ۵: مشخصات فنی کامپوننت‌های جدید + +### ۵.۱ LoadingState Component + +```razor +@* Shared/LoadingState.razor *@ + + + @if (!string.IsNullOrWhiteSpace(Message)) + { + @Message + } + + +@code { + [Parameter] public string Message { get; set; } = "در حال بارگذاری..."; +} +``` + +### ۵.۲ EmptyState Component + +```razor +@* Shared/EmptyState.razor *@ + + + @Title + @if (!string.IsNullOrWhiteSpace(Description)) + { + + @Description + + } + @if (!string.IsNullOrWhiteSpace(ActionText)) + { + + @ActionText + + } + + +@code { + [Parameter] public string Icon { get; set; } = Icons.Material.Filled.Inbox; + [Parameter] public string Title { get; set; } = "موردی یافت نشد"; + [Parameter] public string? Description { get; set; } + [Parameter] public string? ActionText { get; set; } + [Parameter] public string? ActionHref { get; set; } + [Parameter] public EventCallback OnAction { get; set; } +} +``` + +### ۵.۳ PageHeader ارتقاء‌یافته + +```razor +@* Shared/PageHeader.razor — ارتقاء‌یافته *@ + + +@code { + [Parameter] public string Title { get; set; } = ""; + [Parameter] public string? Subtitle { get; set; } + [Parameter] public string? BackHref { get; set; } + + [Inject] private IJSRuntime JS { get; set; } = default!; + private async Task GoBack() => await JS.InvokeVoidAsync("history.back"); +} +``` + +--- + +## ✅ بخش ۶: چک‌لیست تکمیل هر فاز + +### فاز ۱ چک‌لیست: +- [ ] `CustomMudTheme.cs` — Primary→`#6366f1`, PaletteDark اضافه شد +- [ ] `site.css` — CSS Variables جدید (brand, semantic, soft, gradient) +- [ ] `Shared/LoadingState.razor` — ساخته و تست شد +- [ ] `Shared/EmptyState.razor` — ساخته و تست شد +- [ ] `Shared/PageHeader.razor` — Subtitle parameter اضافه شد +- [ ] `.page-container` CSS pattern اضافه شد +- [ ] Build: 0 errors ✅ +- [ ] Dark Mode: صحیح ✅ +- [ ] Mobile: صحیح ✅ + +### فاز ۲ چک‌لیست: +- [ ] `Profile/Personal` → `` +- [ ] `Profile/Addresses` → `` + `` + `` +- [ ] `Profile/Wallet` → `` +- [ ] `Profile/Settings` → `` +- [ ] `Profile/Tree` → `` +- [ ] `Profile/WithdrawalRequests` → `` + `` +- [ ] `Profile/ChangePassword` → `` +- [ ] `Profile/Index` → inline styles → CSS +- [ ] `Club/*` → `` + `` +- [ ] Build: 0 errors ✅ + +### فاز ۳ چک‌لیست: +- [ ] `Commission/*` → `` + `` +- [ ] `Network/*` → `` + `` +- [ ] `PackageDetail` → MudContainer wrapper برای loading/error +- [ ] `Checkout` → Elevation fix + inline cleanup +- [ ] `WeeklyBalance` → inline gradient → CSS class +- [ ] Build: 0 errors ✅ + +### فاز ۴ چک‌لیست: +- [ ] `Store/Products` → hero inline → CSS +- [ ] `Store/ProductDetail` → image inline → CSS class +- [ ] `DiscountStore/ProductDetail` → rgba → variable +- [ ] `Gateway/*` → hardcoded → variable +- [ ] Elevation یکسان‌سازی +- [ ] Container spacing یکسان‌سازی +- [ ] Build: 0 errors ✅ + +### فاز ۵ چک‌لیست: +- [ ] `Index.razor` → hero inline cleanup +- [ ] `About.razor` → radial-gradient cleanup +- [ ] `Blog/Index` + `Blog/Post` → inline cleanup +- [ ] `Contact.razor` + `FAQ.razor` → minor cleanup +- [ ] Build: 0 errors ✅ + +### فاز ۶ چک‌لیست: +- [ ] Routing fix (Categories, Blog/Post) +- [ ] MudSnackbar styling +- [ ] Dialog consistency +- [ ] Dark mode full test +- [ ] Mobile responsive full test +- [ ] Build: 0 errors ✅ + +--- + +## 📋 بخش ۷: خلاصه تغییرات در یک نگاه + +``` +فایل‌های تغییر‌یافته: +├── CustomMudTheme.cs [فاز ۱] — ارتقاء کامل تم +├── site.css [فاز ۱-۵] — CSS Variables + class‌های جدید +├── Shared/LoadingState.razor [فاز ۱] — جدید +├── Shared/EmptyState.razor [فاز ۱] — جدید +├── Shared/PageHeader.razor [فاز ۱] — بهبود +├── Profile/* (۷ فایل) [فاز ۲] — PageHeader + LoadingState +├── Profile/Index.razor [فاز ۲] — حذف inline styles +├── Club/* (۲ فایل) [فاز ۲] — PageHeader + LoadingState +├── Commission/* (۲ فایل) [فاز ۳] — PageHeader + LoadingState + cleanup +├── Network/* (۱ فایل) [فاز ۳] — PageHeader + LoadingState +├── PackageDetail.razor [فاز ۳] — Container fix +├── Checkout.razor [فاز ۳] — Elevation + cleanup +├── Store/* (۲ فایل) [فاز ۴] — inline → CSS +├── DiscountStore/ProductDetail [فاز ۴] — rgba → variable +├── Gateway/* (۳ فایل) [فاز ۴] — hardcoded → variable +├── Index.razor [فاز ۵] — hero cleanup +├── About.razor [فاز ۵] — gradient cleanup +├── Blog/* (۲ فایل) [فاز ۵] — inline cleanup +├── Contact.razor + FAQ.razor [فاز ۵] — minor cleanup +└── Categories + Blog/Post [فاز ۶] — routing fix +``` + +**مجموع فایل‌های تأثیرپذیر**: ~۳۵ فایل +**فایل‌های جدید**: ۲ (LoadingState, EmptyState) +**میزان تغییر UI**: ~۵۰٪ +**میزان تغییر UX**: ~۲۰٪ +**ریسک شکست**: پایین (تغییرات تدریجی، build verification در هر فاز) diff --git a/src/FrontOffice.Main/ConfigureServices.cs b/src/FrontOffice.Main/ConfigureServices.cs index 76ab52e..0f1c61a 100644 --- a/src/FrontOffice.Main/ConfigureServices.cs +++ b/src/FrontOffice.Main/ConfigureServices.cs @@ -21,6 +21,13 @@ using CMSMicroservice.Protobuf.Protos.Configuration; using CMSMicroservice.Protobuf.Protos.NetworkMembership; using CMSMicroservice.Protobuf.Protos.Commission; using CMSMicroservice.Protobuf.Protos.AppVersion; +using CMSMicroservice.Protobuf.Protos.SitePage; +using CMSMicroservice.Protobuf.Protos.BlogPost; +using CMSMicroservice.Protobuf.Protos.BlogCategory; +using CMSMicroservice.Protobuf.Protos.DiscountProduct; +using CMSMicroservice.Protobuf.Protos.DiscountCategory; +using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart; +using CMSMicroservice.Protobuf.Protos.DiscountOrder; using FrontOffice.Main.Utilities; namespace Microsoft.Extensions.DependencyInjection; @@ -66,6 +73,16 @@ public static class ConfigureServices services.AddScoped(); // Main Service for core functionality services.AddScoped(); + // Site Page service for dynamic content (About, Contact) + services.AddScoped(); + // Blog Post service for landing page latest posts & blog pages + services.AddScoped(); + // Blog Category service for blog sidebar filters + services.AddScoped(); + // Discount Store services + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); // Device detection: very light, dependency-free services.AddTransient(); // PDF generation (Chromium only) @@ -115,6 +132,17 @@ public static class ConfigureServices services.AddScoped(CreateAuthenticatedClient); services.AddScoped(CreateAuthenticatedClient); services.AddScoped(CreateAuthenticatedClient); + services.AddScoped(CreateAuthenticatedClient); + services.AddScoped(CreateAuthenticatedClient); + services.AddScoped(CreateAuthenticatedClient); + // Image Resolver Service + services.AddScoped(CreateAuthenticatedClient); + services.AddScoped(); + // Discount Store gRPC clients + services.AddScoped(CreateAuthenticatedClient); + services.AddScoped(CreateAuthenticatedClient); + services.AddScoped(CreateAuthenticatedClient); + services.AddScoped(CreateAuthenticatedClient); return services; } diff --git a/src/FrontOffice.Main/Pages/About.razor b/src/FrontOffice.Main/Pages/About.razor index b6ddbc0..57485ba 100644 --- a/src/FrontOffice.Main/Pages/About.razor +++ b/src/FrontOffice.Main/Pages/About.razor @@ -1,192 +1,280 @@ @attribute [Route(RouteConstants.About.Index)] @inject NavigationManager Navigation +@inject SitePageService SitePageService درباره ما | کارا بازار سلامت - -
- - - - درباره کارا بازار سلامت - - - پلتفرم هوشمند تیم‌سازی و مدیریت فروش - - - ما با ارائه ابزارهای نوآورانه، به کسب‌وکارها کمک می‌کنیم تا تیم‌های فروش خود را گسترش دهند و به صورت کارآمد مدیریت کنند. - - - - شروع کنید - - - تماس با ما - - - - - - - - - - +@if (_loading) +{ + + -
- - - - -
+} +else +{ + +
- چشم‌انداز و مأموریت ما - + - - - چشم‌انداز - - تبدیل شدن به پیشروترین پلتفرم مدیریت تیم‌های فروش در منطقه، با ارائه راهکارهای هوشمند و کاربرپسند برای کسب‌وکارهای کوچک و بزرگ. + درباره کارا بازار سلامت + + + @(_page?.HeroTitle ?? "پلتفرم هوشمند تیم‌سازی و مدیریت فروش") - + + @(_page?.HeroSubtitle ?? "ما با ارائه ابزارهای نوآورانه، به کسب‌وکارها کمک می‌کنیم تا تیم‌های فروش خود را گسترش دهند و به صورت کارآمد مدیریت کنند.") + + + + شروع کنید + + + تماس با ما + + + - - - مأموریت - - توانمندسازی کسب‌وکارها از طریق فناوری‌های نوین، ساده‌سازی فرآیندهای پیچیده و ایجاد کارا بازار سلامت‌های جدید برای رشد و توسعه پایدار. - + +
- -
- - ارزش‌های ما - - - - - صداقت و شفافیت - - در تمامی تعاملات خود با مشتریان و همکاران، صداقت و شفافیت را سرلوحه کار خود قرار داده‌ایم. - - - - - - - نوآوری - - همواره در پی یافتن راهکارهای جدید و بهبود فرآیندها هستیم تا بهترین تجربه را برای کاربران فراهم کنیم. - - - - - - - مشتری‌مداری - - رضایت و موفقیت مشتریان اولویت اصلی ما است و تمامی تصمیمات خود را بر اساس نیازهای آنها می‌گیریم. - - - - - - - کیفیت - - به کیفیت محصولات و خدمات خود افتخار می‌کنیم و همواره استانداردهای بالایی را رعایت می‌کنیم. - - - - - - - پاسخگویی - - به سرعت به نیازهای مشتریان پاسخ می‌دهیم و پشتیبانی ۲۴ ساعته را فراهم کرده‌ایم. - - - - - - - پایداری - - به دنبال ایجاد ارزش بلندمدت برای مشتریان و جامعه هستیم و به توسعه پایدار متعهد هستیم. - - - - - -
+ - -
- - تیم ما - - - - - - - علی رضایی - مدیرعامل - - بیش از ۱۰ سال تجربه در حوزه فناوری و مدیریت کسب‌وکار - - - - - - - - - مریم احمدی - مدیر محصول - - متخصص در طراحی تجربه کاربری و توسعه محصول - - - - - - - - - حسن کریمی - مدیر فنی - - مهندس نرم‌افزار با تخصص در معماری سیستم‌های توزیع‌شده - - - - - -
-
+ +
+ + چشم‌انداز و مأموریت ما + + + + + + @(_visionSection?.Title ?? "چشم‌انداز") + + @if (!string.IsNullOrWhiteSpace(_visionSection?.HtmlContent)) + { + + @((MarkupString)_visionSection.HtmlContent) + + } + else + { + + تبدیل شدن به پیشروترین پلتفرم مدیریت تیم‌های فروش در منطقه، با ارائه راهکارهای هوشمند و کاربرپسند برای کسب‌وکارهای کوچک و بزرگ. + + } + + + + + + + @(_missionSection?.Title ?? "مأموریت") + + @if (!string.IsNullOrWhiteSpace(_missionSection?.HtmlContent)) + { + + @((MarkupString)_missionSection.HtmlContent) + + } + else + { + + توانمندسازی کسب‌وکارها از طریق فناوری‌های نوین، ساده‌سازی فرآیندهای پیچیده و ایجاد کارا بازار سلامت‌های جدید برای رشد و توسعه پایدار. + + } + + + + +
+ + +
+ + ارزش‌های ما + + @if (_valueSections.Any()) + { + @foreach (var value in _valueSections) + { + + + @if (!string.IsNullOrWhiteSpace(value.IconName)) + { + + } + @value.Title + @if (!string.IsNullOrWhiteSpace(value.HtmlContent)) + { + + @((MarkupString)value.HtmlContent) + + } + + + } + } + else + { + @* Fallback: hardcoded values *@ + + + + صداقت و شفافیت + + در تمامی تعاملات خود با مشتریان و همکاران، صداقت و شفافیت را سرلوحه کار خود قرار داده‌ایم. + + + + + + + نوآوری + + همواره در پی یافتن راهکارهای جدید و بهبود فرآیندها هستیم تا بهترین تجربه را برای کاربران فراهم کنیم. + + + + + + + مشتری‌مداری + + رضایت و موفقیت مشتریان اولویت اصلی ما است و تمامی تصمیمات خود را بر اساس نیازهای آنها می‌گیریم. + + + + + + + کیفیت + + به کیفیت محصولات و خدمات خود افتخار می‌کنیم و همواره استانداردهای بالایی را رعایت می‌کنیم. + + + + + + + پاسخگویی + + به سرعت به نیازهای مشتریان پاسخ می‌دهیم و پشتیبانی ۲۴ ساعته را فراهم کرده‌ایم. + + + + + + + پایداری + + به دنبال ایجاد ارزش بلندمدت برای مشتریان و جامعه هستیم و به توسعه پایدار متعهد هستیم. + + + + } + + +
+ + +
+ + تیم ما + + @if (_teamSections.Any()) + { + @foreach (var member in _teamSections) + { + + + + @if (!string.IsNullOrWhiteSpace(member.ImagePath)) + { + + } + else + { + + } + + @member.Title + @if (!string.IsNullOrWhiteSpace(member.Subtitle)) + { + @member.Subtitle + } + @if (!string.IsNullOrWhiteSpace(member.HtmlContent)) + { + + @((MarkupString)member.HtmlContent) + + } + + + } + } + else + { + @* Fallback: hardcoded team *@ + + + + + + علی رضایی + مدیرعامل + + بیش از ۱۰ سال تجربه در حوزه فناوری و مدیریت کسب‌وکار + + + + + + + + + مریم احمدی + مدیر محصول + + متخصص در طراحی تجربه کاربری و توسعه محصول + + + + + + + + + حسن کریمی + مدیر فنی + + مهندس نرم‌افزار با تخصص در معماری سیستم‌های توزیع‌شده + + + + } + + +
+ +} diff --git a/src/FrontOffice.Main/Pages/About.razor.cs b/src/FrontOffice.Main/Pages/About.razor.cs index 59858ec..af24e4b 100644 --- a/src/FrontOffice.Main/Pages/About.razor.cs +++ b/src/FrontOffice.Main/Pages/About.razor.cs @@ -1,8 +1,37 @@ +using FrontOffice.Main.Utilities; + namespace FrontOffice.Main.Pages; + public partial class About { - private void ScrollToContact() + private bool _loading = true; + private SitePageDto? _page; + private SitePageSectionDto? _visionSection; + private SitePageSectionDto? _missionSection; + private List _valueSections = new(); + private List _teamSections = new(); + + protected override async Task OnInitializedAsync() { - // TODO: Implement smooth scroll to contact section + try + { + _page = await SitePageService.GetByKeyAsync("about"); + + if (_page != null) + { + _visionSection = _page.GetSection("vision"); + _missionSection = _page.GetSection("mission"); + _valueSections = _page.GetSections("value-").ToList(); + _teamSections = _page.GetSections("team-").ToList(); + } + } + catch + { + // Fallback: page remains null → hardcoded content will render + } + finally + { + _loading = false; + } } } \ No newline at end of file diff --git a/src/FrontOffice.Main/Pages/Blog/Index.razor b/src/FrontOffice.Main/Pages/Blog/Index.razor new file mode 100644 index 0000000..3dc07b7 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Blog/Index.razor @@ -0,0 +1,172 @@ +@attribute [Route(RouteConstants.Blog.Index)] + +بلاگ | کارا بازار سلامت + +@* ═══════════════════════════════════════════════ + BLOG HEADER + ═══════════════════════════════════════════════ *@ +
+ +
+ بلاگ + + آخرین مطالب، آموزش‌ها و اخبار کارا بازار سلامت + +
+
+
+ +@* ═══════════════════════════════════════════════ + SEARCH + FILTERS + ═══════════════════════════════════════════════ *@ + + + + + + + + + + @foreach (var cat in _categories) + { + + @cat.Title + @if (cat.PostCount > 0) + { + (@cat.PostCount) + } + + } + + + + + جستجو + + + + + + @* ═══════════════════════════════════════════════ + BLOG POSTS GRID + ═══════════════════════════════════════════════ *@ + @if (_isLoading) + { + + } + else if (!_posts.Any()) + { + + + مقاله‌ای یافت نشد + + @if (!string.IsNullOrWhiteSpace(_searchTerm) || _selectedCategoryId.HasValue) + { + فیلترها را تغییر دهید یا عبارت دیگری جستجو کنید. + } + else + { + به‌زودی مقالات جدید منتشر خواهد شد. + } + + + } + else + { + + @foreach (var post in _posts) + { + + + @* Thumbnail *@ +
+ @if (!string.IsNullOrWhiteSpace(post.ThumbnailUrl)) + { + + } + else + { +
+ +
+ } +
+ + @* Content *@ +
+ @if (post.Categories.Any()) + { + + @post.Categories.First().Title + + } + + + @post.Title + + + @if (!string.IsNullOrWhiteSpace(post.Summary)) + { + + @post.Summary + + } + + + @if (post.PublishedAt.HasValue) + { + + + @post.PublishedAt.Value.ToString("yyyy/MM/dd") + + } + + + + @post.ViewCount.ToString("N0") + + +
+
+
+ } +
+ + @* ── Pagination ── *@ + @if (_totalPages > 1) + { +
+ +
+ } + } + +
diff --git a/src/FrontOffice.Main/Pages/Blog/Index.razor.cs b/src/FrontOffice.Main/Pages/Blog/Index.razor.cs new file mode 100644 index 0000000..271b4c8 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Blog/Index.razor.cs @@ -0,0 +1,91 @@ +using FrontOffice.Main.Utilities; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; + +namespace FrontOffice.Main.Pages.Blog; + +public partial class Index +{ + [Inject] private BlogPostService BlogPostService { get; set; } = default!; + [Inject] private BlogCategoryService BlogCategoryService { get; set; } = default!; + + // ── State ── + private List _posts = new(); + private List _categories = new(); + private string? _searchTerm; + private long? _selectedCategoryId; + private int _currentPage = 1; + private int _totalPages; + private bool _isLoading = true; + private const int PageSize = 9; + + protected override async Task OnInitializedAsync() + { + // Load categories and posts in parallel + var categoriesTask = BlogCategoryService.GetActiveCategoriesAsync(); + var postsTask = LoadPostsAsync(); + + _categories = await categoriesTask; + await postsTask; + } + + private async Task LoadPostsAsync() + { + _isLoading = true; + StateHasChanged(); + + try + { + var result = await BlogPostService.GetPublishedPostsAsync( + page: _currentPage, + pageSize: PageSize, + search: _searchTerm, + categoryId: _selectedCategoryId); + + _posts = result.Posts; + _totalPages = result.TotalPages; + } + catch + { + _posts = new(); + _totalPages = 0; + } + finally + { + _isLoading = false; + StateHasChanged(); + } + } + + private async Task SearchPosts() + { + _currentPage = 1; + await LoadPostsAsync(); + } + + private async Task OnSearchKeyUp(KeyboardEventArgs e) + { + if (e.Key == "Enter") + { + await SearchPosts(); + } + } + + private async Task OnCategoryChanged(long? categoryId) + { + _selectedCategoryId = categoryId; + _currentPage = 1; + await LoadPostsAsync(); + } + + private async Task OnPageChanged(int page) + { + _currentPage = page; + await LoadPostsAsync(); + } + + private void NavigateToPost(string slug) + { + Navigation.NavigateTo($"/blog/{slug}"); + } +} diff --git a/src/FrontOffice.Main/Pages/Blog/Post.razor b/src/FrontOffice.Main/Pages/Blog/Post.razor new file mode 100644 index 0000000..0878173 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Blog/Post.razor @@ -0,0 +1,122 @@ +@page "/blog/{Slug}" + +@(_post?.Title ?? "مقاله") | بلاگ + +@if (_isLoading) +{ + + + +} +else if (_post == null) +{ + + + +} +else +{ + @* ═══ Featured Image Header ═══ *@ + @if (!string.IsNullOrWhiteSpace(_post.FeaturedImagePath)) + { +
+ +
+
+ } + + + @* ── Breadcrumb ── *@ + + خانه + + بلاگ + + @_post.Title + + + @* ── Article Header ── *@ + + @if (_post.Categories.Any()) + { + + @foreach (var cat in _post.Categories) + { + + @cat.Title + + } + + } + + + @_post.Title + + + @if (!string.IsNullOrWhiteSpace(_post.Summary)) + { + + @_post.Summary + + } + + + @if (_post.PublishedAt.HasValue) + { + + + + @_post.PublishedAt.Value.ToString("yyyy/MM/dd") + + + } + + + + @_post.ViewCount.ToString("N0") بازدید + + + + + + + + @* ── Article Content ── *@ + + @((MarkupString)_post.HtmlContent) + + + @* ── Tags ── *@ + @if (_post.Tags.Any()) + { + + + + @foreach (var tag in _post.Tags) + { + + @tag.Title + + } + + } + + @* ── Back to Blog ── *@ +
+ + بازگشت به بلاگ + +
+
+} diff --git a/src/FrontOffice.Main/Pages/Blog/Post.razor.cs b/src/FrontOffice.Main/Pages/Blog/Post.razor.cs new file mode 100644 index 0000000..3a4240b --- /dev/null +++ b/src/FrontOffice.Main/Pages/Blog/Post.razor.cs @@ -0,0 +1,51 @@ +using FrontOffice.Main.Utilities; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; + +namespace FrontOffice.Main.Pages.Blog; + +public partial class Post +{ + [Inject] private IJSRuntime JS { get; set; } = default!; + [Parameter] public string Slug { get; set; } = string.Empty; + + [Inject] private BlogPostService BlogPostService { get; set; } = default!; + + private BlogPostDetailDto? _post; + private bool _isLoading = true; + + protected override async Task OnParametersSetAsync() + { + if (string.IsNullOrWhiteSpace(Slug)) return; + + _isLoading = true; + StateHasChanged(); + + try + { + _post = await BlogPostService.GetBySlugAsync(Slug); + + // Fire-and-forget: increment view count + if (_post != null) + { + _ = BlogPostService.IncrementViewCountAsync(_post.Id); + } + } + catch + { + _post = null; + } + finally + { + _isLoading = false; + StateHasChanged(); + } + } + + private void NavigateToCategoryFilter(long categoryId) + { + Navigation.NavigateTo($"/blog?category={categoryId}"); + } + + private async Task GoBack() => await JS.InvokeVoidAsync("history.back"); +} diff --git a/src/FrontOffice.Main/Pages/Checkout.razor b/src/FrontOffice.Main/Pages/Checkout.razor index 0c3e800..6662eb7 100644 --- a/src/FrontOffice.Main/Pages/Checkout.razor +++ b/src/FrontOffice.Main/Pages/Checkout.razor @@ -5,10 +5,11 @@ تکمیل خرید + - + @@ -23,7 +24,7 @@ - + جزئیات پکیج @if (_selectedPackage != null) @@ -33,9 +34,9 @@ - - + انتخاب آدرس @if (_isLoadingAddresses) @@ -149,7 +150,7 @@ - + کد تخفیف @if (_selectedPackage != null) { - + خلاصه سفارش diff --git a/src/FrontOffice.Main/Pages/Checkout.razor.cs b/src/FrontOffice.Main/Pages/Checkout.razor.cs index 6a4813e..b132c8d 100644 --- a/src/FrontOffice.Main/Pages/Checkout.razor.cs +++ b/src/FrontOffice.Main/Pages/Checkout.razor.cs @@ -59,7 +59,7 @@ public partial class Checkout Id: response.Id, Title: response.Title, Body: response.Description, - Image: UrlUtility.DownloadUrl + response.ImagePath, + Image: response.ImagePath ?? string.Empty, Price: response.Price ); _finalPrice = _selectedPackage.Price; diff --git a/src/FrontOffice.Main/Pages/Club/FeaturesPage.razor b/src/FrontOffice.Main/Pages/Club/FeaturesPage.razor index 8e9768b..3e4bc21 100644 --- a/src/FrontOffice.Main/Pages/Club/FeaturesPage.razor +++ b/src/FrontOffice.Main/Pages/Club/FeaturesPage.razor @@ -3,15 +3,13 @@ @using MudBlazor @inject ClubConfigurationService ClubConfigService @inject IDialogService DialogService +@inject IJSRuntime JS ویژگی‌های باشگاه مشتریان - - ویژگی‌های باشگاه مشتریان - بازگشت - + @@ -30,13 +28,13 @@ @if (_isLoading) { - + } else if (_features == null || !_features.Any()) { - - هنوز ویژگی‌ای برای شما فعال نشده است. - + } else { @@ -73,7 +71,7 @@ FullWidth="true" Size="Size.Large" StartIcon="@Icons.Material.Filled.ArrowBack" - Href="@RouteConstants.Club.Membership"> + OnClick="GoBack"> بازگشت به صفحه باشگاه @@ -190,4 +188,6 @@ _selectedFeature = feature; _showDetailDialog = true; } + + private async Task GoBack() => await JS.InvokeVoidAsync("history.back"); } diff --git a/src/FrontOffice.Main/Pages/Club/MembershipPage.razor b/src/FrontOffice.Main/Pages/Club/MembershipPage.razor index 55784f1..a788171 100644 --- a/src/FrontOffice.Main/Pages/Club/MembershipPage.razor +++ b/src/FrontOffice.Main/Pages/Club/MembershipPage.razor @@ -7,14 +7,11 @@ - - عضویت باشگاه مشتریان - بازگشت - + @if (_isLoading) { - + } else if (_membership is not null) { diff --git a/src/FrontOffice.Main/Pages/Commission/CommissionDashboardPage.razor b/src/FrontOffice.Main/Pages/Commission/CommissionDashboardPage.razor index c9bf44a..bd0dda9 100644 --- a/src/FrontOffice.Main/Pages/Commission/CommissionDashboardPage.razor +++ b/src/FrontOffice.Main/Pages/Commission/CommissionDashboardPage.razor @@ -8,10 +8,7 @@ - - پاداش‌های من - بازگشت - + @@ -52,7 +49,7 @@ @if (_isLoading) { - + } else { diff --git a/src/FrontOffice.Main/Pages/Commission/WeeklyBalancePage.razor b/src/FrontOffice.Main/Pages/Commission/WeeklyBalancePage.razor index 1dc2cfe..0e7d9cb 100644 --- a/src/FrontOffice.Main/Pages/Commission/WeeklyBalancePage.razor +++ b/src/FrontOffice.Main/Pages/Commission/WeeklyBalancePage.razor @@ -7,10 +7,7 @@ - - گزارش هفتگی پاداش - بازگشت - + @@ -50,7 +47,7 @@ @if (_isLoading) { - + } else if (_weeklyBalance is not null) { @@ -76,11 +73,11 @@ - + تیم دوم - @_weeklyBalance.RightBalanceFormatted + @_weeklyBalance.RightBalanceFormatted - + تیم اول - @_weeklyBalance.LeftBalanceFormatted + @_weeklyBalance.LeftBalanceFormatted - محاسبات پاداش + محاسبات پاداش diff --git a/src/FrontOffice.Main/Pages/Contact.razor b/src/FrontOffice.Main/Pages/Contact.razor index c98a984..ed61fa4 100644 --- a/src/FrontOffice.Main/Pages/Contact.razor +++ b/src/FrontOffice.Main/Pages/Contact.razor @@ -1,4 +1,5 @@ @attribute [Route(RouteConstants.Contact.Index)] +@inject SitePageService SitePageService ارتباط با ما | کارا بازار سلامت @@ -8,10 +9,10 @@ ارتباط با ما - آماده شنیدن صدای شما هستیم + @(_page?.HeroTitle ?? "آماده شنیدن صدای شما هستیم") - سوالات، پیشنهادات یا انتقادات خود را با ما در میان بگذارید. تیم ما آماده پاسخگویی به شماست. + @(_page?.HeroSubtitle ?? "سوالات، پیشنهادات یا انتقادات خود را با ما در میان بگذارید. تیم ما آماده پاسخگویی به شماست.") @@ -25,7 +26,7 @@ - + فرم تماس @@ -72,12 +73,12 @@ Variant="Variant.Outlined" Required="true" RequiredError="انتخاب موضوع الزامی است."> - عمومی - پشتیبانی فنی - فروش و قیمت‌گذاری - شریک تجاری - شکایت - پیشنهاد + عمومی + پشتیبانی فنی + فروش و قیمت‌گذاری + شریک تجاری + شکایت + پیشنهاد @@ -123,7 +124,7 @@ - + اطلاعات تماس @@ -132,7 +133,7 @@
آدرس - کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶ + @(_contactAddress ?? "کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶")
@@ -141,7 +142,7 @@
تلفن - 026-34233563 + @(_contactPhone ?? "026-34233563")
@@ -149,7 +150,7 @@
ایمیل - info@kbs1.co + @(_contactEmail ?? "info@kbs1.co")
@@ -158,8 +159,7 @@
ساعات کاری - شنبه تا پنج‌شنبه
- ۹ صبح تا ۶ عصر + @((MarkupString)(_contactHours ?? "شنبه تا پنج‌شنبه
۹ صبح تا ۶ عصر"))
@@ -167,14 +167,14 @@
- + شبکه‌های اجتماعی تلگرام @@ -183,7 +183,7 @@ اینستاگرام @@ -192,7 +192,7 @@ لینکدین @@ -201,7 +201,7 @@ واتس‌اپ @@ -219,14 +219,14 @@ موقعیت مکانی - + -
+
نقشه موقعیت مکانی - کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶ + @(_contactAddress ?? "کرج مهرویلا میدان مادر ساختمان بزرگمهر طبقه ۴ واحد ۱۶")
diff --git a/src/FrontOffice.Main/Pages/Contact.razor.cs b/src/FrontOffice.Main/Pages/Contact.razor.cs index 4c81289..d44c34c 100644 --- a/src/FrontOffice.Main/Pages/Contact.razor.cs +++ b/src/FrontOffice.Main/Pages/Contact.razor.cs @@ -1,8 +1,10 @@ using FluentValidation; +using FrontOffice.Main.Utilities; using MudBlazor; using Severity = MudBlazor.Severity; namespace FrontOffice.Main.Pages; + public partial class Contact { private ContactForm _contactForm = new(); @@ -10,6 +12,60 @@ public partial class Contact private bool _isSubmitting; private readonly ContactFormValidator _contactFormValidator = new(); + // Dynamic CMS data + private SitePageDto? _page; + private string? _contactAddress; + private string? _contactPhone; + private string? _contactEmail; + private string? _contactHours; + private string? _socialTelegram; + private string? _socialInstagram; + private string? _socialLinkedin; + private string? _socialWhatsapp; + + protected override async Task OnInitializedAsync() + { + try + { + _page = await SitePageService.GetByKeyAsync("contact"); + + if (_page != null) + { + // Load contact info from CMS section + var contactInfoSection = _page.GetSection("contact-info"); + if (contactInfoSection != null) + { + var contactData = contactInfoSection.GetExtraData(); + if (contactData != null) + { + _contactAddress = contactData.Address; + _contactPhone = contactData.Phone; + _contactEmail = contactData.Email; + _contactHours = contactData.Hours; + } + } + + // Load social media links from CMS section + var socialSection = _page.GetSection("social-media"); + if (socialSection != null) + { + var socialData = socialSection.GetExtraData(); + if (socialData != null) + { + _socialTelegram = socialData.Telegram; + _socialInstagram = socialData.Instagram; + _socialLinkedin = socialData.Linkedin; + _socialWhatsapp = socialData.Whatsapp; + } + } + } + } + catch + { + // Fallback: all values remain null → hardcoded defaults will render + } + } + private async Task SubmitContactForm() { if (_form is null) return; @@ -55,14 +111,12 @@ public partial class Contact private void CallSupport() { - // TODO: Initiate phone call or show phone number - Snackbar.Add("شماره تماس: ۰۲۱-۱۲۳۴۵۶۷۸", Severity.Info); + Snackbar.Add($"شماره تماس: {_contactPhone ?? "026-34233563"}", Severity.Info); } private void SendEmail() { - // TODO: Open email client or redirect to email page - Snackbar.Add("ایمیل: info@kbs1.co", Severity.Info); + Snackbar.Add($"ایمیل: {_contactEmail ?? "info@kbs1.co"}", Severity.Info); } public class ContactForm @@ -87,4 +141,21 @@ public partial class Contact RuleFor(x => x.Message).NotEmpty().MinimumLength(10).WithMessage("پیام باید حداقل ۱۰ کاراکتر باشد"); } } + + // DTO classes for ExtraData JSON deserialization + private class ContactInfoData + { + public string? Address { get; set; } + public string? Phone { get; set; } + public string? Email { get; set; } + public string? Hours { get; set; } + } + + private class SocialMediaData + { + public string? Telegram { get; set; } + public string? Instagram { get; set; } + public string? Linkedin { get; set; } + public string? Whatsapp { get; set; } + } } \ No newline at end of file diff --git a/src/FrontOffice.Main/Pages/DiscountStore/Cart.razor b/src/FrontOffice.Main/Pages/DiscountStore/Cart.razor new file mode 100644 index 0000000..72f705b --- /dev/null +++ b/src/FrontOffice.Main/Pages/DiscountStore/Cart.razor @@ -0,0 +1,190 @@ +@attribute [Route(RouteConstants.DiscountStore.Cart)] + +سبد خرید تخفیفی + + + + + + @if (DiscountCart.Items.Count == 0) + { + سبد خرید تخفیفی شما خالی است. + + بازگشت به فروشگاه تخفیفی + + } + else + { + + + + + + محصول + قیمت واحد + سقف تخفیف + تعداد + تخفیف + قیمت نهایی + + + + + + + @context.Title + + + @FormatPrice(context.UnitPrice) + + + تا @context.MaxDiscountPercent% + + + + + + @context.Quantity + + + + + @if (context.DiscountAmount > 0) + { + @FormatPrice(context.DiscountAmount)- + } + else + { + + } + + @FormatPrice(context.FinalPrice) + + + + + + + + + + + + @foreach (var item in DiscountCart.Items) + { + + + + + + @item.Title + + + تا @item.MaxDiscountPercent% + + + + + + @FormatPrice(item.UnitPrice) واحد + + + @item.Quantity + + + + + @if (item.DiscountAmount > 0) + { + تخفیف: @FormatPrice(item.DiscountAmount)- + } + جمع: @FormatPrice(item.FinalPrice) + + + + + + } + + + + + @if (DeviceDetector.IsMobile()) + { + + + جمع کل: + @FormatPrice(DiscountCart.TotalPrice) تومان + + @if (DiscountCart.TotalDiscount > 0) + { + + سقف تخفیف قابل اعمال: + @FormatPrice(DiscountCart.TotalDiscount)- تومان + + } + + حداقل پرداخت: + @FormatPrice(DiscountCart.FinalPrice) تومان + + + + + + افزودن محصول + + ادامه خرید + + } + else + { + + + + جمع کل: + @FormatPrice(DiscountCart.TotalPrice) تومان + + @if (DiscountCart.TotalDiscount > 0) + { + + سقف تخفیف قابل اعمال: + @FormatPrice(DiscountCart.TotalDiscount)- تومان + + } + + + حداقل مبلغ پرداخت درگاه: + @FormatPrice(DiscountCart.FinalPrice) تومان + + + مبلغ دقیق پرداخت در مرحله تسویه مشخص خواهد شد. + + + + + + + افزودن محصول + + ادامه فرایند خرید + + } + } + + diff --git a/src/FrontOffice.Main/Pages/DiscountStore/Cart.razor.cs b/src/FrontOffice.Main/Pages/DiscountStore/Cart.razor.cs new file mode 100644 index 0000000..256e9cb --- /dev/null +++ b/src/FrontOffice.Main/Pages/DiscountStore/Cart.razor.cs @@ -0,0 +1,56 @@ +using Microsoft.AspNetCore.Components; +using FrontOffice.Main.Utilities; + +namespace FrontOffice.Main.Pages.DiscountStore; + +public partial class Cart : IDisposable +{ + [Inject] private DiscountCartService DiscountCart { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + await DiscountCart.EnsureInitializedAsync(); + DiscountCart.OnChange += StateHasChanged; + } + + private async Task IncrementQty(long productId) + { + var item = DiscountCart.Items.FirstOrDefault(i => i.ProductId == productId); + if (item is null) return; + await DiscountCart.UpdateQuantityAsync(productId, item.Quantity + 1); + } + + private async Task DecrementQty(long productId) + { + var item = DiscountCart.Items.FirstOrDefault(i => i.ProductId == productId); + if (item is null) return; + if (item.Quantity <= 1) + { + await DiscountCart.RemoveAsync(productId); + } + else + { + await DiscountCart.UpdateQuantityAsync(productId, item.Quantity - 1); + } + } + + private async Task Remove(long productId) + { + await DiscountCart.RemoveAsync(productId); + } + + private void ProceedCheckout() + { + Navigation.NavigateTo(RouteConstants.DiscountStore.Checkout); + } + + private static string FormatPrice(long price) => $"{price:N0} "; + + private static string GetImageUrl(string? imageUrl) + => string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/'); + + public void Dispose() + { + DiscountCart.OnChange -= StateHasChanged; + } +} diff --git a/src/FrontOffice.Main/Pages/DiscountStore/Checkout.razor b/src/FrontOffice.Main/Pages/DiscountStore/Checkout.razor new file mode 100644 index 0000000..41b664a --- /dev/null +++ b/src/FrontOffice.Main/Pages/DiscountStore/Checkout.razor @@ -0,0 +1,193 @@ +@using CMSMicroservice.Protobuf.Protos.UserAddress +@attribute [Route(RouteConstants.DiscountStore.Checkout)] + +تسویه حساب فروشگاه تخفیفی + + + + + + + + + انتخاب آدرس + + @if (_loadingAddresses) + { + + + در حال بارگذاری آدرس‌ها... + + } + else if (_addresses.Count == 0) + { + + هیچ آدرسی ثبت نشده است. لطفاً از بخش پروفایل آدرس خود را اضافه کنید. + + افزودن آدرس + } + else + { + + @foreach (var address in _addresses) + { + + + + @address.Title + @address.Address + + @if (address.IsDefault) + { + پیش‌فرض + } + + + } + + } + + + + + + + پرداخت از کیف تخفیفی + + + + + موجودی کیف تخفیفی شما: + + @FormatPrice(_discountBalance) تومان + + + + + سقف مجاز تخفیف روی این سفارش: + + @FormatPrice(MaxAllowedDiscount) تومان + + + + + + + مبلغی که از کیف تخفیفی کسر شود (باقیمانده از درگاه پرداخت می‌شود): + + + + استفاده از @FormatPrice(_discountBalanceToUse) تومان + + + + + @if (_discountBalanceToUse > 0) + { + + مبلغ @FormatPrice(_discountBalanceToUse) تومان از کیف تخفیفی کسر و + مبلغ @FormatPrice(GatewayAmount) تومان از درگاه پرداخت دریافت خواهد شد. + + } + + + + + + توضیحات سفارش + + + + + + + + خلاصه سفارش + + @if (DiscountCart.Items.Count == 0) + { + سبد خرید شما خالی است. + } + else + { + + @foreach (var item in DiscountCart.Items) + { + + + + + + @item.Title + @FormatPrice(item.FinalPrice) + + + + @item.Quantity × @FormatPrice(item.UnitPrice) + + @if (item.MaxDiscountPercent > 0) + { + تا @item.MaxDiscountPercent% تخفیف + } + + + + + } + + + + + + + جمع کل: + @FormatPrice(DiscountCart.TotalPrice) + + + @if (_discountBalanceToUse > 0) + { + + از کیف تخفیفی: + @FormatPrice(_discountBalanceToUse)- + + } + + + + + پرداخت از درگاه: + + @FormatPrice(GatewayAmount) تومان + + + + + مالیات بر ارزش افزوده صرفاً بر مبلغ درگاه محاسبه خواهد شد. + + + + + @(_placing ? "در حال ثبت سفارش..." : "ثبت و پرداخت سفارش") + + } + + + + diff --git a/src/FrontOffice.Main/Pages/DiscountStore/Checkout.razor.cs b/src/FrontOffice.Main/Pages/DiscountStore/Checkout.razor.cs new file mode 100644 index 0000000..a7fa826 --- /dev/null +++ b/src/FrontOffice.Main/Pages/DiscountStore/Checkout.razor.cs @@ -0,0 +1,146 @@ +using Microsoft.AspNetCore.Components; +using CMSMicroservice.Protobuf.Protos.UserAddress; +using FrontOffice.Main.Utilities; +using MudBlazor; + +namespace FrontOffice.Main.Pages.DiscountStore; + +public partial class Checkout +{ + [Inject] private DiscountCartService DiscountCart { get; set; } = default!; + [Inject] private DiscountOrderService DiscountOrderService { get; set; } = default!; + [Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!; + + private List _addresses = new(); + private CustomerAddressModel? _selectedAddress; + private bool _loadingAddresses; + + private long _discountBalance; + private long _discountBalanceToUse; + private string? _notes; + private bool _placing; + + /// + /// Maximum discount allowed based on cart items' MaxDiscountPercent + /// + private long MaxAllowedDiscount => DiscountCart.TotalDiscount; + + /// + /// Maximum the user can actually use = min(balance, allowed discount) + /// + private long MaxUsable => Math.Min(_discountBalance, MaxAllowedDiscount); + + /// + /// Amount to be charged via payment gateway + /// + private long GatewayAmount => DiscountCart.TotalPrice - _discountBalanceToUse; + + private bool CanPlaceOrder => DiscountCart.Items.Count > 0 && _selectedAddress is not null; + + protected override async Task OnInitializedAsync() + { + await DiscountCart.EnsureInitializedAsync(); + + var addressTask = LoadAddresses(); + var balanceTask = LoadDiscountBalance(); + await Task.WhenAll(addressTask, balanceTask); + + // Default: use maximum possible discount + _discountBalanceToUse = MaxUsable; + } + + private async Task LoadDiscountBalance() + { + try + { + var balances = await WalletService.GetBalancesAsync(); + _discountBalance = balances.DiscountBalance; + } + catch + { + _discountBalance = 0; + } + } + + private async Task LoadAddresses() + { + _loadingAddresses = true; + try + { + var response = await UserAddressContract.GetCustomerAddressesAsync(new()); + if (response?.Models?.Any() == true) + { + _addresses = response.Models.ToList(); + _selectedAddress = _addresses.FirstOrDefault(a => a.IsDefault) ?? _addresses.First(); + } + else + { + _addresses = new(); + } + } + catch (Exception ex) + { + Snackbar.Add($"خطا در بارگذاری آدرس‌ها: {ex.Message}", Severity.Error); + _addresses = new(); + } + finally + { + _loadingAddresses = false; + await InvokeAsync(StateHasChanged); + } + } + + private async Task PlaceOrder() + { + if (!CanPlaceOrder || _selectedAddress is null) + { + Snackbar.Add("لطفاً آدرس را انتخاب کنید.", Severity.Warning); + return; + } + + _placing = true; + try + { + // Clamp the discount balance to use + var discountToUse = Math.Min(_discountBalanceToUse, MaxUsable); + + var result = await DiscountOrderService.PlaceOrderAsync( + _selectedAddress.Id, + discountToUse, + _notes); + + if (!result.Success) + { + Snackbar.Add(result.Message, Severity.Error); + return; + } + + // If there's a gateway payment URL, redirect to it + if (!string.IsNullOrWhiteSpace(result.PaymentUrl) && result.GatewayAmount > 0) + { + Snackbar.Add("در حال انتقال به درگاه پرداخت...", Severity.Info); + await DiscountCart.ClearAsync(); + Navigation.NavigateTo(result.PaymentUrl, forceLoad: true); + return; + } + + // If fully paid via discount balance (no gateway needed) + await DiscountCart.ClearAsync(); + Snackbar.Add("سفارش با موفقیت ثبت شد!", Severity.Success); + Navigation.NavigateTo($"{RouteConstants.DiscountStore.OrderDetail}{result.OrderId}"); + } + catch (Exception ex) + { + Snackbar.Add($"خطا در ثبت سفارش: {ex.Message}", Severity.Error); + } + finally + { + _placing = false; + } + } + + private static string FormatPrice(long price) => $"{price:N0}"; + + private static string GetImageUrl(string? imageUrl) + => string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/'); +} diff --git a/src/FrontOffice.Main/Pages/DiscountStore/OrderDetail.razor b/src/FrontOffice.Main/Pages/DiscountStore/OrderDetail.razor new file mode 100644 index 0000000..a00c0de --- /dev/null +++ b/src/FrontOffice.Main/Pages/DiscountStore/OrderDetail.razor @@ -0,0 +1,198 @@ +@attribute [Route("/discount-store/order/{Id:long}")] + +جزئیات سفارش تخفیفی + +@if (!string.IsNullOrEmpty(_paymentMessage)) +{ + + + @_paymentMessage + + +} + +@if (_loading) +{ + + + +} +else if (_order is null) +{ + + + +} +else +{ + + + + + + @(_order.PaymentCompleted ? "پرداخت شده" : "در انتظار پرداخت") + + + @DiscountOrderService.GetDeliveryStatusText(_order.DeliveryStatus) + + + + + + + تاریخ ثبت: @FormatDate(_order.Created) + + + @* ── Address ── *@ + @if (_order.Address is not null) + { + + آدرس: @_order.Address.Title — @_order.Address.Address + + @if (!string.IsNullOrWhiteSpace(_order.Address.PostalCode)) + { + کد پستی: @_order.Address.PostalCode + } + } + + + + @* ── Desktop: Table ── *@ + + اقلام سفارش + + + محصول + قیمت واحد + تعداد + سقف تخفیف + تخفیف + قیمت نهایی + + + @context.Title + @FormatPrice(context.UnitPrice) + @context.Count + + @if (context.MaxDiscountPercent > 0) + { + @context.MaxDiscountPercent% + } + + + @if (context.DiscountAmount > 0) + { + @FormatPrice(context.DiscountAmount)- + } + else + { + + } + + @FormatPrice(context.FinalPrice) + + + + + @* ── Mobile: Cards ── *@ + + اقلام سفارش + + @foreach (var item in _order.Items) + { + + + @item.Title + + قیمت واحد: + @FormatPrice(item.UnitPrice) + + + تعداد: + @item.Count + + @if (item.DiscountAmount > 0) + { + + تخفیف (@item.MaxDiscountPercent%): + @FormatPrice(item.DiscountAmount)- + + } + + قیمت نهایی: + @FormatPrice(item.FinalPrice) + + + + } + + + + + + @* ── Financial Summary ── *@ + + خلاصه مالی + + جمع کل: + @FormatPrice(_order.TotalPrice) + + + @if (_order.DiscountBalanceUsed > 0) + { + + از کیف تخفیفی: + @FormatPrice(_order.DiscountBalanceUsed)- + + } + + + + + پرداخت درگاه: + + @FormatPrice(_order.GatewayAmount) + + + + + @* ── Tracking / Transaction ── *@ + @if (!string.IsNullOrWhiteSpace(_order.TrackingCode)) + { + + کد رهگیری: + @_order.TrackingCode + + } + @if (!string.IsNullOrWhiteSpace(_order.TransactionId)) + { + + شناسه تراکنش: + @_order.TransactionId + + } + + @* ── Notes ── *@ + @if (!string.IsNullOrWhiteSpace(_order.Notes)) + { + + + توضیحات: @_order.Notes + + } + @if (!string.IsNullOrWhiteSpace(_order.AdminNotes)) + { + + پیام مدیر: @_order.AdminNotes + + } + + + +} diff --git a/src/FrontOffice.Main/Pages/DiscountStore/OrderDetail.razor.cs b/src/FrontOffice.Main/Pages/DiscountStore/OrderDetail.razor.cs new file mode 100644 index 0000000..4c57dac --- /dev/null +++ b/src/FrontOffice.Main/Pages/DiscountStore/OrderDetail.razor.cs @@ -0,0 +1,76 @@ +using Microsoft.AspNetCore.Components; +using DateTimeConverterCL; +using FrontOffice.Main.Utilities; + +namespace FrontOffice.Main.Pages.DiscountStore; + +public partial class OrderDetail +{ + [Parameter] public long Id { get; set; } + + [SupplyParameterFromQuery(Name = "payment")] + public string? PaymentResult { get; set; } + + [Inject] private DiscountOrderService DiscountOrderService { get; set; } = default!; + + private DiscountOrderDetail? _order; + private bool _loading = true; + private string? _paymentMessage; + private MudBlazor.Severity _paymentSeverity; + + protected override async Task OnInitializedAsync() + { + // نمایش پیام نتیجه پرداخت (بعد از بازگشت از درگاه) + if (!string.IsNullOrEmpty(PaymentResult)) + { + switch (PaymentResult.ToLower()) + { + case "success": + _paymentMessage = "پرداخت با موفقیت انجام شد! سفارش شما ثبت نهایی شد."; + _paymentSeverity = MudBlazor.Severity.Success; + break; + case "failed": + _paymentMessage = "پرداخت ناموفق بود. مبلغ رزرو شده آزاد خواهد شد."; + _paymentSeverity = MudBlazor.Severity.Error; + break; + case "error": + _paymentMessage = "خطایی در پردازش پرداخت رخ داد. لطفاً با پشتیبانی تماس بگیرید."; + _paymentSeverity = MudBlazor.Severity.Warning; + break; + } + } + + await LoadOrderAsync(); + } + + private async Task LoadOrderAsync() + { + _loading = true; + try + { + _order = await DiscountOrderService.GetOrderByIdAsync(Id); + } + catch + { + Snackbar.Add("خطا در بارگذاری سفارش", MudBlazor.Severity.Error); + } + finally + { + _loading = false; + } + } + + private static string FormatPrice(long price) => $"{price:N0} تومان"; + + private static string FormatDate(DateTime date) + { + try + { + return date.ToLocalTime().MiladiToJalaliWithTime(); + } + catch + { + return date.ToString("yyyy/MM/dd"); + } + } +} diff --git a/src/FrontOffice.Main/Pages/DiscountStore/Orders.razor b/src/FrontOffice.Main/Pages/DiscountStore/Orders.razor new file mode 100644 index 0000000..a91cb51 --- /dev/null +++ b/src/FrontOffice.Main/Pages/DiscountStore/Orders.razor @@ -0,0 +1,130 @@ +@attribute [Route(RouteConstants.DiscountStore.Orders)] + +سفارش‌های فروشگاه تخفیفی + + + + + + @if (_loading) + { + + } + else if (_orders.Count == 0) + { + هنوز سفارشی ثبت نشده است. + + مشاهده فروشگاه تخفیفی + + } + else + { + + + + + + شماره سفارش + تعداد اقلام + مبلغ کل + از کیف تخفیفی + درگاه + وضعیت پرداخت + وضعیت ارسال + تاریخ + + + + @context.OrderNumber + @context.ItemsCount + @FormatPrice(context.TotalPrice) + + @if (context.DiscountBalanceUsed > 0) + { + @FormatPrice(context.DiscountBalanceUsed) + } + else + { + + } + + @FormatPrice(context.GatewayAmount) + + + @(context.PaymentCompleted ? "پرداخت شده" : "در انتظار پرداخت") + + + + + @DiscountOrderService.GetDeliveryStatusText(context.DeliveryStatus) + + + @FormatDate(context.Created) + + + جزئیات + + + + + + + + + + + @foreach (var order in _orders) + { + + + + سفارش #@order.OrderNumber + + @(order.PaymentCompleted ? "پرداخت شده" : "در انتظار") + + + + @FormatDate(order.Created) + + @DiscountOrderService.GetDeliveryStatusText(order.DeliveryStatus) + + + + + @order.ItemsCount قلم + @FormatPrice(order.TotalPrice) تومان + + @if (order.DiscountBalanceUsed > 0) + { + + از کیف تخفیفی: @FormatPrice(order.DiscountBalanceUsed) تومان + + } + + + } + + + + @if (_totalPages > 1) + { + + + + } + } + + diff --git a/src/FrontOffice.Main/Pages/DiscountStore/Orders.razor.cs b/src/FrontOffice.Main/Pages/DiscountStore/Orders.razor.cs new file mode 100644 index 0000000..89c6e59 --- /dev/null +++ b/src/FrontOffice.Main/Pages/DiscountStore/Orders.razor.cs @@ -0,0 +1,65 @@ +using Microsoft.AspNetCore.Components; +using DateTimeConverterCL; +using FrontOffice.Main.Utilities; + +namespace FrontOffice.Main.Pages.DiscountStore; + +public partial class Orders +{ + [Inject] private DiscountOrderService DiscountOrderService { get; set; } = default!; + + private List _orders = new(); + private int _currentPage = 1; + private int _totalPages; + private bool _loading = true; + private const int PageSize = 10; + + protected override async Task OnInitializedAsync() + { + await LoadOrdersAsync(); + } + + private async Task LoadOrdersAsync() + { + _loading = true; + try + { + var result = await DiscountOrderService.GetUserOrdersAsync(_currentPage, PageSize); + _orders = result.Orders; + _totalPages = result.TotalPages; + } + catch + { + Snackbar.Add("خطا در بارگذاری سفارش‌ها", MudBlazor.Severity.Error); + } + finally + { + _loading = false; + } + } + + private async Task OnPageChanged(int page) + { + _currentPage = page; + await LoadOrdersAsync(); + } + + private void ViewOrder(long orderId) + { + Navigation.NavigateTo($"{RouteConstants.DiscountStore.OrderDetail}{orderId}"); + } + + private static string FormatPrice(long price) => $"{price:N0}"; + + private static string FormatDate(DateTime date) + { + try + { + return date.ToLocalTime().MiladiToJalaliWithTime(); + } + catch + { + return date.ToString("yyyy/MM/dd"); + } + } +} diff --git a/src/FrontOffice.Main/Pages/DiscountStore/ProductDetail.razor b/src/FrontOffice.Main/Pages/DiscountStore/ProductDetail.razor new file mode 100644 index 0000000..fcabe1d --- /dev/null +++ b/src/FrontOffice.Main/Pages/DiscountStore/ProductDetail.razor @@ -0,0 +1,140 @@ +@attribute [Route("/discount-store/product/{Id:long}")] + +@(_product?.Title ?? "محصول تخفیفی") + + + @if (_loading) + { + + } + else if (_product is null) + { + + } + else + { + + + + + + + + + + @if (_product.Images.Count > 1) + { + + @foreach (var img in _product.Images) + { + + + + } + + } + + + + + + + @_product.Title + + + @if (_product.Categories.Any()) + { + + @foreach (var cat in _product.Categories) + { + @cat.Title + } + + } + + + + + + قیمت محصول: + + @($"{_product.Price:N0}") تومان + + + @if (_product.MaxDiscountPercent > 0) + { + + سقف پرداخت از کیف تخفیفی: + + تا @_product.MaxDiscountPercent% (تا @($"{_product.Price * _product.MaxDiscountPercent / 100:N0}") تومان) + + + } + + + + + + + @(_product.RemainingCount > 0 ? $"موجود ({_product.RemainingCount} عدد)" : "ناموجود") + + + @_product.ViewCount بازدید + + + + + @if (!string.IsNullOrWhiteSpace(_product.ShortInfo)) + { + @_product.ShortInfo + } + + + + + + @(_addingToCart ? "در حال افزودن..." : "افزودن به سبد") + + + مشاهده سبد + + + + + + + + @if (!string.IsNullOrWhiteSpace(_product.FullInfo)) + { + + توضیحات محصول + +
+ @((MarkupString)_product.FullInfo) +
+
+ } + } +
diff --git a/src/FrontOffice.Main/Pages/DiscountStore/ProductDetail.razor.cs b/src/FrontOffice.Main/Pages/DiscountStore/ProductDetail.razor.cs new file mode 100644 index 0000000..fd90d66 --- /dev/null +++ b/src/FrontOffice.Main/Pages/DiscountStore/ProductDetail.razor.cs @@ -0,0 +1,64 @@ +using Microsoft.AspNetCore.Components; +using FrontOffice.Main.Utilities; + +namespace FrontOffice.Main.Pages.DiscountStore; + +public partial class ProductDetail +{ + [Parameter] public long Id { get; set; } + + [Inject] private DiscountProductService DiscountProductService { get; set; } = default!; + [Inject] private DiscountCartService DiscountCartService { get; set; } = default!; + + private DiscountProductDetail? _product; + private string _selectedImage = string.Empty; + private int _quantity = 1; + private bool _loading = true; + private bool _addingToCart; + + protected override async Task OnInitializedAsync() + { + await LoadProductAsync(); + } + + private async Task LoadProductAsync() + { + _loading = true; + try + { + _product = await DiscountProductService.GetByIdAsync(Id); + if (_product is not null) + { + _selectedImage = _product.Images.FirstOrDefault()?.ImageUrl + ?? _product.ThumbnailUrl; + } + } + catch + { + Snackbar.Add("خطا در بارگذاری محصول", MudBlazor.Severity.Error); + } + finally + { + _loading = false; + } + } + + private async Task AddToCart() + { + if (_product is null) return; + _addingToCart = true; + try + { + await DiscountCartService.AddAsync(_product.Id, _quantity); + Snackbar.Add($"{_product.Title} به سبد خرید اضافه شد", MudBlazor.Severity.Success); + } + catch + { + Snackbar.Add("خطا در افزودن به سبد خرید", MudBlazor.Severity.Error); + } + finally + { + _addingToCart = false; + } + } +} diff --git a/src/FrontOffice.Main/Pages/DiscountStore/Products.razor b/src/FrontOffice.Main/Pages/DiscountStore/Products.razor new file mode 100644 index 0000000..7312608 --- /dev/null +++ b/src/FrontOffice.Main/Pages/DiscountStore/Products.razor @@ -0,0 +1,133 @@ +@attribute [Route(RouteConstants.DiscountStore.Products)] + +فروشگاه تخفیفی + + + + + + + فروشگاه تخفیفی + + خرید با استفاده از موجودی کیف پول تخفیفی + + + + + + + + + + + + + @foreach (var cat in _categories) + { + @cat.Title (@cat.ProductCount) + } + + + + جستجو + + + + سبد خرید + @if (DiscountCart.Count > 0) + { + + } + + + + + + + @if (_loading) + { + + + در حال بارگذاری... + + } + else if (_result.Products.Count == 0) + { + محصولی یافت نشد. + } + else + { + + + @foreach (var p in _result.Products) + { + + + +
+ @if (p.RemainingCount <= 0) + { +
+ ناموجود +
+ } + else if (p.RemainingCount <= 5) + { + + فقط @p.RemainingCount عدد + + } + @if (p.MaxDiscountPercent > 0) + { + + تا @p.MaxDiscountPercent% تخفیف + + } +
+
+ @p.Title + @FormatPrice(p.Price) +
+
+ + + @(p.RemainingCount <= 0 ? "ناموجود" : "افزودن") + + +
+
+ } +
+ + + @if (_result.TotalPages > 1) + { +
+ +
+ } + } +
diff --git a/src/FrontOffice.Main/Pages/DiscountStore/Products.razor.cs b/src/FrontOffice.Main/Pages/DiscountStore/Products.razor.cs new file mode 100644 index 0000000..e84463c --- /dev/null +++ b/src/FrontOffice.Main/Pages/DiscountStore/Products.razor.cs @@ -0,0 +1,87 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using FrontOffice.Main.Utilities; + +namespace FrontOffice.Main.Pages.DiscountStore; + +public partial class Products : ComponentBase, IDisposable +{ + [Inject] private DiscountProductService ProductService { get; set; } = default!; + [Inject] private DiscountCartService DiscountCart { get; set; } = default!; + + private string _search = string.Empty; + private long? _selectedCategoryId; + private int _currentPage = 1; + private bool _loading; + private const int PageSize = 12; + + private DiscountProductListResult _result = new(new(), 0, 0, 1); + private List _categories = new(); + + protected override async Task OnInitializedAsync() + { + _loading = true; + await DiscountCart.EnsureInitializedAsync(); + DiscountCart.OnChange += StateHasChanged; + var categoriesTask = ProductService.GetCategoriesAsync(); + var productsTask = ProductService.GetProductsAsync(page: 1, pageSize: PageSize); + await Task.WhenAll(categoriesTask, productsTask); + _categories = categoriesTask.Result; + _result = productsTask.Result; + _loading = false; + } + + private async Task LoadProductsAsync() + { + _loading = true; + StateHasChanged(); + _result = await ProductService.GetProductsAsync( + page: _currentPage, + pageSize: PageSize, + search: string.IsNullOrWhiteSpace(_search) ? null : _search, + categoryId: _selectedCategoryId); + _loading = false; + StateHasChanged(); + } + + private async Task SearchProducts() => await LoadProductsAsync(); + + private async Task OnSearchKeyUp(KeyboardEventArgs e) + { + if (e.Key == "Enter") + { + _currentPage = 1; + await LoadProductsAsync(); + } + } + + private async Task OnCategoryChanged(long? value) + { + _selectedCategoryId = value; + _currentPage = 1; + await LoadProductsAsync(); + } + + private async Task OnPageChanged(int page) + { + _currentPage = page; + await LoadProductsAsync(); + } + + private async Task AddToCart(DiscountProductCard p) + { + await DiscountCart.AddAsync(p.Id); + } + + private void NavigateToProduct(long id) + { + Navigation.NavigateTo($"{RouteConstants.DiscountStore.ProductDetail}{id}"); + } + + private static string FormatPrice(long price) => $"{price:N0} تومان"; + + public void Dispose() + { + DiscountCart.OnChange -= StateHasChanged; + } +} diff --git a/src/FrontOffice.Main/Pages/FAQ.razor b/src/FrontOffice.Main/Pages/FAQ.razor index 1277aef..7c31375 100644 --- a/src/FrontOffice.Main/Pages/FAQ.razor +++ b/src/FrontOffice.Main/Pages/FAQ.razor @@ -116,7 +116,7 @@
- + سوال شما پاسخ داده نشد؟ diff --git a/src/FrontOffice.Main/Pages/Gateway/CartChooser.razor b/src/FrontOffice.Main/Pages/Gateway/CartChooser.razor new file mode 100644 index 0000000..c1e57d1 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Gateway/CartChooser.razor @@ -0,0 +1,62 @@ +@attribute [Route(RouteConstants.Gateway.CartChooser)] + +سبد خرید + + + + + + + سبد خرید کدام فروشگاه را می‌خواهید مشاهده کنید؟ + + + + + + + + + + سبد خرید فروشگاه + @if (_cartCount > 0) + { + + @_cartCount محصول + + } + else + { + سبد خالی + } + + + + + + + + + + + سبد خرید تخفیفی + @if (_discountCartCount > 0) + { + + @_discountCartCount محصول + + } + else + { + سبد خالی + } + + + + + + + بازگشت به داشبورد + + + diff --git a/src/FrontOffice.Main/Pages/Gateway/CartChooser.razor.cs b/src/FrontOffice.Main/Pages/Gateway/CartChooser.razor.cs new file mode 100644 index 0000000..1c9b990 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Gateway/CartChooser.razor.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using FrontOffice.Main.Utilities; + +namespace FrontOffice.Main.Pages.Gateway; + +public partial class CartChooser : IDisposable +{ + [Inject] private CartService CartService { get; set; } = default!; + [Inject] private DiscountCartService DiscountCartService { get; set; } = default!; + [Inject] private IJSRuntime JS { get; set; } = default!; + + private int _cartCount; + private int _discountCartCount; + + protected override async Task OnInitializedAsync() + { + await CartService.EnsureInitializedAsync(); + await DiscountCartService.EnsureInitializedAsync(); + + _cartCount = CartService.Count; + _discountCartCount = DiscountCartService.Count; + + CartService.OnChange += OnCartChanged; + DiscountCartService.OnChange += OnDiscountCartChanged; + } + + private void OnCartChanged() + { + _cartCount = CartService.Count; + InvokeAsync(StateHasChanged); + } + + private void OnDiscountCartChanged() + { + _discountCartCount = DiscountCartService.Count; + InvokeAsync(StateHasChanged); + } + + public void Dispose() + { + CartService.OnChange -= OnCartChanged; + DiscountCartService.OnChange -= OnDiscountCartChanged; + } + + private async Task GoBack() => await JS.InvokeVoidAsync("history.back"); +} diff --git a/src/FrontOffice.Main/Pages/Gateway/OrdersChooser.razor b/src/FrontOffice.Main/Pages/Gateway/OrdersChooser.razor new file mode 100644 index 0000000..58c531b --- /dev/null +++ b/src/FrontOffice.Main/Pages/Gateway/OrdersChooser.razor @@ -0,0 +1,49 @@ +@attribute [Route(RouteConstants.Gateway.OrdersChooser)] +@inject IJSRuntime JS + +سفارشات من + + + + + + + سفارشات کدام فروشگاه را می‌خواهید مشاهده کنید؟ + + + + + + + + + + سفارشات فروشگاه + سفارشات خرید معمولی + + + + + + + + + + + سفارشات فروشگاه تخفیفی + سفارشات خرید تخفیفی + + + + + + + بازگشت به داشبورد + + + + +@code { + private async Task GoBack() => await JS.InvokeVoidAsync("history.back"); +} diff --git a/src/FrontOffice.Main/Pages/Gateway/StoreChooser.razor b/src/FrontOffice.Main/Pages/Gateway/StoreChooser.razor new file mode 100644 index 0000000..30becda --- /dev/null +++ b/src/FrontOffice.Main/Pages/Gateway/StoreChooser.razor @@ -0,0 +1,56 @@ +@attribute [Route(RouteConstants.Gateway.StoreChooser)] + +فروشگاه‌ها + + + + + + برای خرید، یکی از فروشگاه‌ها را انتخاب کنید. هر فروشگاه سبد خرید و سفارشات مجزای خود را دارد. + + + + + + + + + + + فروشگاه + + خرید محصولات با پرداخت از کیف پول اعتباری + + @if (_cartCount > 0) + { + + @_cartCount محصول در سبد خرید + + } + + + + + + + + + + + + فروشگاه تخفیفی + + خرید با استفاده از موجودی کیف تخفیفی + درگاه پرداخت + + @if (_discountCartCount > 0) + { + + @_discountCartCount محصول در سبد خرید + + } + + + + + + diff --git a/src/FrontOffice.Main/Pages/Gateway/StoreChooser.razor.cs b/src/FrontOffice.Main/Pages/Gateway/StoreChooser.razor.cs new file mode 100644 index 0000000..e19a095 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Gateway/StoreChooser.razor.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Components; +using FrontOffice.Main.Utilities; + +namespace FrontOffice.Main.Pages.Gateway; + +public partial class StoreChooser : IDisposable +{ + [Inject] private CartService CartService { get; set; } = default!; + [Inject] private DiscountCartService DiscountCartService { get; set; } = default!; + + private int _cartCount; + private int _discountCartCount; + + protected override async Task OnInitializedAsync() + { + await CartService.EnsureInitializedAsync(); + await DiscountCartService.EnsureInitializedAsync(); + + _cartCount = CartService.Count; + _discountCartCount = DiscountCartService.Count; + + CartService.OnChange += OnCartChanged; + DiscountCartService.OnChange += OnDiscountCartChanged; + } + + private void OnCartChanged() + { + _cartCount = CartService.Count; + InvokeAsync(StateHasChanged); + } + + private void OnDiscountCartChanged() + { + _discountCartCount = DiscountCartService.Count; + InvokeAsync(StateHasChanged); + } + + public void Dispose() + { + CartService.OnChange -= OnCartChanged; + DiscountCartService.OnChange -= OnDiscountCartChanged; + } +} diff --git a/src/FrontOffice.Main/Pages/Index.razor b/src/FrontOffice.Main/Pages/Index.razor index 0da1b8c..2be5568 100644 --- a/src/FrontOffice.Main/Pages/Index.razor +++ b/src/FrontOffice.Main/Pages/Index.razor @@ -1,354 +1,343 @@ @attribute [Route(RouteConstants.Main.MainPage)] -@inject NavigationManager Navigation +@inject IJSRuntime JS -صفحه اصلی +صفحه اصلی | کارا بازار سلامت - -
- - - - پیشنهاد ویژه برای شروع - - - با دعوت از دوستان خود، از خریدهای واقعی پاداش بگیرید - - - ثبت‌نام آسان، تجربهٔ سریع و شفاف. همین امروز حساب بساز و با دعوت دوستانت، امتیاز و پاداش دریافت کن. - +@* ═══════════════════════════════════════════════ + 1. HERO — Bold, minimal, centered + ═══════════════════════════════════════════════ *@ +
+ + + + 🌿 پلتفرم سلامت‌محور فروش و تیم‌سازی + - - مشاهده قیمت‌ها - شروع کن - - - - ثبت‌نام زیر ۲ دقیقه - پشتیبانی ۷×۲۴ - پرداخت ایمن + + رشد تیم، فروش واقعی، پاداش شفاف + + + + با دعوت از دوستان، از خریدهای واقعی محصولات سلامت پاداش بگیرید. ثبت‌نام سریع، داشبورد لحظه‌ای. + + + + + شروع رایگان + + + آخرین اخبار + + + + @* ── Trust badges ── *@ + + @foreach (var badge in _trustBadges) + { + + + @badge.Text + } + + + +
+@* ═══════════════════════════════════════════════ + 1b. FEATURED POST BANNER — below hero + ═══════════════════════════════════════════════ *@ +@if (_latestPosts.Any()) +{ + var featured = _latestPosts.First(); +
+ + + + @if (!string.IsNullOrWhiteSpace(featured.ThumbnailUrl)) + { + + } + else + { + + + + } + + + 🔥 جدیدترین مطلب + + @featured.Title + @if (!string.IsNullOrWhiteSpace(featured.Summary)) + { + @featured.Summary + } + + - + + +
+} - - - - - +@* ═══════════════════════════════════════════════ + 2. HOW IT WORKS — 3 numbered steps + ═══════════════════════════════════════════════ *@ +
+ +
+ سه گام تا شروع + + بدون پیچیدگی، سریع و آسان + +
+ + + @foreach (var (step, i) in _steps.Select((s, i) => (s, i))) + { + + + + @(i + 1) + + @step.Title + @step.Desc + + + }
- - -
- - - - چرا کاربران ما را انتخاب می‌کنند؟ - - - سه دلیل روشن برای شروع امروز. +@* ═══════════════════════════════════════════════ + 3. FEATURES — 6 cards with icons + ═══════════════════════════════════════════════ *@ +
+ +
+ چرا کارا بازار سلامت؟ + + ابزارهایی ساده و قدرتمند برای رشد کسب‌وکار شما +
- - - - - - - - ثبت‌نام سریع و ساده - - - - در چند گام کوتاه، حسابت را بساز و شروع کن. - - - - - - - - - - - مزایا و پاداش‌های شفاف - - - - قوانین روشن، دسترسی آسان به سوابق و گزارش‌ها. - - - - - - - - - - - اپلیکیشن واکنش‌گرا - - - - تجربه‌ای روان در موبایل و دسکتاپ. - - - - - - -
-
- - @* -
- - - پکیج‌های سرمایه گذاری - - - بر اساس اندازهٔ تیم خود انتخاب کنید. - - @if (_isLoadingPackages) + + @foreach (var (icon, title, desc, delay) in _features) { - - - در حال بارگذاری پکیج‌ها... - + + + + + + + @title + @desc + + + } - else if (_packs.Any()) + + +
+ +@* ═══════════════════════════════════════════════ + 4. STATS — Counter cards + ═══════════════════════════════════════════════ *@ +
+ + + @foreach (var stat in _stats) { - - @foreach (var p in _packs) - { - - - - - @(p.Title) - @((MarkupString)p.Body) - - - @(p.Price.ToThousands().ToCurrencyUnitIRT()) + + + @stat.Display + @stat.Label + + + } + + +
+ +@* ═══════════════════════════════════════════════ + 5. LATEST BLOG POSTS + ═══════════════════════════════════════════════ *@ +@if (_latestPosts.Any()) +{ +
+ +
+ آخرین مقالات + + جدیدترین مطالب و اخبار کارا بازار سلامت + +
+ + + @foreach (var post in _latestPosts.Take(2)) + { + + +
+ @if (!string.IsNullOrWhiteSpace(post.ThumbnailUrl)) + { + + } + else + { +
+ +
+ } +
+
+ @if (post.Categories.Any()) + { + + @post.Categories.First().Title + + } + + @post.Title + + @if (!string.IsNullOrWhiteSpace(post.Summary)) + { + + @post.Summary + + } + + @if (post.PublishedAt.HasValue) + { + + + @post.PublishedAt.Value.ToString("yyyy/MM/dd") + + } - مشاهده جزئیات - - - - } - - } - else - { - - هیچ پکیجی یافت نشد. - - } - -
- *@ - -
- - - کارا بازار سلامت چطور کار می‌کند؟ - - - سه گام روشن تا شروع یک ماجراجویی جدید - - - - - - - - - ثبت‌نام - - - مرحله اول - یک حساب بساز و وارد شو. - - - - - - دعوت دوستان - - - مرحله دوم - لینک دعوتت را به اشتراک بگذار. - - - - - - دریافت پاداش - - - مرحله سوم - از خریدهای واقعی دوستانت پاداش بگیر. - - - + + + @post.ViewCount + + +
-
-
- - - - - - - - - +۵۰٪ - رشد میانگین تیم - - - - - ۹۹٫۹٪ - آپ‌تایم سرویس - - - - - ۳ روز - میانگین زمان استقرار - - - - - +۲۰ کشور - پوشش ارسال کد - - - - - - +
+ }
-
-
- - -
- - - اعتماد مشتریان - - - بخشی از تجربهٔ استفاده از «کارا بازار سلامت». - - - - - - - - - -
- شرکت سینا نت - مدیر عملیات -
-
- - «با کارا بازار سلامت، محاسبهٔ کارمزدها و پایش تیم‌ها بدون اکسل و دردسر انجام می‌شود.» - -
-
-
- - - - - - - - -
- هولدینگ آریانا - مدیر فروش -
-
- - «شجره‌نامهٔ بصری و گزارش‌های دقیق باعث شد رشد تیم را لحظه‌ای ببینیم.» - -
-
-
- -
-
-
- - -
- - - سوالات متداول - - - پاسخ به سوالات رایج شما. - -
- - @foreach (var q in _faqs) - { - - @q.A - - } - +
+ + مشاهده همه مقالات +
+} -
+@* ═══════════════════════════════════════════════ + 6. TESTIMONIALS + ═══════════════════════════════════════════════ *@ +
+ +
+ اعتماد مشتریان + + بخشی از تجربه استفاده از «کارا بازار سلامت» + +
+ + + @foreach (var t in _testimonials) + { + + + + + + «@t.Quote» + + + + + + +
+ @t.Name + @t.Role +
+
+
+
+
+ } +
+
+
+ +@* ═══════════════════════════════════════════════ + 7. FAQ + ═══════════════════════════════════════════════ *@ +
+ +
+ سوالات متداول + + پاسخ به سوالات رایج شما + +
+ +
+ + @foreach (var q in _faqs) + { + + @q.A + + } + +
+
+
+ +@* ═══════════════════════════════════════════════ + 8. CTA BANNER + ═══════════════════════════════════════════════ *@ +
+ +
+ + + آماده شروع هستید؟ + + + همین الان ثبت‌نام کنید و از مزایای کارا بازار سلامت بهره‌مند شوید. + + + شروع رایگان + + +
+
+
diff --git a/src/FrontOffice.Main/Pages/Index.razor.cs b/src/FrontOffice.Main/Pages/Index.razor.cs index bd4a508..595ff44 100644 --- a/src/FrontOffice.Main/Pages/Index.razor.cs +++ b/src/FrontOffice.Main/Pages/Index.razor.cs @@ -1,44 +1,66 @@ -using CMSMicroservice.Protobuf.Protos.Package; -using FrontOffice.Main.Utilities; +using FrontOffice.Main.Utilities; using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; using MudBlazor; -using System.Security.Cryptography; namespace FrontOffice.Main.Pages; -public partial class Index:IDisposable +public partial class Index : IDisposable { - [Inject] private PackageContract.PackageContractClient PackageClient { get; set; } = default!; - [Inject] private MobileNumberEncryptor Encryptor { get; set; } - private string? _email; - private bool _isLoadingPackages; - private List _packs = new(); + [Inject] private BlogPostService BlogPostService { get; set; } = default!; [Inject] private AuthService AuthService { get; set; } = default!; + // ── Latest blog posts (loaded from CMS) ── + private List _latestPosts = new(); + protected override async Task OnInitializedAsync() { - MainService.OnChangeHandler +=async () => + MainService.OnChangeHandler += OnStateChanged; + + // Load latest published blog posts + // اول پین‌شده‌ها، اگر کافی نبود آخرین پست‌ها اضافه شود + try { - await InvokeAsync(StateHasChanged); - }; - // await LoadPackagesAsync(); - - //string mobileNumber = "09387342688"; - - //// انکریپت کردن - //string encrypted = Encryptor.EncryptMobileNumber(mobileNumber); - //Console.WriteLine($"Encrypted: {encrypted}"); - - //// دیکریپت کردن برای تست - //string decrypted = Encryptor.DecryptMobileNumber(encrypted); - //Console.WriteLine($"Decrypted: {decrypted}"); + _latestPosts = await BlogPostService.GetFeaturedPostsAsync(2); + + // اگر فقط ۱ پین‌شده داریم، ۱ پست آخر هم اضافه کن + if (_latestPosts.Count < 2) + { + var result = await BlogPostService.GetPublishedPostsAsync(page: 1, pageSize: 2); + var existing = _latestPosts.Select(p => p.Id).ToHashSet(); + foreach (var post in result.Posts) + { + if (!existing.Contains(post.Id)) + { + _latestPosts.Add(post); + if (_latestPosts.Count >= 2) break; + } + } + } + } + catch + { + _latestPosts = new(); + } } protected override async Task OnAfterRenderAsync(bool firstRender) { + if (firstRender) + { + // Init scroll-triggered fade-in animations + await JS.InvokeVoidAsync("initScrollAnimations"); + + // Animate stat counters + foreach (var stat in _stats) + { + await JS.InvokeVoidAsync("animateCounter", stat.ElementId, stat.Target, 2200, stat.Suffix); + } + } + if (await AuthService.IsAuthenticatedAsync()) { - if ((await AuthService.IsCompleteRegisterAsync())) + if (await AuthService.IsCompleteRegisterAsync()) { Navigation.NavigateTo(RouteConstants.Profile.Index); } @@ -47,56 +69,8 @@ public partial class Index:IDisposable Navigation.NavigateTo(RouteConstants.Registration.Wizard); } } - await base.OnAfterRenderAsync(firstRender); - } - // private async Task LoadPackagesAsync() - // { - // _isLoadingPackages = true; - // try - // { - // var response = await PackageClient.GetAllPackageByFilterAsync(request: new()); - // if (response?.Models?.Any() == true) - // { - // _packs = response.Models.Select(p => new Pack( - // Id: p.Id, - // Title: p.Title, - // Body: p.Description, - // Image: UrlUtility.DownloadUrl + p.ImagePath, - // Price: p.Price - // )).ToList(); - // } - // else - // _packs = new List(); - // } - // catch (Exception ex) - // { - // Snackbar.Add($"خطا در بارگذاری پکیج‌ها: {ex.Message}", Severity.Error); - // // Fallback to empty list - // _packs = new List(); - // } - // finally - // { - // _isLoadingPackages = false; - // await InvokeAsync(StateHasChanged); - // } - // } - - private void JoinWaitlist() - { - if (string.IsNullOrWhiteSpace(_email)) - { - Snackbar.Add("لطفاً ایمیل معتبر وارد کنید.", Severity.Warning); - return; - } - - Snackbar.Add("به لیست انتظار «کارا بازار سلامت» اضافه شدید.", Severity.Success); - _email = string.Empty; - } - - private void NavigateToPackage(long packageId) - { - Navigation.NavigateTo($"{RouteConstants.Package.Detail}{packageId}"); + await base.OnAfterRenderAsync(firstRender); } private void NavigateToRegistrationWizard() @@ -104,25 +78,77 @@ public partial class Index:IDisposable Navigation.NavigateTo(RouteConstants.Registration.Wizard); } - private record Pack(long Id, string Title, string Body, string Image, long Price); + private void NavigateToPost(string slug) + { + Navigation.NavigateTo($"/blog/{slug}"); + } - private record Plan(string Name, string Price, bool Highlight, IEnumerable Features); + private async void OnStateChanged() + { + await InvokeAsync(StateHasChanged); + } - private record QA(string Q, string A); + // ── Trust badges ── + private readonly List<(string Icon, string Text)> _trustBadges = new() + { + (Icons.Material.Outlined.Timer, "ثبت‌نام زیر ۲ دقیقه"), + (Icons.Material.Outlined.SupportAgent, "پشتیبانی ۷×۲۴"), + (Icons.Material.Outlined.Lock, "پرداخت ایمن"), + (Icons.Material.Outlined.Verified, "تضمین کیفیت"), + }; + // ── How it works steps ── + private readonly List<(string Title, string Desc)> _steps = new() + { + ("ثبت‌نام و احراز هویت", "یک حساب بسازید، شماره موبایل را تأیید و اطلاعات هویتی را تکمیل کنید."), + ("دعوت دوستان", "لینک دعوت اختصاصی خود را با دوستان و آشنایان به اشتراک بگذارید."), + ("دریافت پاداش", "از خریدهای واقعی اعضای تیمتان پاداش شفاف و لحظه‌ای دریافت کنید."), + }; + + // ── Feature cards data (6 cards) ── + private readonly List<(string Icon, string Title, string Desc, int Delay)> _features = new() + { + (Icons.Material.Outlined.VerifiedUser, "ثبت‌نام سریع و ساده", "در چند گام کوتاه حساب بسازید و شروع کنید.", 0), + (Icons.Material.Outlined.StarBorder, "پاداش‌های شفاف", "قوانین روشن، دسترسی آسان به سوابق و گزارش‌ها.", 100), + (Icons.Material.Outlined.Devices, "طراحی واکنش‌گرا", "تجربه‌ای روان در موبایل و دسکتاپ.", 200), + (Icons.Material.Outlined.Groups, "تیم‌سازی هوشمند", "ساختار درختی شبکه و مدیریت تیم‌های فروش.", 300), + (Icons.Material.Outlined.Insights, "گزارش‌های لحظه‌ای", "داشبورد پویا برای مشاهده عملکرد و کمیسیون.", 400), + (Icons.Material.Outlined.Lock, "امنیت بالا", "رمزنگاری اطلاعات و احراز هویت چندمرحله‌ای.", 500), + }; + + // ── Stats data (animated counters) ── + private readonly List _stats = new() + { + new("stat-growth", "+۵۰٪", 50, "%+", Color.Success, "رشد میانگین تیم"), + new("stat-uptime", "۹۹.۹٪", 99.9, "%", Color.Primary, "آپ‌تایم سرویس"), + new("stat-deploy", "۳ روز", 3, " روز", Color.Warning, "میانگین زمان استقرار"), + new("stat-coverage", "+۲۰ کشور", 20, "+", Color.Secondary, "پوشش ارسال کد"), + }; + + // ── Testimonials ── + private readonly List _testimonials = new() + { + new("با کارا بازار سلامت، محاسبه کارمزدها و پایش تیم‌ها بدون اکسل و دردسر انجام می‌شود.", "شرکت سینا نت", "مدیر عملیات", 0), + new("شجره‌نامه بصری و گزارش‌های دقیق باعث شد رشد تیم را لحظه‌ای ببینیم.", "هولدینگ آریانا", "مدیر فروش", 150), + new("سادگی ثبت‌نام و شفافیت پاداش‌ها مهم‌ترین مزیت این پلتفرم است.", "گروه بهداشتی نوین", "مدیر توسعه", 300), + }; + + // ── FAQ data ── private readonly List _faqs = new() { - new("دامنهٔ اختصاصی دارم؛ قابل اتصال است؟", "بله، پشت دامنه و گواهی SSL خودتان مستقر می‌شود."), + new("دامنه اختصاصی دارم؛ قابل اتصال است؟", "بله، پشت دامنه و گواهی SSL خودتان مستقر می‌شود."), new("با دیتابیس خودم کار می‌کند؟", "کاملاً. SQL Server، PostgreSQL و MySQL پشتیبانی می‌شود."), new("چه درگاه‌هایی پشتیبانی می‌شود؟", "Stripe و PayPal یا درگاه اختصاصی از طریق وب‌هوک‌ها."), new("می‌توانم داده‌ها را خروجی بگیرم؟", "هر زمان از داشبورد ادمین خروجی CSV/Excel بگیرید."), }; + // ── Records ── + private record QA(string Q, string A); + private record StatItem(string ElementId, string Display, double Target, string Suffix, Color Color, string Label); + private record TestimonialItem(string Quote, string Name, string Role, int Delay); + public void Dispose() { - MainService.OnChangeHandler -=async () => - { - await InvokeAsync(StateHasChanged); - }; + MainService.OnChangeHandler -= OnStateChanged; } } \ No newline at end of file diff --git a/src/FrontOffice.Main/Pages/Network/NetworkStatisticsPage.razor b/src/FrontOffice.Main/Pages/Network/NetworkStatisticsPage.razor index 2b2f254..95f7384 100644 --- a/src/FrontOffice.Main/Pages/Network/NetworkStatisticsPage.razor +++ b/src/FrontOffice.Main/Pages/Network/NetworkStatisticsPage.razor @@ -6,14 +6,11 @@ - - آمار باشگاه توسعه دهندگان - بازگشت - + @if (_isLoading) { - + } else if (_statistics is not null) { diff --git a/src/FrontOffice.Main/Pages/Package/MyPackages.razor b/src/FrontOffice.Main/Pages/Package/MyPackages.razor index 6eda2aa..95a2685 100644 --- a/src/FrontOffice.Main/Pages/Package/MyPackages.razor +++ b/src/FrontOffice.Main/Pages/Package/MyPackages.razor @@ -3,16 +3,7 @@ پکیج‌های من - - - - - - پکیج‌های من - - وضعیت پکیج‌های خریداری شده و عضویت باشگاه مشتریان - - + @if (_isLoading) { diff --git a/src/FrontOffice.Main/Pages/Package/Packages.razor b/src/FrontOffice.Main/Pages/Package/Packages.razor index 05c42dc..c7fb39c 100644 --- a/src/FrontOffice.Main/Pages/Package/Packages.razor +++ b/src/FrontOffice.Main/Pages/Package/Packages.razor @@ -3,13 +3,7 @@ پکیج‌ها - - - پکیج‌های سرمایه‌گذاری - - با خرید پکیج طلایی، به باشگاه مشتریان بپیوندید و از مزایای ویژه بهره‌مند شوید. - - + @if (_isLoading) { diff --git a/src/FrontOffice.Main/Pages/PackageDetail.razor b/src/FrontOffice.Main/Pages/PackageDetail.razor index 80f7a4e..b8b31b6 100644 --- a/src/FrontOffice.Main/Pages/PackageDetail.razor +++ b/src/FrontOffice.Main/Pages/PackageDetail.razor @@ -4,26 +4,25 @@ @if (_isLoading) { - - - در حال بارگذاری... - + + + } else if (_package == null) { - - - پکیج یافت نشد - پکیج مورد نظر وجود ندارد یا حذف شده است. - - بازگشت به صفحه اصلی - - + + + } else { + @@ -36,9 +35,9 @@ else - - @relatedPackage.Title diff --git a/src/FrontOffice.Main/Pages/PackageDetail.razor.cs b/src/FrontOffice.Main/Pages/PackageDetail.razor.cs index d73c758..2ea07bf 100644 --- a/src/FrontOffice.Main/Pages/PackageDetail.razor.cs +++ b/src/FrontOffice.Main/Pages/PackageDetail.razor.cs @@ -74,7 +74,7 @@ public partial class PackageDetail : IDisposable Id = packageResponse.Id, Title = packageResponse.Title, Body = packageResponse.Description, - Image = UrlUtility.DownloadUrl + packageResponse.ImagePath, + Image = packageResponse.ImagePath ?? string.Empty, Specifications = new List { new() { Name = "ظرفیت", Value = "تا ۲۰۰ عضو", Icon = Icons.Material.Filled.Group }, diff --git a/src/FrontOffice.Main/Pages/Profile/Addresses.razor b/src/FrontOffice.Main/Pages/Profile/Addresses.razor index 5748acc..4131798 100644 --- a/src/FrontOffice.Main/Pages/Profile/Addresses.razor +++ b/src/FrontOffice.Main/Pages/Profile/Addresses.razor @@ -4,10 +4,7 @@ - - مدیریت آدرس‌ها - بازگشت - + diff --git a/src/FrontOffice.Main/Pages/Profile/ChangePassword.razor b/src/FrontOffice.Main/Pages/Profile/ChangePassword.razor index da5c583..f453fbc 100644 --- a/src/FrontOffice.Main/Pages/Profile/ChangePassword.razor +++ b/src/FrontOffice.Main/Pages/Profile/ChangePassword.razor @@ -4,10 +4,7 @@ - - تغییر رمز عبور - بازگشت - + diff --git a/src/FrontOffice.Main/Pages/Profile/Components/OrganizationChart.razor b/src/FrontOffice.Main/Pages/Profile/Components/OrganizationChart.razor index 4554ebb..8b563fa 100644 --- a/src/FrontOffice.Main/Pages/Profile/Components/OrganizationChart.razor +++ b/src/FrontOffice.Main/Pages/Profile/Components/OrganizationChart.razor @@ -7,12 +7,12 @@ - 2 سطح - 3 سطح - 4 سطح - 5 سطح - 6 سطح - همه + 2 سطح + 3 سطح + 4 سطح + 5 سطح + 6 سطح + همه diff --git a/src/FrontOffice.Main/Pages/Profile/Components/OrganizationChartLevel.razor b/src/FrontOffice.Main/Pages/Profile/Components/OrganizationChartLevel.razor index fc1cf30..e9d4a2d 100644 --- a/src/FrontOffice.Main/Pages/Profile/Components/OrganizationChartLevel.razor +++ b/src/FrontOffice.Main/Pages/Profile/Components/OrganizationChartLevel.razor @@ -9,9 +9,9 @@ @if (!string.IsNullOrWhiteSpace(node.Avatar)) { - + } else diff --git a/src/FrontOffice.Main/Pages/Profile/Hub.razor b/src/FrontOffice.Main/Pages/Profile/Hub.razor new file mode 100644 index 0000000..83f24db --- /dev/null +++ b/src/FrontOffice.Main/Pages/Profile/Hub.razor @@ -0,0 +1,64 @@ +@attribute [Route(RouteConstants.Profile.Hub)] + +پروفایل + + + + + + + + + @($"{_userProfile.FirstName} {_userProfile.LastName}") + + @(_userProfile.Mobile) + + + + + + + + + + + اطلاعات شخصی + نام، کد ملی و تاریخ تولد + + + + + + + + + + + + + + آدرس‌ها + مدیریت آدرس‌های تحویل + + + + + + + + + + + + + + تنظیمات + تغییر رمز و تنظیمات حساب + + + + + + + + diff --git a/src/FrontOffice.Main/Pages/Profile/Hub.razor.cs b/src/FrontOffice.Main/Pages/Profile/Hub.razor.cs new file mode 100644 index 0000000..ab37be3 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Profile/Hub.razor.cs @@ -0,0 +1,34 @@ +using CMSMicroservice.Protobuf.Protos.User; +using FrontOffice.Main.Utilities; +using Microsoft.AspNetCore.Components; + +namespace FrontOffice.Main.Pages.Profile; + +public partial class Hub +{ + [Inject] private UserContract.UserContractClient UserContract { get; set; } = default!; + + private GetUserResponse _userProfile = new(); + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (firstRender) + { + await LoadUserProfile(); + } + } + + private async Task LoadUserProfile() + { + try + { + _userProfile = await UserContract.GetUserAsync(request: new()); + } + catch + { + _userProfile = new GetUserResponse(); + } + StateHasChanged(); + } +} diff --git a/src/FrontOffice.Main/Pages/Profile/Index.razor b/src/FrontOffice.Main/Pages/Profile/Index.razor index 86e2f93..0a0761b 100644 --- a/src/FrontOffice.Main/Pages/Profile/Index.razor +++ b/src/FrontOffice.Main/Pages/Profile/Index.razor @@ -5,299 +5,237 @@ - + + @* ═══════════════════════════════════════════════ + 1. PROFILE HEADER — Modern gradient card + ═══════════════════════════════════════════════ *@ - - - - - - - -
- @($"{_userProfile.FirstName} {_userProfile.LastName}") - @(_userProfile.Mobile) - عضو از @(_userProfile.MobileVerifiedAt?.ToDateTime().MiladiToJalali()) -
-
- - - سامانه دایا + + + + + + + + @($"{_userProfile.FirstName} {_userProfile.LastName}") + + + @(_userProfile.Mobile) + + + عضو از @(_userProfile.MobileVerifiedAt?.ToDateTime().MiladiToJalali()) + - - - - - - @if (CanShowReferralLink) - { - - - کد دعوت شما - - - اشتراک‌گذاری - - - - این کد را با دوستان خود به اشتراک بگذارید تا از خریدهای آنها پاداش دریافت کنید. - - - - - - - @if (!string.IsNullOrWhiteSpace(_copyMessage)) - { - @(_copyMessage) - } - - } - else - { - - - - - لینک دعوت شما هنوز فعال نشده است - - - برای فعال‌سازی لینک دعوت و دعوت دوستان خود به جمع مشتریان ویژه کارابازار، کافی است ابتدا پکیج پایه ۵۶ میلیونی را تهیه کنید و سپس در باشگاه مشتریان عضو شوید. - - - از مزایای ویژه عضویت در باشگاه مشتریان کارابازار سلامت بهره‌مند شده و با فعالیت در زمینه توسعه فروشگاه‌ها پاداش دریافت کنید. - - - شروع فرآیند تامین اعتبار - - - - } + + سامانه دایا +
- + @* ═══════════════════════════════════════════════ + 2. WALLET STRIP — Compact inline stats + ═══════════════════════════════════════════════ *@ - - - - - موجودی اعتباری - @_walletCredit - - - - - موجودی تخفیف باشگاه - @_walletDiscount - - - - - موجودی پاداش تیمی - @_walletNetwork - - - - جزئیات کیف پول - - + + +
+
+ + اعتباری +
+ @_walletCredit +
+ +
+
+ + تخفیف باشگاه +
+ @_walletDiscount +
+ +
+
+ + پاداش تیمی +
+ @_walletNetwork +
+ + + کیف پول + +
- + @* ═══════════════════════════════════════════════ + 3. REFERRAL SECTION + ═══════════════════════════════════════════════ *@ - - - - - - - - اطلاعات شخصی - نمایش و ویرایش اطلاعات - - - - - - - - - - آدرس‌ها - مدیریت آدرس‌های شما - - - - - - - - - - شجره‌نامه - ساختار تیم - - - - - - - - - - تنظیمات حساب - اعلان‌ها و تنظیمات - - - - - - - - - - فروشگاه - مشاهده و خرید محصولات - - - - - - - - - - کیف پول - مدیریت و تاریخچه - - - - - - - - - - درخواست برداشت - ثبت و پیگیری برداشت - - - - - - - - - - باشگاه مشتریان - عضویت و مزایا - - - - - - - - - - آمار باشگاه - آمار جزئیات باشگاه - - - - - - - - - - پاداش - درآمد و تاریخچه - - - - - - + @if (CanShowReferralLink) + { + + + + کد دعوت شما + + + اشتراک‌گذاری + + + + این کد را با دوستان خود به اشتراک بگذارید تا از خریدهای آنها پاداش دریافت کنید. + + + @if (!string.IsNullOrWhiteSpace(_copyMessage)) + { + @(_copyMessage) + } + + } + else + { + + + + لینک دعوت شما هنوز فعال نشده است + + + برای فعال‌سازی لینک دعوت، ابتدا پکیج پایه را تهیه کنید و سپس در باشگاه مشتریان عضو شوید. + + + از مزایای ویژه عضویت بهره‌مند شده و با فعالیت در زمینه توسعه فروشگاه‌ها پاداش دریافت کنید. + + + شروع فرآیند تامین اعتبار + + + } + + @* ═══════════════════════════════════════════════ + 3.5 LATEST BLOG BANNER + ═══════════════════════════════════════════════ *@ + @if (_latestPost is not null) + { + + + + + @if (!string.IsNullOrWhiteSpace(_latestPost.ThumbnailUrl)) + { +
+ +
+ } + else + { + + + + } + + جدیدترین مطلب + @_latestPost.Title + + +
+
+
+
+ } + + @* ═══════════════════════════════════════════════ + 4. QUICK ACCESS TILES — Modern grid + ═══════════════════════════════════════════════ *@ + + دسترسی سریع + + @foreach (var tile in _dashTiles) + { + + + + + + + @tile.Title + @tile.Subtitle + + + + } + + +
- +@* ── Purchase Options Dialog ── *@ خرید پکیج پایه ۵۶ میلیون تومان - - برای خرید پکیج پایه، یکی از روش‌های زیر را انتخاب کنید: - پرداخت مستقیم - - پرداخت آنی از طریق درگاه بانکی - - + پرداخت آنی از طریق درگاه بانکی + پرداخت با کارت بانکی(بزودی) - اعتبار الماسی دایا - - تأمین اعتبار از طریق خرید توکن الماس و دریافت وام - - + تأمین اعتبار از طریق خرید توکن الماس و دریافت وام + تأمین اعتبار الماسی - + بستن diff --git a/src/FrontOffice.Main/Pages/Profile/Index.razor.cs b/src/FrontOffice.Main/Pages/Profile/Index.razor.cs index 6282e3c..68f8c5e 100644 --- a/src/FrontOffice.Main/Pages/Profile/Index.razor.cs +++ b/src/FrontOffice.Main/Pages/Profile/Index.razor.cs @@ -22,10 +22,11 @@ public partial class Index [Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!; [Inject] private PackageContract.PackageContractClient PackageContract { get; set; } = default!; [Inject] private AuthService AuthService { get; set; } = default!; + [Inject] private BlogPostService BlogPostService { get; set; } = default!; - private GetUserResponse _userProfile = new(); - private UpdateUserRequest _updateUserRequest = new(); - private readonly UpdateUserRequestValidator _personalValidator = new(); + private GetUserForCustomerResponse _userProfile = new(); + private BlogPostCardDto? _latestPost; + private UpdateCustomerProfileRequest _updateUserRequest = new(); private MudForm? _personalForm; @@ -45,6 +46,22 @@ public partial class Index private bool _showPurchaseBottomSheet; private bool _isProcessingPayment; + // Dashboard Tiles + private readonly List _dashTiles = new() + { + new(RouteConstants.Profile.Personal, Icons.Material.Filled.Person, "اطلاعات شخصی", "نمایش و ویرایش", "background:rgba(99,102,241,.12); color:#6366f1;"), + new(RouteConstants.Profile.Addresses, Icons.Material.Filled.LocationOn, "آدرس‌ها", "مدیریت آدرس‌ها", "background:rgba(99,102,241,.12); color:#6366f1;"), + new(RouteConstants.Profile.Tree, Icons.Material.Filled.AccountTree, "شجره‌نامه", "ساختار تیم", "background:rgba(99,102,241,.12); color:#6366f1;"), + new(RouteConstants.Gateway.StoreChooser, Icons.Material.Filled.Store, "فروشگاه‌ها", "ورود به فروشگاه", "background:rgba(16,185,129,.12); color:#10b981;"), + new(RouteConstants.Profile.Wallet, Icons.Material.Filled.AccountBalanceWallet, "کیف پول", "مدیریت و تاریخچه", "background:rgba(16,185,129,.12); color:#10b981;"), + new(RouteConstants.Profile.WithdrawalRequests, Icons.Material.Filled.RequestPage, "درخواست برداشت", "ثبت و پیگیری", "background:rgba(245,158,11,.12); color:#f59e0b;"), + new(RouteConstants.Club.Membership, Icons.Material.Filled.CardMembership, "باشگاه مشتریان", "عضویت و مزایا", "background:rgba(168,85,247,.12); color:#a855f7;"), + new(RouteConstants.Network.Statistics, Icons.Material.Filled.Groups, "آمار باشگاه", "جزئیات باشگاه", "background:rgba(59,130,246,.12); color:#3b82f6;"), + new(RouteConstants.Commission.Dashboard, Icons.Material.Filled.Payments, "پاداش", "درآمد و تاریخچه", "background:rgba(16,185,129,.12); color:#10b981;"), + }; + + private record DashTile(string Href, string Icon, string Title, string Subtitle, string AvatarStyle); + // Address management private List _addresses = new(); private bool _isLoadingAddresses; @@ -64,6 +81,7 @@ public partial class Index await LoadUserProfile(); await LoadAddresses(); await LoadWallet(); + await LoadLatestPost(); // نمایش Modal قرارداد باشگاه اگر پکیج خریده ولی باشگاه فعال نشده await CheckAndShowClubContractModal(); @@ -134,19 +152,47 @@ public partial class Index private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price); + private async Task LoadLatestPost() + { + try + { + var posts = await BlogPostService.GetFeaturedPostsAsync(1); + if (!posts.Any()) + { + var result = await BlogPostService.GetPublishedPostsAsync(page: 1, pageSize: 1); + posts = result.Posts; + } + _latestPost = posts.FirstOrDefault(); + } + catch + { + _latestPost = null; + } + StateHasChanged(); + } + private async Task LoadUserProfile() { try { - _userProfile = await UserContract.GetUserAsync(request: new()); - _updateUserRequest = _userProfile.Adapt(); + _userProfile = await UserContract.GetUserForCustomerAsync(new()); + _updateUserRequest = new UpdateCustomerProfileRequest + { + FirstName = _userProfile.FirstName, + LastName = _userProfile.LastName, + Email = _userProfile.Email, + NationalCode = _userProfile.NationalCode, + }; if (_userProfile.BirthDate != null) + { _date = _userProfile.BirthDate.ToDateTime(); + _updateUserRequest.BirthDate = _userProfile.BirthDate; + } } catch (Exception ex) { // Handle the case when user is not authenticated or API fails - _userProfile = new GetUserResponse(); + _userProfile = new GetUserForCustomerResponse(); } StateHasChanged(); } @@ -168,7 +214,7 @@ public partial class Index if (_date != null) _updateUserRequest.BirthDate = _date.Value.DateTimeToTimestamp(); - await UserContract.UpdateUserAsync(request: _updateUserRequest); + await UserContract.UpdateCustomerProfileAsync(request: _updateUserRequest); await LoadUserProfile(); Snackbar.Add("اطلاعات شخصی با موفقیت ذخیره شد.", Severity.Success); } diff --git a/src/FrontOffice.Main/Pages/Profile/PaymentCallback.razor b/src/FrontOffice.Main/Pages/Profile/PaymentCallback.razor index a02dcd3..89e42ae 100644 --- a/src/FrontOffice.Main/Pages/Profile/PaymentCallback.razor +++ b/src/FrontOffice.Main/Pages/Profile/PaymentCallback.razor @@ -3,6 +3,7 @@ @using Blazored.LocalStorage @using CMSMicroservice.Protobuf.Protos.Package @using CMSMicroservice.Protobuf.Protos.User +@inject IJSRuntime JS نتیجه پرداخت | کارا بازار سلامت @@ -37,7 +38,7 @@ + OnClick="GoBack"> بازگشت به پروفایل } @@ -48,7 +49,7 @@ @_message + OnClick="GoBack"> بازگشت به پروفایل @@ -173,4 +174,6 @@ } private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price); + + private async Task GoBack() => await JS.InvokeVoidAsync("history.back"); } diff --git a/src/FrontOffice.Main/Pages/Profile/Personal.razor b/src/FrontOffice.Main/Pages/Profile/Personal.razor index 0e72bbc..e5687cd 100644 --- a/src/FrontOffice.Main/Pages/Profile/Personal.razor +++ b/src/FrontOffice.Main/Pages/Profile/Personal.razor @@ -4,13 +4,10 @@ - - اطلاعات شخصی - بازگشت - + - + (); + _userProfile = await UserContract.GetUserForCustomerAsync(new()); + _updateUserRequest = new UpdateCustomerProfileRequest + { + FirstName = _userProfile.FirstName, + LastName = _userProfile.LastName, + Email = _userProfile.Email, + NationalCode = _userProfile.NationalCode, + }; if (_userProfile.BirthDate != null) _date = _userProfile.BirthDate.ToDateTime(); } @@ -56,7 +59,7 @@ public partial class Personal : ComponentBase if (_date != null) _updateUserRequest.BirthDate = _date.Value.DateTimeToTimestamp(); - await UserContract.UpdateUserAsync(_updateUserRequest); + await UserContract.UpdateCustomerProfileAsync(_updateUserRequest); Snackbar.Add("اطلاعات شخصی با موفقیت ذخیره شد.", Severity.Success); } catch (Exception ex) diff --git a/src/FrontOffice.Main/Pages/Profile/Settings.razor b/src/FrontOffice.Main/Pages/Profile/Settings.razor index 264c049..83d6dc3 100644 --- a/src/FrontOffice.Main/Pages/Profile/Settings.razor +++ b/src/FrontOffice.Main/Pages/Profile/Settings.razor @@ -4,10 +4,7 @@ - - تنظیمات حساب - بازگشت - + diff --git a/src/FrontOffice.Main/Pages/Profile/Settings.razor.cs b/src/FrontOffice.Main/Pages/Profile/Settings.razor.cs index df0f1d5..df1d6b6 100644 --- a/src/FrontOffice.Main/Pages/Profile/Settings.razor.cs +++ b/src/FrontOffice.Main/Pages/Profile/Settings.razor.cs @@ -1,5 +1,4 @@ using CMSMicroservice.Protobuf.Protos.User; -using Mapster; using Microsoft.AspNetCore.Components; using MudBlazor; @@ -16,8 +15,18 @@ public partial class Settings : ComponentBase { try { - var user = await UserContract.GetUserAsync(new()); - _request = user.Adapt(); + var user = await UserContract.GetUserForCustomerAsync(new()); + _request = new UpdateUserRequest + { + Id = user.Id, + FirstName = user.FirstName, + LastName = user.LastName, + Email = user.Email, + NationalCode = user.NationalCode, + EmailNotifications = user.EmailNotifications, + SmsNotifications = user.SmsNotifications, + PushNotifications = user.PushNotifications, + }; } catch { diff --git a/src/FrontOffice.Main/Pages/Profile/Tree.razor b/src/FrontOffice.Main/Pages/Profile/Tree.razor index bb50fbf..1720fac 100644 --- a/src/FrontOffice.Main/Pages/Profile/Tree.razor +++ b/src/FrontOffice.Main/Pages/Profile/Tree.razor @@ -5,10 +5,7 @@ - - شجره‌نامه - بازگشت - + diff --git a/src/FrontOffice.Main/Pages/Profile/Wallet.razor b/src/FrontOffice.Main/Pages/Profile/Wallet.razor index a42716f..98a94c1 100644 --- a/src/FrontOffice.Main/Pages/Profile/Wallet.razor +++ b/src/FrontOffice.Main/Pages/Profile/Wallet.razor @@ -4,10 +4,7 @@ - - کیف پول - بازگشت - + diff --git a/src/FrontOffice.Main/Pages/Profile/WithdrawalRequests.razor b/src/FrontOffice.Main/Pages/Profile/WithdrawalRequests.razor index 6703628..f0fa1f5 100644 --- a/src/FrontOffice.Main/Pages/Profile/WithdrawalRequests.razor +++ b/src/FrontOffice.Main/Pages/Profile/WithdrawalRequests.razor @@ -4,10 +4,7 @@ - - درخواست‌های برداشت - بازگشت به کیف پول - + @@ -100,7 +97,7 @@ @if (_isLoading) { - + } else if (!_withdrawals.Any()) { diff --git a/src/FrontOffice.Main/Pages/RegisterWizard.razor b/src/FrontOffice.Main/Pages/RegisterWizard.razor index 93e2590..97bb624 100644 --- a/src/FrontOffice.Main/Pages/RegisterWizard.razor +++ b/src/FrontOffice.Main/Pages/RegisterWizard.razor @@ -6,10 +6,11 @@ - - ثبت‌نام سه - مرحله‌ای - + + + ثبت‌نام سه مرحله‌ای + + فقط در چند دقیقه حساب خود را فعال کنید @@ -18,34 +19,47 @@ - - - - + + + + + + مزایای ثبت‌نام آنلاین - - • تایید سریع و آنلاین شماره موبایل
- • تکمیل اطلاعات هویتی بدون مراجعه حضوری
- • دریافت نسخه‌ی دیجیتال قرارداد برای بررسی دقیق -
+ + + + تایید سریع و آنلاین شماره موبایل + + + + تکمیل اطلاعات هویتی بدون مراجعه حضوری + + + + دریافت نسخه‌ی دیجیتال قرارداد برای بررسی دقیق + +
- + @if (_completed) { - - + + + + درخواست شما ثبت شد - + تیم ما پس از بررسی اطلاعات با شما تماس خواهد گرفت. می‌توانید از طریق داشبورد وضعیت ثبت‌نام را دنبال کنید. - بازگشت به صفحه اصلی + - مراحل ثبت‌نام - ۳ مرحله + مراحل ثبت‌نام + + مرحله @(_activeStep + 1) از ۳ @if (_isSubmitting) { - + } - + - + @* Inline AuthDialog with captcha enabled *@ - + - - - + + + + + - + - + لطفاً قوانین و شرایط همکاری را با دقت مطالعه کنید و در صورت موافقت، تیک - تایید را فعال نمایید. همچنین می‌توانید نسخه‌ی قرارداد را دانلود و ذخیره - کنید. + تایید را فعال نمایید. بخشی از قوانین: @@ -106,47 +122,44 @@ - - - دانلود/پرینت قرارداد نمونه - - فرمت: PDF - + + دانلود قرارداد نمونه (PDF) + - - + @if (_isAuthenticated && _activeStep == 1) { - خروج از حساب } else { - - مرحله قبل + مرحله قبل } - + @if (_isSubmitting) + { + + } @_nextButtonText - - } diff --git a/src/FrontOffice.Main/Pages/RegisterWizard.razor.cs b/src/FrontOffice.Main/Pages/RegisterWizard.razor.cs index 54c1175..02891d0 100644 --- a/src/FrontOffice.Main/Pages/RegisterWizard.razor.cs +++ b/src/FrontOffice.Main/Pages/RegisterWizard.razor.cs @@ -23,13 +23,14 @@ public partial class RegisterWizard private bool _completed; private AuthDialog? _authDialog; private MudStepper? _mudStepper; - private UpdateUserRequest _updateUserRequest = new(); + private UpdateCustomerProfileRequest _updateUserRequest = new(); private bool _isAuthenticated; private CancellationTokenSource? _operationCts; private UserAuthInfo _userAuthInfo; private Guid signGuid = Guid.NewGuid(); private const string TokenStorageKey = "auth:token"; private readonly DialogOptions _normalWidth = new() { MaxWidth = MaxWidth.ExtraSmall, FullWidth = true }; + private bool _initialDataLoaded; [Inject] private AuthService AuthService { get; set; } = default!; [Inject] private IDeviceDetector _deviceDetector { get; set; } = default!; @@ -55,6 +56,7 @@ public partial class RegisterWizard if (_userAuthInfo.IsSignMainContract) { Navigation.NavigateTo(RouteConstants.Profile.Index); + return; } if (_activeStep == 0) @@ -63,8 +65,9 @@ public partial class RegisterWizard _activeStep = 1; await InvokeAsync(StateHasChanged); } - else if (_activeStep == 1) + else if (_activeStep == 1 && !_initialDataLoaded) { + _initialDataLoaded = true; try { var existUser = await UserContract.GetUserForCustomerAsync(new GetUserForCustomerRequest()); @@ -95,7 +98,6 @@ public partial class RegisterWizard } } - await base.OnAfterRenderAsync(firstRender); } @@ -355,7 +357,7 @@ public partial class RegisterWizard _updateUserRequest.NationalCode = _model.NationalCode.PersianToEnglish(); - await UserContract.UpdateUserAsync(request: _updateUserRequest); + await UserContract.UpdateCustomerProfileAsync(request: _updateUserRequest); Snackbar.Add("اطلاعات شخصی با موفقیت ذخیره شد.", Severity.Success); return true; } diff --git a/src/FrontOffice.Main/Pages/Store/Cart.razor b/src/FrontOffice.Main/Pages/Store/Cart.razor index 9b72d10..60f8a38 100644 --- a/src/FrontOffice.Main/Pages/Store/Cart.razor +++ b/src/FrontOffice.Main/Pages/Store/Cart.razor @@ -5,7 +5,7 @@ - سبد خرید + @if (CartData.Items.Count == 0) { @@ -30,8 +30,8 @@ - + @context.Title @if (context.Discount > 0 || !string.IsNullOrWhiteSpace(context.Created) || !string.IsNullOrWhiteSpace(context.Description)) @@ -86,8 +86,8 @@ - + @item.Title @if (item.Discount > 0) @@ -154,8 +154,8 @@ } - مبلغ قابل پرداخت: - @FormatPrice(TotalWithVAT) تومان + مبلغ قابل پرداخت: + @FormatPrice(TotalWithVAT) تومان @@ -185,8 +185,8 @@ } - مبلغ قابل پرداخت: - @FormatPrice(TotalWithVAT) تومان + مبلغ قابل پرداخت: + @FormatPrice(TotalWithVAT) تومان diff --git a/src/FrontOffice.Main/Pages/Store/Cart.razor.cs b/src/FrontOffice.Main/Pages/Store/Cart.razor.cs index 9977d37..ee5adbd 100644 --- a/src/FrontOffice.Main/Pages/Store/Cart.razor.cs +++ b/src/FrontOffice.Main/Pages/Store/Cart.razor.cs @@ -66,7 +66,7 @@ public partial class Cart : ComponentBase, IDisposable private long VATAmount => VAT.CalculateVAT(CartData.Total); private static string GetProductImageUrl(string? imageUrl) - => string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl; + => string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/'); public void Dispose() { diff --git a/src/FrontOffice.Main/Pages/Store/Categories.razor b/src/FrontOffice.Main/Pages/Store/Categories.razor index c81de5d..81cc383 100644 --- a/src/FrontOffice.Main/Pages/Store/Categories.razor +++ b/src/FrontOffice.Main/Pages/Store/Categories.razor @@ -5,9 +5,9 @@ دسته‌بندی‌ها + - دسته‌بندی محصولات از بین درخت دسته‌بندی‌ها انتخاب کنید تا محصولات آن دسته را مشاهده نمایید. diff --git a/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor b/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor index 23c3fd7..95d7974 100644 --- a/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor +++ b/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor @@ -5,6 +5,7 @@ خلاصه خرید + @@ -30,7 +31,7 @@ @foreach (var address in _addresses) { @@ -84,8 +85,8 @@ - + @item.Title @FormatPrice(item.LineTotal) @@ -118,8 +119,8 @@ } - مبلغ قابل پرداخت: - @FormatPrice(VAT.AddVAT(Cart.Total)) + مبلغ قابل پرداخت: + @FormatPrice(VAT.AddVAT(Cart.Total)) diff --git a/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor.cs b/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor.cs index ad5c5c3..7ca7aad 100644 --- a/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor.cs +++ b/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor.cs @@ -113,5 +113,5 @@ public partial class CheckoutSummary : ComponentBase private long CalculateVAT() => VAT.CalculateVAT(Cart.Total); private static string GetProductImageUrl(string? imageUrl) - => string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl; + => string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/'); } \ No newline at end of file diff --git a/src/FrontOffice.Main/Pages/Store/OrderDetail.razor b/src/FrontOffice.Main/Pages/Store/OrderDetail.razor index 98e25cf..823f6a5 100644 --- a/src/FrontOffice.Main/Pages/Store/OrderDetail.razor +++ b/src/FrontOffice.Main/Pages/Store/OrderDetail.razor @@ -6,24 +6,24 @@ @if (_loading) { - - - + } else if (_order is null) { - سفارش یافت نشد. - بازگشت + } else { + - سفارش #@_order.Id @if (_order.PaymentDate != null) { تاریخ پرداخت: @_order.PaymentDate.ToDateTime().MiladiToJalaliWithTime() @@ -46,8 +46,8 @@ else - + @context.ProductTitle @@ -65,8 +65,8 @@ else - + @it.ProductTitle @@ -100,15 +100,15 @@ else - مبلغ قابل پرداخت: - @FormatPrice(_order.VatInfo.TotalAmount) + مبلغ قابل پرداخت: + @FormatPrice(_order.VatInfo.TotalAmount) } else { - مبلغ قابل پرداخت: - @FormatPrice(subtotal) + مبلغ قابل پرداخت: + @FormatPrice(subtotal) } diff --git a/src/FrontOffice.Main/Pages/Store/OrderDetail.razor.cs b/src/FrontOffice.Main/Pages/Store/OrderDetail.razor.cs index b492929..5961f8e 100644 --- a/src/FrontOffice.Main/Pages/Store/OrderDetail.razor.cs +++ b/src/FrontOffice.Main/Pages/Store/OrderDetail.razor.cs @@ -45,6 +45,6 @@ public partial class OrderDetail : ComponentBase } private static string GetProductImageUrl(string? imageUrl) - => string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : UrlUtility.DownloadUrl+imageUrl; + => string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/'); } diff --git a/src/FrontOffice.Main/Pages/Store/OrderTracking.razor b/src/FrontOffice.Main/Pages/Store/OrderTracking.razor index 1207505..ccc6af8 100644 --- a/src/FrontOffice.Main/Pages/Store/OrderTracking.razor +++ b/src/FrontOffice.Main/Pages/Store/OrderTracking.razor @@ -4,6 +4,7 @@ پیگیری سفارش + @if (_loading) { @@ -15,7 +16,7 @@ { سفارش یافت نشد. + OnClick="GoBack"> بازگشت به سفارش‌ها } diff --git a/src/FrontOffice.Main/Pages/Store/OrderTracking.razor.cs b/src/FrontOffice.Main/Pages/Store/OrderTracking.razor.cs index 84561ed..c15808c 100644 --- a/src/FrontOffice.Main/Pages/Store/OrderTracking.razor.cs +++ b/src/FrontOffice.Main/Pages/Store/OrderTracking.razor.cs @@ -1,6 +1,7 @@ using CMSMicroservice.Protobuf.Protos.UserOrder; using FrontOffice.Main.Utilities; using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; using Messages = CMSMicroservice.Protobuf.Protos; namespace FrontOffice.Main.Pages.Store; @@ -8,6 +9,7 @@ namespace FrontOffice.Main.Pages.Store; public partial class OrderTracking : ComponentBase { [Inject] private OrderService OrderService { get; set; } = default!; + [Inject] private IJSRuntime JS { get; set; } = default!; [Parameter] public long id { get; set; } @@ -123,4 +125,6 @@ public partial class OrderTracking : ComponentBase public bool IsCurrent { get; set; } public string Date { get; set; } = ""; } + + private async Task GoBack() => await JS.InvokeVoidAsync("history.back"); } diff --git a/src/FrontOffice.Main/Pages/Store/Orders.razor b/src/FrontOffice.Main/Pages/Store/Orders.razor index a40cd5a..811451b 100644 --- a/src/FrontOffice.Main/Pages/Store/Orders.razor +++ b/src/FrontOffice.Main/Pages/Store/Orders.razor @@ -4,12 +4,10 @@ سفارشات من - سفارشات من + @if (_loading) { - - - + } else if (_orders.Count == 0) { diff --git a/src/FrontOffice.Main/Pages/Store/ProductDetail.razor b/src/FrontOffice.Main/Pages/Store/ProductDetail.razor index 79b17aa..3426a9a 100644 --- a/src/FrontOffice.Main/Pages/Store/ProductDetail.razor +++ b/src/FrontOffice.Main/Pages/Store/ProductDetail.razor @@ -6,23 +6,22 @@ @if (_loading) { - - - + } else if (_product is null) { - محصول یافت نشد. - بازگشت - + } else { + @if (_product is not null) @@ -32,7 +31,9 @@ else @if (!string.IsNullOrWhiteSpace(MainImageUrl)) { -
@MainImageAlt
+ } else { @@ -45,7 +46,7 @@ else @foreach (var item in _galleryItems) { - + }
diff --git a/src/FrontOffice.Main/Pages/Store/Products.razor b/src/FrontOffice.Main/Pages/Store/Products.razor index 88d5cca..b3cdb04 100644 --- a/src/FrontOffice.Main/Pages/Store/Products.razor +++ b/src/FrontOffice.Main/Pages/Store/Products.razor @@ -3,7 +3,18 @@ محصولات - + + + + + + فروشگاه کارا بازار سلامت + + محصولات با کیفیت با ضمانت اصالت و ارسال سریع + + + + @* ردیف اول: جستجو و دکمه‌ها *@ @@ -44,25 +55,25 @@ Margin="Margin.Dense" AnchorOrigin="Origin.BottomCenter" AdornmentIcon="@Icons.Material.Filled.Sort"> - +
گران‌ترین
- +
ارزان‌ترین
- +
جدیدترین
- +
الفبایی diff --git a/src/FrontOffice.Main/Pages/_Host.cshtml b/src/FrontOffice.Main/Pages/_Host.cshtml index e22f5de..c01aef2 100644 --- a/src/FrontOffice.Main/Pages/_Host.cshtml +++ b/src/FrontOffice.Main/Pages/_Host.cshtml @@ -8,15 +8,19 @@ + + + - - - + + + + @@ -42,6 +46,7 @@ +