diff --git a/src/FrontOffice.Main/ConfigureServices.cs b/src/FrontOffice.Main/ConfigureServices.cs index 7d9ae68..8c01087 100644 --- a/src/FrontOffice.Main/ConfigureServices.cs +++ b/src/FrontOffice.Main/ConfigureServices.cs @@ -16,6 +16,7 @@ using FrontOffice.BFF.ShopingCart.Protobuf.Protos.ShopingCart; // New Proto imports using FrontOffice.BFF.ClubMembership.Protobuf.Protos.ClubMembership; using FrontOffice.BFF.Commission.Protobuf.Protos.Commission; +using FrontOffice.BFF.Configuration.Protobuf.Protos.Configuration; using FrontOffice.BFF.NetworkMembership.Protobuf.Protos.NetworkMembership; using FrontOffice.BFF.DiscountShop.Protobuf.Protos.DiscountShop; using FrontOffice.BFF.City.Protobuf; @@ -52,10 +53,12 @@ public static class ConfigureServices services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddSingleton(); // Singleton برای cache روزانه // Package service services.AddScoped(); // New services for Club, Network, Commission services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); // Device detection: very light, dependency-free @@ -103,6 +106,7 @@ public static class ConfigureServices // New gRPC clients for Club, Network, Commission, DiscountShop services.AddScoped(CreateAuthenticatedClient); services.AddScoped(CreateAuthenticatedClient); + services.AddScoped(CreateAuthenticatedClient); services.AddScoped(CreateAuthenticatedClient); services.AddScoped(CreateAuthenticatedClient); services.AddScoped(CreateAuthenticatedClient); diff --git a/src/FrontOffice.Main/FrontOffice.Main.csproj b/src/FrontOffice.Main/FrontOffice.Main.csproj index 0515eae..3a9c916 100644 --- a/src/FrontOffice.Main/FrontOffice.Main.csproj +++ b/src/FrontOffice.Main/FrontOffice.Main.csproj @@ -20,7 +20,7 @@ - + @@ -40,8 +40,10 @@ + + diff --git a/src/FrontOffice.Main/Pages/Club/FeaturesPage.razor b/src/FrontOffice.Main/Pages/Club/FeaturesPage.razor index d27d177..8e9768b 100644 --- a/src/FrontOffice.Main/Pages/Club/FeaturesPage.razor +++ b/src/FrontOffice.Main/Pages/Club/FeaturesPage.razor @@ -1,6 +1,8 @@ @attribute [Route(RouteConstants.Club.Features)] @using FrontOffice.Main.Utilities @using MudBlazor +@inject ClubConfigurationService ClubConfigService +@inject IDialogService DialogService ویژگی‌های باشگاه مشتریان @@ -11,142 +13,181 @@ بازگشت + - چرا باشگاه مشتریان؟ + + + ویژگی‌های اختصاصی شما + - با عضویت در باشگاه مشتریان فورست، از تخفیف‌های ویژه، امتیازات خرید و خدمات اختصاصی بهره‌مند شوید. + علاوه بر مزایای پایه عضویت در باشگاه مشتریان، شما به مجموعه‌ای از ویژگی‌های اختصاصی دسترسی دارید. برخی از این ویژگی‌ها ممکن است نیاز به زمان برای فعال‌سازی داشته باشند. + با کلیک روی دکمه «جزئیات» هر ویژگی، می‌توانید اطلاعات بیشتری درباره نحوه استفاده از آن امکان را مشاهده کنید. - - - - - - - تخفیف‌های ویژه - - - • تخفیف 10% تا 30% برای تمام محصولات
- • تخفیف‌های فصلی و مناسبتی اختصاصی
- • پیشنهادات ویژه برای اعضای باشگاه
- • کد تخفیف اختصاصی ماهانه -
-
-
-
- - - - - - - امتیاز خرید - - - • دریافت امتیاز برای هر خرید
- • تبدیل امتیاز به تخفیف باشگاه
- • امتیاز ویژه در روزهای خاص
- • قابلیت انتقال امتیاز به دوستان -
-
-
-
- - - - - - - ارسال رایگان - - - • ارسال رایگان برای خریدهای بالای 500 هزار تومان
- • اولویت در ارسال سفارشات
- • ارسال اکسپرس با تخفیف 50%
- • امکان ارسال به چند آدرس -
-
-
-
- - - - - - - پشتیبانی اختصاصی - - - • پشتیبانی 24/7 برای اعضای باشگاه
- • مشاوره خرید تخصصی
- • خط ویژه پاسخگویی
- • پیگیری سریع‌تر سفارشات -
-
-
-
- - - - - - - درآمد شبکه - - - • دریافت کمیسیون از خریدهای زیرمجموعه
- • سیستم باینری درختی
- • محاسبه خودکار درآمد هفتگی
- • امکان برداشت درآمد شبکه -
-
-
-
- - - - - - - رویدادهای ویژه - - - • دعوت به رویدادهای اختصاصی
- • پیش‌فروش محصولات جدید
- • وبینارهای آموزشی رایگان
- • جوایز و مسابقات ماهانه -
-
-
-
-
- - - چگونه عضو شویم؟ - - - بسته عضویت مناسب خود را انتخاب کنید - - - هزینه عضویت را پرداخت کنید - - - عضویت شما فورا فعال می‌شود - - - از مزایای باشگاه لذت ببرید - - - + + ویژگی‌های شما + + @if (_isLoading) + { + + } + else if (_features == null || !_features.Any()) + { + + هنوز ویژگی‌ای برای شما فعال نشده است. + + } + else + { + + + @foreach (var feature in _features) + { + + + + + @feature.Title + + + جزئیات + + + + @if (feature != _features.Last()) + { + + } + } + + + } - همین حالا عضو شوید + بازگشت به صفحه باشگاه + + + + + + + @_selectedFeature?.Title + + + + @if (_selectedFeature != null) + { + + + + وضعیت: + @if (_selectedFeature.IsEnabled) + { + فعال + } + else + { + غیرفعال + } + + + + @if (!string.IsNullOrEmpty(_selectedFeature.Description)) + { + @_selectedFeature.Description + } + + + + + + تاریخ ایجاد: + + @(_selectedFeature.CreatedAt?.ToLocalTime().ToString("yyyy/MM/dd HH:mm") ?? "-") + + + + تاریخ فعال‌سازی: + + @(_selectedFeature.GrantedAt.HasValue && _selectedFeature.GrantedAt.Value > DateTime.MinValue + ? _selectedFeature.GrantedAt.Value.ToLocalTime().ToString("yyyy/MM/dd HH:mm") + : "-") + + + + + + @if (!string.IsNullOrEmpty(_selectedFeature.Notes)) + { + + توضیحات تکمیلی: + + @((MarkupString)_selectedFeature.Notes) + + } + + } + + + + بستن + + + + +@code { + private List? _features; + private bool _isLoading = true; + private bool _showDetailDialog; + private ClubFeatureDto? _selectedFeature; + + private DialogOptions _dialogOptions = new() + { + MaxWidth = MaxWidth.Small, + FullWidth = true, + CloseButton = true, + CloseOnEscapeKey = true + }; + + protected override async Task OnInitializedAsync() + { + await LoadFeatures(); + } + + private async Task LoadFeatures() + { + _isLoading = true; + try + { + _features = await ClubConfigService.GetClubFeaturesAsync(); + } + catch (Exception ex) + { + Console.WriteLine($"Error loading features: {ex.Message}"); + _features = new List(); + } + finally + { + _isLoading = false; + } + } + + private void ShowFeatureDetails(ClubFeatureDto feature) + { + _selectedFeature = feature; + _showDetailDialog = true; + } +} diff --git a/src/FrontOffice.Main/Pages/Club/MembershipPage.razor b/src/FrontOffice.Main/Pages/Club/MembershipPage.razor index a91097d..c012429 100644 --- a/src/FrontOffice.Main/Pages/Club/MembershipPage.razor +++ b/src/FrontOffice.Main/Pages/Club/MembershipPage.razor @@ -56,6 +56,19 @@ شما هنوز عضو باشگاه مشتریان نیستید. برای استفاده از مزایای ویژه، اکنون فعال کنید! + + @if (_clubConfig != null) + { + + + + هزینه عضویت در باشگاه مشتریان + شامل @((_clubConfig.MembershipGiftValue / 10000).ToString("N0")) تومان هدیه + + @((_clubConfig.ActivationFee / 10000).ToString("N0")) تومان + + + } } @@ -63,41 +76,31 @@ مزایای عضویت باشگاه - - - - - - - تخفیف ویژه - تا 30% تخفیف - - - - - - - - - - امتیاز خرید - برای هر خرید - - - - - - - - - - ارسال رایگان - برای سفارشات بالای 500 هزار تومان - - - - - + + + + + شارژ کیف پول فروشگاه تخفیفی + شارژ ۵۶ میلیون تومان کیف پول فروشگاه تخفیفی + + + + + + + عضویت در شبکه بازاریابی + عضویت در شبکه بازاریابی و دریافت پورسانت + + + + + + + جذب زیرمجموعه + امکان جذب زیرمجموعه و گسترش شبکه + + + @@ -120,7 +123,7 @@ خطا در دریافت اطلاعات عضویت. لطفا دوباره تلاش کنید. - تلاش مجدد + تلاش مجدد } diff --git a/src/FrontOffice.Main/Pages/Club/MembershipPage.razor.cs b/src/FrontOffice.Main/Pages/Club/MembershipPage.razor.cs index 3b2b37a..faad87c 100644 --- a/src/FrontOffice.Main/Pages/Club/MembershipPage.razor.cs +++ b/src/FrontOffice.Main/Pages/Club/MembershipPage.razor.cs @@ -7,23 +7,33 @@ namespace FrontOffice.Main.Pages.Club; public partial class MembershipPage : ComponentBase { [Inject] private ClubMembershipService ClubService { get; set; } = default!; + [Inject] private ClubConfigurationService ConfigService { get; set; } = default!; private ClubMembershipDto? _membership; + private ClubConfigDto? _clubConfig; private bool _isLoading = true; private bool _hasError = false; protected override async Task OnInitializedAsync() { - await LoadMembershipAsync(); + await LoadDataAsync(); } - private async Task LoadMembershipAsync() + private async Task LoadDataAsync() { try { _isLoading = true; _hasError = false; - _membership = await ClubService.GetMyMembershipAsync(); + + // بارگذاری همزمان عضویت و تنظیمات + var membershipTask = ClubService.GetMyMembershipAsync(); + var configTask = ConfigService.GetClubConfigurationAsync(); + + await Task.WhenAll(membershipTask, configTask); + + _membership = await membershipTask; + _clubConfig = await configTask; } catch (Exception ex) { @@ -38,7 +48,7 @@ public partial class MembershipPage : ComponentBase private async Task HandleActivationSuccess() { - await LoadMembershipAsync(); + await LoadDataAsync(); Snackbar.Add("عضویت باشگاه با موفقیت فعال شد!", Severity.Success); } diff --git a/src/FrontOffice.Main/Pages/Profile/Addresses.razor.cs b/src/FrontOffice.Main/Pages/Profile/Addresses.razor.cs index 69486c8..b413453 100644 --- a/src/FrontOffice.Main/Pages/Profile/Addresses.razor.cs +++ b/src/FrontOffice.Main/Pages/Profile/Addresses.razor.cs @@ -40,7 +40,7 @@ public partial class Addresses : ComponentBase { var dialog = await DialogService.ShowAsync("افزودن آدرس جدید"); var result = await dialog.Result; - if (!result.Canceled) + if (result is not null && !result.Canceled) await LoadAddresses(); } @@ -51,7 +51,7 @@ public partial class Addresses : ComponentBase { x => x.Model, address } }); var result = await dialog.Result; - if (!result.Canceled) + if (result is not null && !result.Canceled) await LoadAddresses(); } diff --git a/src/FrontOffice.Main/Pages/Profile/Components/AddAddressDialog.razor b/src/FrontOffice.Main/Pages/Profile/Components/AddAddressDialog.razor index c8b12bb..a91bd48 100644 --- a/src/FrontOffice.Main/Pages/Profile/Components/AddAddressDialog.razor +++ b/src/FrontOffice.Main/Pages/Profile/Components/AddAddressDialog.razor @@ -1,5 +1,5 @@ - + افزودن آدرس جدید diff --git a/src/FrontOffice.Main/Pages/Profile/Components/AddAddressDialog.razor.cs b/src/FrontOffice.Main/Pages/Profile/Components/AddAddressDialog.razor.cs index a092d06..1b0be38 100644 --- a/src/FrontOffice.Main/Pages/Profile/Components/AddAddressDialog.razor.cs +++ b/src/FrontOffice.Main/Pages/Profile/Components/AddAddressDialog.razor.cs @@ -9,7 +9,7 @@ namespace FrontOffice.Main.Pages.Profile.Components; public partial class AddAddressDialog : ComponentBase { - [CascadingParameter] private IDialogReference MudDialog { get; set; } = default!; + [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = default!; [Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!; [Inject] private CityProto.CityContract.CityContractClient CityContract { get; set; } = default!; @@ -19,7 +19,8 @@ public partial class AddAddressDialog : ComponentBase private CreateNewUserAddressRequest _request = new(); private CityProto.GetAllCitiesByFilterResponseModel? _selectedCity; - private async Task> SearchCities(string value, CancellationToken ct) + private async Task> SearchCities(string value, + CancellationToken ct) { if (string.IsNullOrWhiteSpace(value) || value.Length < 2) return Enumerable.Empty(); @@ -71,5 +72,6 @@ public partial class AddAddressDialog : ComponentBase } } + private void Cancel() => MudDialog.Close(DialogResult.Cancel()); } \ No newline at end of file diff --git a/src/FrontOffice.Main/Pages/Profile/Components/EditAddressDialog.razor.cs b/src/FrontOffice.Main/Pages/Profile/Components/EditAddressDialog.razor.cs index 1a61d9f..a028f88 100644 --- a/src/FrontOffice.Main/Pages/Profile/Components/EditAddressDialog.razor.cs +++ b/src/FrontOffice.Main/Pages/Profile/Components/EditAddressDialog.razor.cs @@ -10,7 +10,7 @@ namespace FrontOffice.Main.Pages.Profile.Components; public partial class EditAddressDialog : ComponentBase { - [CascadingParameter] private IDialogReference MudDialog { get; set; } = default!; + [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = default!; [Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!; [Inject] private CityProto.CityContract.CityContractClient CityContract { get; set; } = default!; diff --git a/src/FrontOffice.Main/Pages/Store/Cart.razor b/src/FrontOffice.Main/Pages/Store/Cart.razor index 8772047..9b72d10 100644 --- a/src/FrontOffice.Main/Pages/Store/Cart.razor +++ b/src/FrontOffice.Main/Pages/Store/Cart.razor @@ -141,11 +141,22 @@ @if (DeviceDetector.IsMobile()) { - - تخفیف کل: @FormatPrice(CartData.TotalDiscount) - مبلغ کل: @FormatPrice(CartData.Total) - + + + جمع کالاها: + @FormatPrice(CartData.Total) تومان + + @if (VAT.IsEnabled) + { + + مالیات (@VAT.VatPercentage%): + @FormatPrice(VATAmount) تومان + + } + + مبلغ قابل پرداخت: + @FormatPrice(TotalWithVAT) تومان + @@ -159,12 +170,29 @@ } else { - - تخفیف کل: @FormatPrice(CartData.TotalDiscount) - مبلغ کل: @FormatPrice(CartData.Total) - - + + + جمع کالاها: + @FormatPrice(CartData.Total) تومان + + @if (VAT.IsEnabled) + { + + مالیات بر ارزش افزوده (@VAT.VatPercentage%): + @FormatPrice(VATAmount) تومان + + } + + + مبلغ قابل پرداخت: + @FormatPrice(TotalWithVAT) تومان + + + + + + افزودن محصول CartService; - protected override void OnInitialized() + protected override async Task OnInitializedAsync() { + // لود سبد خرید (فقط اگر کاربر لاگین کرده باشد) + await CartService.EnsureInitializedAsync(); CartService.OnChange += StateHasChanged; + await VAT.LoadAsync(); } private async Task IncrementQty(long productId) @@ -44,7 +48,13 @@ public partial class Cart : ComponentBase, IDisposable Navigation.NavigateTo(RouteConstants.Store.CheckoutSummary); } - private static string FormatPrice(long price) => string.Format("{0:N0} ", price); + private string FormatPrice(long price) => $"{price:N0} "; + + private string FormatPriceWithVAT(long price) => $"{VAT.AddVAT(price):N0} "; + + private long TotalWithVAT => VAT.AddVAT(CartData.Total); + + private long VATAmount => VAT.CalculateVAT(CartData.Total); private static string GetProductImageUrl(string? imageUrl) => string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl; diff --git a/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor b/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor index 794bebb..0563e91 100644 --- a/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor +++ b/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor @@ -108,14 +108,17 @@ جمع کالاها: @FormatPrice(Cart.Total) - - مالیات بر ارزش افزوده (۹%): - @FormatPrice(CalculateVAT()) - + @if (VAT.IsEnabled) + { + + مالیات بر ارزش افزوده (@VAT.VatPercentage%): + @FormatPrice(CalculateVAT()) + + } مبلغ قابل پرداخت: - @FormatPrice(Cart.Total + CalculateVAT()) + @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 4de3f0b..2192a91 100644 --- a/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor.cs +++ b/src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor.cs @@ -10,6 +10,7 @@ public partial class CheckoutSummary : ComponentBase { [Inject] private CartService Cart { get; set; } = default!; [Inject] private OrderService OrderService { get; set; } = default!; + [Inject] private VATService VAT { get; set; } = default!; [Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!; [Inject] private UserOrderContract.UserOrderContractClient UserOrderContract { get; set; } = default!; // Snackbar and Navigation are injected via _Imports.razor @@ -25,8 +26,11 @@ public partial class CheckoutSummary : ComponentBase protected override async Task OnInitializedAsync() { + // لود سبد خرید (فقط اگر کاربر لاگین کرده باشد) + await Cart.EnsureInitializedAsync(); await LoadAddresses(); await LoadWalletBalance(); + await VAT.LoadAsync(); } private async Task LoadWalletBalance() @@ -92,9 +96,9 @@ public partial class CheckoutSummary : ComponentBase private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price); /// - /// محاسبه مالیات بر ارزش افزوده (۹%) + /// محاسبه مالیات بر ارزش افزوده /// - private long CalculateVAT() => (long)(Cart.Total * 0.09); + private long CalculateVAT() => VAT.CalculateVAT(Cart.Total); private static string GetProductImageUrl(string? imageUrl) => string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl; diff --git a/src/FrontOffice.Main/Pages/Store/OrderDetail.razor b/src/FrontOffice.Main/Pages/Store/OrderDetail.razor index 742a2c9..cdff00e 100644 --- a/src/FrontOffice.Main/Pages/Store/OrderDetail.razor +++ b/src/FrontOffice.Main/Pages/Store/OrderDetail.razor @@ -78,8 +78,6 @@ else @* نمایش جزئیات مالی *@ @{ var subtotal = _order.FactorDetails.Sum(s => s.UnitPrice.Value * s.Count.Value); - var vatAmount = (long)(subtotal * 0.09); - var totalWithVat = subtotal + vatAmount; } @@ -87,15 +85,26 @@ else @FormatPrice(subtotal) - - مالیات بر ارزش افزوده (۹%): - @FormatPrice(vatAmount) - - - - مبلغ قابل پرداخت: - @FormatPrice(totalWithVat) - + @if (_order.VatInfo is not null) + { + var vatPercent = (int)(_order.VatInfo.VatRate * 100); + + مالیات بر ارزش افزوده (@vatPercent%): + @FormatPrice(_order.VatInfo.VatAmount) + + + + مبلغ قابل پرداخت: + @FormatPrice(_order.VatInfo.TotalAmount) + + } + else + { + + مبلغ قابل پرداخت: + @FormatPrice(subtotal) + + } diff --git a/src/FrontOffice.Main/Pages/Store/Orders.razor.cs b/src/FrontOffice.Main/Pages/Store/Orders.razor.cs index 6053be7..35ff7b0 100644 --- a/src/FrontOffice.Main/Pages/Store/Orders.razor.cs +++ b/src/FrontOffice.Main/Pages/Store/Orders.razor.cs @@ -9,6 +9,7 @@ namespace FrontOffice.Main.Pages.Store; public partial class Orders : ComponentBase { [Inject] private OrderService OrderService { get; set; } = default!; + [Inject] private VATService VAT { get; set; } = default!; private List _orders = new(); private bool _loading; @@ -18,6 +19,7 @@ public partial class Orders : ComponentBase _loading = true; try { + await VAT.LoadAsync(); _orders = await OrderService.GetOrdersAsync(); } catch (Exception ex) diff --git a/src/FrontOffice.Main/Pages/Store/ProductDetail.razor b/src/FrontOffice.Main/Pages/Store/ProductDetail.razor index 71da6d1..79b17aa 100644 --- a/src/FrontOffice.Main/Pages/Store/ProductDetail.razor +++ b/src/FrontOffice.Main/Pages/Store/ProductDetail.razor @@ -77,18 +77,51 @@ else @* *@ @* } *@ - @FormatPrice(_product.Price) + + @FormatPrice(_product.Price) + @if (VAT.IsEnabled) + { + + (شامل @VAT.VatPercentage% مالیات بر ارزش افزوده) + + } + + + + @if (IsInStock) + { + + + موجود در انبار (@_product.RemainingCount عدد) + + } + else + { + + + ناموجود + + } - - - افزودن به - سبد - - + @if (IsInStock) + { + + + افزودن به + سبد + + + } + else + { + + این محصول در حال حاضر موجود نیست. + + } @@ -103,51 +136,58 @@ else - - - - @if (HasDiscount && OriginalPrice is not null) - { - - @FormatPrice(OriginalPrice.Value) - @($"٪{_product!.Discount}") - - } - @FormatPrice(TotalPrice) - - - - @if (IsInCart) - { - - - - @CurrentCartQuantity - - - - } - else - { - + @if (IsInStock) + { + + - - - افزودن به سبد - + @if (HasDiscount && OriginalPrice is not null) + { + + @FormatPrice(OriginalPrice.Value) + @($"٪{_product!.Discount}") + + } + @FormatPrice(TotalPrice) - } - + @if (IsInCart) + { + + + + @CurrentCartQuantity + + + + } + else + { + + + + افزودن به سبد + + + + } + + } + else + { + + این محصول ناموجود است + + } diff --git a/src/FrontOffice.Main/Pages/Store/ProductDetail.razor.cs b/src/FrontOffice.Main/Pages/Store/ProductDetail.razor.cs index 387d203..3a8ff5a 100644 --- a/src/FrontOffice.Main/Pages/Store/ProductDetail.razor.cs +++ b/src/FrontOffice.Main/Pages/Store/ProductDetail.razor.cs @@ -11,6 +11,7 @@ public partial class ProductDetail : ComponentBase, IDisposable { [Inject] private ProductService ProductService { get; set; } = default!; [Inject] private CartService Cart { get; set; } = default!; + [Inject] private VATService VAT { get; set; } = default!; [Parameter] public long id { get; set; } @@ -18,12 +19,18 @@ public partial class ProductDetail : ComponentBase, IDisposable private bool _loading; private int _qty = 1; private const int MinQty = 1; - private const int MaxQty = 20; + + // حداکثر تعداد قابل خرید بر اساس موجودی انبار + private int MaxQty => _product?.RemainingCount > 0 ? _product.RemainingCount : 0; + + // آیا محصول موجود است + private bool IsInStock => _product is not null && _product.RemainingCount > 0; + private IReadOnlyList _galleryItems = Array.Empty(); private IReadOnlyList _categoryPaths = Array.Empty(); private ProductGalleryImage? _selectedGalleryImage; private readonly List _breadcrumbItems = new(); - private long TotalPrice => (_product?.Price ?? 0) * _qty; + private long TotalPrice => VAT.AddVAT((_product?.Price ?? 0) * _qty); private bool HasDiscount => _product is { Discount: > 0 and < 100 }; private long? OriginalPrice => HasDiscount && _product is not null @@ -44,8 +51,13 @@ public partial class ProductDetail : ComponentBase, IDisposable protected override async Task OnInitializedAsync() { + // لود سبد خرید (فقط اگر کاربر لاگین کرده باشد) + await Cart.EnsureInitializedAsync(); Cart.OnChange += HandleCartChanged; + // بارگذاری نرخ VAT + await VAT.LoadAsync(); + _loading = true; _product = await ProductService.GetByIdAsync(id); _loading = false; @@ -127,7 +139,9 @@ public partial class ProductDetail : ComponentBase, IDisposable Cart.OnChange -= HandleCartChanged; } - private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price); + private string FormatPrice(long price) => $"{VAT.AddVAT(price):N0} تومان"; + + private string FormatPriceWithoutVAT(long price) => $"{price:N0} تومان"; private static IReadOnlyList BuildGalleryItems(Product product) { diff --git a/src/FrontOffice.Main/Pages/Store/Products.razor b/src/FrontOffice.Main/Pages/Store/Products.razor index bb900e9..4207ed2 100644 --- a/src/FrontOffice.Main/Pages/Store/Products.razor +++ b/src/FrontOffice.Main/Pages/Store/Products.razor @@ -59,8 +59,20 @@ -
- +
+ @if (p.RemainingCount <= 0) + { +
+ ناموجود +
+ } + else if (p.RemainingCount <= 5) + { + + فقط @p.RemainingCount عدد + + }
@p.Title @@ -73,7 +85,9 @@ @* Href="@($"{RouteConstants.Store.ProductDetail}{p.Id}")">جزئیات *@ @* *@ افزودن + StartIcon="@Icons.Material.Filled.AddShoppingCart" + Disabled="@(p.RemainingCount <= 0)"> + @(p.RemainingCount <= 0 ? "ناموجود" : "افزودن") diff --git a/src/FrontOffice.Main/Pages/Store/Products.razor.cs b/src/FrontOffice.Main/Pages/Store/Products.razor.cs index da2c995..7a6dcef 100644 --- a/src/FrontOffice.Main/Pages/Store/Products.razor.cs +++ b/src/FrontOffice.Main/Pages/Store/Products.razor.cs @@ -12,6 +12,7 @@ public partial class Products : ComponentBase, IDisposable [Inject] private ProductService ProductService { get; set; } = default!; [Inject] private CategoryService CategoryService { get; set; } = default!; [Inject] private CartService Cart { get; set; } = default!; + [Inject] private VATService VAT { get; set; } = default!; private string _query = string.Empty; private bool _loading; @@ -21,8 +22,11 @@ public partial class Products : ComponentBase, IDisposable protected override async Task OnInitializedAsync() { + // لود سبد خرید (فقط اگر کاربر لاگین کرده باشد) + await Cart.EnsureInitializedAsync(); Cart.OnChange += StateHasChanged; Navigation.LocationChanged += HandleLocationChanged; + await VAT.LoadAsync(); await Load(); } @@ -47,7 +51,7 @@ public partial class Products : ComponentBase, IDisposable await Cart.Add(p, 1); } - private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price); + private string FormatPrice(long price) => $"{VAT.AddVAT(price):N0} تومان"; public void Dispose() { diff --git a/src/FrontOffice.Main/Shared/MainLayout.razor b/src/FrontOffice.Main/Shared/MainLayout.razor index 9b4093a..4d31492 100644 --- a/src/FrontOffice.Main/Shared/MainLayout.razor +++ b/src/FrontOffice.Main/Shared/MainLayout.razor @@ -58,11 +58,11 @@
@if (_isAuthenticated) { - - - - + @* *@ + @* *@ + @* *@ + پروفایل diff --git a/src/FrontOffice.Main/Shared/MainLayout.razor.cs b/src/FrontOffice.Main/Shared/MainLayout.razor.cs index 9a56b70..ec1dfd3 100644 --- a/src/FrontOffice.Main/Shared/MainLayout.razor.cs +++ b/src/FrontOffice.Main/Shared/MainLayout.razor.cs @@ -38,6 +38,8 @@ public partial class MainLayout : IDisposable if (_isAuthenticated) { + // لود سبد خرید فقط برای کاربر لاگین شده + await CartService.EnsureInitializedAsync(); CartService.OnChange += OnCartChanged; _cartCount = CartService.Count; } diff --git a/src/FrontOffice.Main/Utilities/CartService.cs b/src/FrontOffice.Main/Utilities/CartService.cs index 8df15ad..7270681 100644 --- a/src/FrontOffice.Main/Utilities/CartService.cs +++ b/src/FrontOffice.Main/Utilities/CartService.cs @@ -1,6 +1,7 @@ using DateTimeConverterCL; using FrontOffice.BFF.ShopingCart.Protobuf.Protos.ShopingCart; using Google.Protobuf.WellKnownTypes; +using Blazored.LocalStorage; namespace FrontOffice.Main.Utilities; @@ -16,13 +17,18 @@ public record CartItem(long cartId,long ProductId, string Title, string ImageUrl public class CartService { private readonly ShopingCartContract.ShopingCartContractClient _client; + private readonly ILocalStorageService _localStorage; private readonly List _items = new(); + private const string TokenStorageKey = "auth:token"; + private bool _isInitialized; + public event Action? OnChange; - public CartService(ShopingCartContract.ShopingCartContractClient client) + public CartService(ShopingCartContract.ShopingCartContractClient client, ILocalStorageService localStorage) { _client = client; - _ = LoadFromServerAsync(); + _localStorage = localStorage; + // لود سبد خرید به صورت lazy انجام میشه - نه در constructor } public IReadOnlyList Items => _items.AsReadOnly(); @@ -33,6 +39,13 @@ public class CartService public async Task Add(Product product, int quantity = 1) { if (quantity <= 0) return; + + // اطمینان از لود شدن سبد خرید + await EnsureInitializedAsync(); + + // چک لاگین بودن کاربر + if (!await IsAuthenticatedAsync()) return; + var existing = _items.FirstOrDefault(i => i.ProductId == product.Id); int newQuantity; @@ -82,11 +95,14 @@ public class CartService public async Task UpdateQuantity(long productId, int quantity) { + // چک لاگین بودن کاربر + if (!await IsAuthenticatedAsync()) return; + var existing = _items.FirstOrDefault(i => i.ProductId == productId); if (existing is null) return; if (quantity <= 0) { - Remove(existing.cartId); + await Remove(existing.cartId); return; } var idx = _items.IndexOf(existing); @@ -109,6 +125,9 @@ public class CartService public async Task Remove(long cartId) { + // چک لاگین بودن کاربر + if (!await IsAuthenticatedAsync()) return; + _items.RemoveAll(i => i.cartId == cartId); Notify(); @@ -129,6 +148,9 @@ public class CartService public async Task Clear() { + // چک لاگین بودن کاربر + if (!await IsAuthenticatedAsync()) return; + var productIds = _items.Select(i => i.ProductId).ToList(); _items.Clear(); Notify(); @@ -152,8 +174,44 @@ public class CartService private void Notify() => OnChange?.Invoke(); + /// + /// بررسی اینکه کاربر لاگین کرده یا نه + /// + private async Task IsAuthenticatedAsync() + { + try + { + var token = await _localStorage.GetItemAsync(TokenStorageKey); + return !string.IsNullOrWhiteSpace(token); + } + catch + { + return false; + } + } + + /// + /// اطمینان از لود شدن سبد خرید - فقط اگر کاربر لاگین کرده باشد + /// + public async Task EnsureInitializedAsync() + { + if (_isInitialized) return; + + if (await IsAuthenticatedAsync()) + { + await LoadFromServerAsync(); + } + _isInitialized = true; + } + private async Task LoadFromServerAsync() { + // ابتدا چک کن که کاربر لاگین کرده + if (!await IsAuthenticatedAsync()) + { + return; + } + try { var response = await _client.GetAllUserCartAsync(new Empty()); diff --git a/src/FrontOffice.Main/Utilities/ClubConfigurationService.cs b/src/FrontOffice.Main/Utilities/ClubConfigurationService.cs new file mode 100644 index 0000000..b1ec45b --- /dev/null +++ b/src/FrontOffice.Main/Utilities/ClubConfigurationService.cs @@ -0,0 +1,68 @@ +using FrontOffice.BFF.Configuration.Protobuf.Protos.Configuration; + +namespace FrontOffice.Main.Utilities; + +/// +/// سرویس تنظیمات باشگاه مشتریان +/// +public class ClubConfigurationService +{ + private readonly ConfigurationContract.ConfigurationContractClient _client; + + public ClubConfigurationService(ConfigurationContract.ConfigurationContractClient client) + { + _client = client; + } + + /// + /// دریافت تنظیمات باشگاه مشتریان + /// + public async Task GetClubConfigurationAsync() + { + var response = await _client.GetClubConfigurationAsync(new Google.Protobuf.WellKnownTypes.Empty()); + + return new ClubConfigDto + { + ActivationFee = response.ActivationFee, + MembershipGiftValue = response.MembershipGiftValue + }; + } + + /// + /// دریافت لیست فیچرهای باشگاه برای کاربر جاری + /// + public async Task> GetClubFeaturesAsync() + { + var response = await _client.GetClubFeaturesAsync(new Google.Protobuf.WellKnownTypes.Empty()); + + return response.Features.Select(f => new ClubFeatureDto + { + Id = f.Id, + Title = f.Title, + Description = f.Description, + IsEnabled = f.IsEnabled, + DisplayOrder = f.DisplayOrder, + GrantedAt = f.GrantedAt?.ToDateTime(), + CreatedAt = f.CreatedAt?.ToDateTime(), + Notes = f.Notes + }).ToList(); + } +} + +public class ClubConfigDto +{ + public long ActivationFee { get; set; } + public long MembershipGiftValue { get; set; } +} + +public class ClubFeatureDto +{ + public long Id { get; set; } + public string Title { get; set; } = string.Empty; + public string? Description { get; set; } + public bool IsEnabled { get; set; } + public int DisplayOrder { get; set; } + public DateTime? GrantedAt { get; set; } + public DateTime? CreatedAt { get; set; } + public string? Notes { get; set; } +} diff --git a/src/FrontOffice.Main/Utilities/OrderService.cs b/src/FrontOffice.Main/Utilities/OrderService.cs index 6112233..f7c38f7 100644 --- a/src/FrontOffice.Main/Utilities/OrderService.cs +++ b/src/FrontOffice.Main/Utilities/OrderService.cs @@ -79,4 +79,25 @@ public class OrderService return order; } + + /// + /// دریافت نرخ مالیات بر ارزش افزوده + /// + public async Task GetVATRateAsync() + { + try + { + return await _userOrderContractClient.GetVATRateAsync(new Google.Protobuf.WellKnownTypes.Empty()); + } + catch + { + // در صورت خطا، مقادیر پیش‌فرض + return new GetVATRateResponse + { + VatRate = 0.10, + VatPercentage = 10, + IsEnabled = true + }; + } + } } \ No newline at end of file diff --git a/src/FrontOffice.Main/Utilities/VATService.cs b/src/FrontOffice.Main/Utilities/VATService.cs new file mode 100644 index 0000000..a62219e --- /dev/null +++ b/src/FrontOffice.Main/Utilities/VATService.cs @@ -0,0 +1,144 @@ +using Blazored.LocalStorage; +using FrontOffice.BFF.UserOrder.Protobuf.Protos.UserOrder; +using Microsoft.Extensions.DependencyInjection; + +namespace FrontOffice.Main.Utilities; + +/// +/// سرویس مدیریت مالیات بر ارزش افزوده +/// نرخ VAT یک بار در روز از سرور گرفته و در LocalStorage ذخیره می‌شود +/// +public class VATService +{ + private readonly IServiceProvider _serviceProvider; + + private const string VAT_RATE_KEY = "vat_rate"; + private const string VAT_PERCENTAGE_KEY = "vat_percentage"; + private const string VAT_DATE_KEY = "vat_date"; + + // مقادیر پیش‌فرض + private double _vatRate = 0.0999; + private int _vatPercentage = 99; + private bool _isEnabled = true; + private bool _isLoaded = false; + + public VATService(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + /// + /// نرخ مالیات (مثلاً 0.09) + /// + public double VatRate => _vatRate; + + /// + /// درصد مالیات (مثلاً 9) + /// + public int VatPercentage => _vatPercentage; + + /// + /// آیا مالیات فعال است + /// + public bool IsEnabled => _isEnabled; + + /// + /// بارگذاری نرخ VAT - اگر امروز گرفته شده از cache، وگرنه از سرور + /// + public async Task LoadAsync() + { + if (_isLoaded) return; + + try + { + using var scope = _serviceProvider.CreateScope(); + var localStorage = scope.ServiceProvider.GetRequiredService(); + + // چک کن آیا امروز قبلاً گرفته شده + var savedDate = await localStorage.GetItemAsStringAsync(VAT_DATE_KEY); + var today = DateTime.Today.ToString("yyyy-MM-dd"); + + if (savedDate == today) + { + // از cache بخوان + var savedRate = await localStorage.GetItemAsync(VAT_RATE_KEY); + var savedPercentage = await localStorage.GetItemAsync(VAT_PERCENTAGE_KEY); + if (savedRate > 0 && savedPercentage > 0) + { + _vatRate = savedRate; + _vatPercentage = savedPercentage; + _isLoaded = true; + return; + } + } + + // از سرور بگیر + await RefreshFromServerAsync(); + } + catch + { + // در صورت خطا از مقادیر پیش‌فرض استفاده شود + _isLoaded = true; + } + } + + /// + /// بروزرسانی از سرور و ذخیره در cache + /// + public async Task RefreshFromServerAsync() + { + try + { + // ایجاد scope برای دریافت client و localStorage + using var scope = _serviceProvider.CreateScope(); + var client = scope.ServiceProvider.GetRequiredService(); + var localStorage = scope.ServiceProvider.GetRequiredService(); + + var response = await client.GetVATRateAsync(new Google.Protobuf.WellKnownTypes.Empty()); + + _vatRate = response.VatRate; + _vatPercentage = response.VatPercentage; + _isEnabled = response.IsEnabled; + + // ذخیره در LocalStorage + await localStorage.SetItemAsync(VAT_RATE_KEY, _vatRate); + await localStorage.SetItemAsync(VAT_PERCENTAGE_KEY, _vatPercentage); + await localStorage.SetItemAsStringAsync(VAT_DATE_KEY, DateTime.Today.ToString("yyyy-MM-dd")); + + _isLoaded = true; + } + catch + { + // مقادیر پیش‌فرض + _isLoaded = true; + } + } + + /// + /// محاسبه مبلغ مالیات + /// + public long CalculateVAT(long amount) => _isEnabled ? (long)(amount * _vatRate) : 0; + + /// + /// محاسبه قیمت با احتساب مالیات + /// + public long AddVAT(long amount) => _isEnabled ? amount + CalculateVAT(amount) : amount; + + /// + /// فرمت قیمت با نمایش مالیات + /// + public string FormatPriceWithVAT(long price) + { + var priceWithVat = AddVAT(price); + return $"{priceWithVat:N0} تومان"; + } + + /// + /// فرمت قیمت با جزئیات مالیات + /// + public (long BasePrice, long VatAmount, long TotalPrice) GetPriceBreakdown(long basePrice) + { + var vatAmount = CalculateVAT(basePrice); + return (basePrice, vatAmount, basePrice + vatAmount); + } +}