Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 58037f0b49 | |||
| aae4b2693d | |||
| 61fd805c75 | |||
| ef6d233bbc | |||
| 3d84e726db | |||
| 13091849cf | |||
| 8e98c50bdf | |||
| 6123e47968 | |||
| acad1a719f | |||
| c7e6d01c11 | |||
| c764614d26 | |||
| 9203dc7924 | |||
| b64d99c5b3 | |||
| 4989cd00e0 | |||
| a708727241 | |||
| bed35935ad | |||
| 2339ca99b4 | |||
| ceaaac3e23 | |||
| 7f5a4f2c37 | |||
| db35814ce3 | |||
| 06100ca6dd |
@@ -1,3 +1,4 @@
|
|||||||
# Docs moved to totalDoc
|
# Docs moved to totalDoc
|
||||||
|
|
||||||
See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation.
|
See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation.
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
|||||||
@@ -71,80 +71,6 @@
|
|||||||
</MudStack>
|
</MudStack>
|
||||||
}
|
}
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
<!-- Address Selection -->
|
|
||||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
|
||||||
<MudText Typo="Typo.h5" Class="mb-4">انتخاب آدرس</MudText>
|
|
||||||
|
|
||||||
@if (_isLoadingAddresses)
|
|
||||||
{
|
|
||||||
<MudStack AlignItems="AlignItems.Center" Class="py-4">
|
|
||||||
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Medium" />
|
|
||||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-2">بارگذاری آدرسها...</MudText>
|
|
||||||
</MudStack>
|
|
||||||
}
|
|
||||||
else if (_addresses.Any())
|
|
||||||
{
|
|
||||||
<MudStack Spacing="2">
|
|
||||||
@foreach (var address in _addresses)
|
|
||||||
{
|
|
||||||
<MudPaper Outlined="@(_selectedAddress?.Id != address.Id)"
|
|
||||||
Elevation="@(_selectedAddress?.Id == address.Id ? 4 : 0)"
|
|
||||||
Class="pa-3 rounded-xl cursor-pointer"
|
|
||||||
Style="@(_selectedAddress?.Id == address.Id ? "border: 2px solid var(--mud-palette-primary);" : "")"
|
|
||||||
@onclick="() => SetAddressAsDefault(address.Id)">
|
|
||||||
<MudStack Spacing="1">
|
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
|
||||||
<MudCheckBox @bind-Value="address.IsDefault"
|
|
||||||
Disabled="true"
|
|
||||||
Color="Color.Primary" />
|
|
||||||
<MudText Typo="Typo.subtitle2">@(address.Title)</MudText>
|
|
||||||
@if (address.IsDefault)
|
|
||||||
{
|
|
||||||
<MudChip T="string" Color="Color.Success" Variant="Variant.Filled" Size="Size.Small">پیشفرض</MudChip>
|
|
||||||
}
|
|
||||||
@if (!address.IsDefault)
|
|
||||||
{
|
|
||||||
@if (_isSettingDefaultAddress && _settingDefaultAddressId == address.Id)
|
|
||||||
{
|
|
||||||
<MudProgressCircular Size="Size.Small" Color="Color.Primary" Indeterminate="true" />
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<MudButton Variant="Variant.Text"
|
|
||||||
Color="Color.Primary"
|
|
||||||
Size="Size.Small"
|
|
||||||
OnClick="() => SetAddressAsDefault(address.Id)">
|
|
||||||
تنظیم به عنوان پیشفرض
|
|
||||||
</MudButton>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</MudStack>
|
|
||||||
<MudStack Row="true">
|
|
||||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">@(address.Address)</MudText>
|
|
||||||
<MudSpacer />
|
|
||||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">کد پستی: @(address.PostalCode)</MudText>
|
|
||||||
</MudStack>
|
|
||||||
</MudStack>
|
|
||||||
</MudPaper>
|
|
||||||
}
|
|
||||||
</MudStack>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<MudStack AlignItems="AlignItems.Center" Class="py-4">
|
|
||||||
<MudIcon Icon="@Icons.Material.Filled.LocationOff" Size="Size.Large" Color="Color.Default" />
|
|
||||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mt-2">آدرسی ثبت نشده است.</MudText>
|
|
||||||
<MudButton Variant="Variant.Outlined"
|
|
||||||
Color="Color.Primary"
|
|
||||||
StartIcon="@Icons.Material.Filled.Add"
|
|
||||||
OnClick="() => Navigation.NavigateTo(RouteConstants.Profile.Index)"
|
|
||||||
Class="mt-2">
|
|
||||||
افزودن آدرس
|
|
||||||
</MudButton>
|
|
||||||
</MudStack>
|
|
||||||
}
|
|
||||||
</MudPaper>
|
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
|
||||||
@@ -241,7 +167,7 @@
|
|||||||
@if (!CanProceedToPayment)
|
@if (!CanProceedToPayment)
|
||||||
{
|
{
|
||||||
<MudText Typo="Typo.caption" Color="Color.Error" Align="Align.Center">
|
<MudText Typo="Typo.caption" Color="Color.Error" Align="Align.Center">
|
||||||
لطفاً پکیج و آدرس را انتخاب کنید.
|
لطفاً پکیج را انتخاب کنید.
|
||||||
</MudText>
|
</MudText>
|
||||||
}
|
}
|
||||||
</MudStack>
|
</MudStack>
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
|
|
||||||
using CMSMicroservice.Protobuf.Protos.Package;
|
using CMSMicroservice.Protobuf.Protos.Package;
|
||||||
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
|
||||||
using FrontOffice.Main.Utilities;
|
using FrontOffice.Main.Utilities;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
@@ -13,14 +11,10 @@ public partial class Checkout
|
|||||||
{
|
{
|
||||||
[Inject] private PackageService PackageService { get; set; } = default!;
|
[Inject] private PackageService PackageService { get; set; } = default!;
|
||||||
[Inject] private PackageContract.PackageContractClient PackageClient { get; set; } = default!;
|
[Inject] private PackageContract.PackageContractClient PackageClient { get; set; } = default!;
|
||||||
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
|
||||||
|
|
||||||
[Parameter] public long? PackageId { get; set; }
|
[Parameter] public long? PackageId { get; set; }
|
||||||
|
|
||||||
private Pack? _selectedPackage;
|
private Pack? _selectedPackage;
|
||||||
private List<CustomerAddressModel> _addresses = new();
|
|
||||||
private CustomerAddressModel? _selectedAddress;
|
|
||||||
private bool _isLoadingAddresses;
|
|
||||||
private bool _isProcessingPayment;
|
private bool _isProcessingPayment;
|
||||||
|
|
||||||
// Discount code
|
// Discount code
|
||||||
@@ -31,16 +25,11 @@ public partial class Checkout
|
|||||||
private long _discountAmount;
|
private long _discountAmount;
|
||||||
private long _finalPrice;
|
private long _finalPrice;
|
||||||
|
|
||||||
// Address management
|
private bool CanProceedToPayment => _selectedPackage != null;
|
||||||
private bool _isSettingDefaultAddress;
|
|
||||||
private long? _settingDefaultAddressId;
|
|
||||||
|
|
||||||
private bool CanProceedToPayment => _selectedPackage != null && _selectedAddress != null;
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
await LoadPackageDetails();
|
await LoadPackageDetails();
|
||||||
await LoadAddresses();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadPackageDetails()
|
private async Task LoadPackageDetails()
|
||||||
@@ -72,60 +61,6 @@ public partial class Checkout
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadAddresses()
|
|
||||||
{
|
|
||||||
_isLoadingAddresses = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var response = await UserAddressContract.GetCustomerAddressesAsync(request: new());
|
|
||||||
if (response?.Models?.Any() == true)
|
|
||||||
{
|
|
||||||
_addresses = response.Models.ToList();
|
|
||||||
// Select default address if available
|
|
||||||
_selectedAddress = _addresses.FirstOrDefault(a => a.IsDefault) ?? _addresses.First();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_addresses = new List<CustomerAddressModel>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Snackbar.Add($"خطا در بارگذاری آدرسها: {ex.Message}", Severity.Error);
|
|
||||||
_addresses = new List<CustomerAddressModel>();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_isLoadingAddresses = false;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SetAddressAsDefault(long addressId)
|
|
||||||
{
|
|
||||||
if (_isSettingDefaultAddress) return;
|
|
||||||
|
|
||||||
_isSettingDefaultAddress = true;
|
|
||||||
_settingDefaultAddressId = addressId;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await UserAddressContract.SetAddressAsDefaultAsync(new() { Id = addressId });
|
|
||||||
await LoadAddresses(); // Reload addresses to reflect the change
|
|
||||||
Snackbar.Add("آدرس پیشفرض با موفقیت تغییر یافت.", Severity.Success);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Snackbar.Add($"خطا در تغییر آدرس پیشفرض: {ex.Message}", Severity.Error);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_isSettingDefaultAddress = false;
|
|
||||||
_settingDefaultAddressId = null;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ApplyDiscountCode()
|
private async Task ApplyDiscountCode()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(_discountCode))
|
if (string.IsNullOrWhiteSpace(_discountCode))
|
||||||
@@ -157,7 +92,7 @@ public partial class Checkout
|
|||||||
_finalPrice = _selectedPackage!.Price;
|
_finalPrice = _selectedPackage!.Price;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
_discountMessage = "خطا در اعمال کد تخفیف.";
|
_discountMessage = "خطا در اعمال کد تخفیف.";
|
||||||
_discountApplied = false;
|
_discountApplied = false;
|
||||||
@@ -175,9 +110,9 @@ public partial class Checkout
|
|||||||
{
|
{
|
||||||
if (_isProcessingPayment) return;
|
if (_isProcessingPayment) return;
|
||||||
|
|
||||||
if (!CanProceedToPayment || _selectedPackage == null || _selectedAddress == null)
|
if (!CanProceedToPayment || _selectedPackage == null)
|
||||||
{
|
{
|
||||||
Snackbar.Add("لطفاً پکیج و آدرس را انتخاب کنید.", Severity.Warning);
|
Snackbar.Add("لطفاً پکیج را انتخاب کنید.", Severity.Warning);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,9 +154,9 @@ public partial class Checkout
|
|||||||
|
|
||||||
private async Task DayaLoanPayment()
|
private async Task DayaLoanPayment()
|
||||||
{
|
{
|
||||||
if (_selectedAddress == null)
|
if (_selectedPackage == null)
|
||||||
{
|
{
|
||||||
Snackbar.Add("لطفاً آدرس را انتخاب کنید.", Severity.Warning);
|
Snackbar.Add("لطفاً پکیج را انتخاب کنید.", Severity.Warning);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,11 @@ public partial class Checkout
|
|||||||
}
|
}
|
||||||
await VAT.LoadAsync();
|
await VAT.LoadAsync();
|
||||||
await DiscountCart.EnsureInitializedAsync();
|
await DiscountCart.EnsureInitializedAsync();
|
||||||
|
var userInfo = await AuthService.GetUserAuthInfo();
|
||||||
|
if (userInfo.HasAddress)
|
||||||
await LoadAddresses();
|
await LoadAddresses();
|
||||||
|
else
|
||||||
|
_addresses = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadAddresses()
|
private async Task LoadAddresses()
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
{
|
{
|
||||||
<MudItem xs="6" sm="6" md="3"
|
<MudItem xs="6" sm="6" md="3"
|
||||||
onclick="@(() => NavigateToProduct(p.Id))">
|
onclick="@(() => NavigateToProduct(p.Id))">
|
||||||
|
<div id="@($"shop-product-{p.Id}")" class="h-100">
|
||||||
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden"
|
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden"
|
||||||
Style="cursor:pointer;">
|
Style="cursor:pointer;">
|
||||||
<MudCardContent Class="d-flex flex-column pa-1 h-100">
|
<MudCardContent Class="d-flex flex-column pa-1 h-100">
|
||||||
@@ -127,6 +128,7 @@
|
|||||||
</MudButton>
|
</MudButton>
|
||||||
</MudCardActions>
|
</MudCardActions>
|
||||||
</MudCard>
|
</MudCard>
|
||||||
|
</div>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
}
|
}
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.AspNetCore.Components.Routing;
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
using FrontOffice.Main.Utilities;
|
using FrontOffice.Main.Utilities;
|
||||||
|
|
||||||
namespace FrontOffice.Main.Pages.DiscountStore;
|
namespace FrontOffice.Main.Pages.DiscountStore;
|
||||||
@@ -10,6 +12,7 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
|
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
|
||||||
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
||||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||||
|
[Inject] private IJSRuntime Js { get; set; } = default!;
|
||||||
|
|
||||||
private bool _isAuthenticated;
|
private bool _isAuthenticated;
|
||||||
private string _search = string.Empty;
|
private string _search = string.Empty;
|
||||||
@@ -20,42 +23,107 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
private bool _hasMore = true;
|
private bool _hasMore = true;
|
||||||
private int _totalCount;
|
private int _totalCount;
|
||||||
private const int PageSize = 12;
|
private const int PageSize = 12;
|
||||||
|
private const string DefaultSortBy = "price desc";
|
||||||
|
|
||||||
private List<DiscountProductCard> _products = new();
|
private List<DiscountProductCard> _products = new();
|
||||||
private List<DiscountCategoryNode> _categories = new();
|
private List<DiscountCategoryNode> _categories = new();
|
||||||
|
private bool _ignoreNextLocationChange;
|
||||||
|
private bool _pendingScrollRestore;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
|
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
|
||||||
_loading = true;
|
|
||||||
await DiscountCart.EnsureInitializedAsync();
|
await DiscountCart.EnsureInitializedAsync();
|
||||||
DiscountCart.OnChange += StateHasChanged;
|
DiscountCart.OnChange += StateHasChanged;
|
||||||
var categoriesTask = ProductService.GetCategoriesAsync();
|
Navigation.LocationChanged += HandleLocationChanged;
|
||||||
var productsTask = ProductService.GetProductsAsync(page: 1, pageSize: PageSize);
|
|
||||||
await Task.WhenAll(categoriesTask, productsTask);
|
_categories = await ProductService.GetCategoriesAsync();
|
||||||
_categories = categoriesTask.Result;
|
ApplyStateFromUri();
|
||||||
var result = productsTask.Result;
|
_loading = true;
|
||||||
_products = result.Products;
|
await LoadPages(_currentPage);
|
||||||
_totalCount = result.TotalCount;
|
|
||||||
_hasMore = result.CurrentPage < result.TotalPages;
|
|
||||||
_loading = false;
|
_loading = false;
|
||||||
|
_pendingScrollRestore = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadInitial()
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (_pendingScrollRestore && !_loading && _products.Count > 0)
|
||||||
|
{
|
||||||
|
_pendingScrollRestore = false;
|
||||||
|
var payload = await ShopListScrollRestore.TakeAsync(Js, ShopListScrollRestore.DiscountKey);
|
||||||
|
if (payload is not null)
|
||||||
|
await ShopListScrollRestore.RestoreAsync(Js, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
await base.OnAfterRenderAsync(firstRender);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyStateFromUri()
|
||||||
|
{
|
||||||
|
var state = ShopListQueryState.Parse(Navigation.ToAbsoluteUri(Navigation.Uri));
|
||||||
|
_search = state.Query;
|
||||||
|
_selectedCategoryId = state.CategoryId;
|
||||||
|
_currentPage = state.Pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ShopListQueryState CaptureState() => new()
|
||||||
|
{
|
||||||
|
Query = _search,
|
||||||
|
CategoryId = _selectedCategoryId,
|
||||||
|
Pages = Math.Max(1, _currentPage)
|
||||||
|
};
|
||||||
|
|
||||||
|
private void SyncUrl()
|
||||||
|
{
|
||||||
|
var target = CaptureState().ToRelativeUrl(RouteConstants.DiscountStore.Products);
|
||||||
|
var currentPathAndQuery = Navigation.ToAbsoluteUri(Navigation.Uri).PathAndQuery;
|
||||||
|
if (string.Equals(currentPathAndQuery, target, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_ignoreNextLocationChange = true;
|
||||||
|
Navigation.NavigateTo(target, replace: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadPages(int pagesToLoad)
|
||||||
|
{
|
||||||
|
_products.Clear();
|
||||||
|
_hasMore = true;
|
||||||
|
pagesToLoad = Math.Max(1, pagesToLoad);
|
||||||
|
var search = string.IsNullOrWhiteSpace(_search) ? null : _search;
|
||||||
|
|
||||||
|
for (var page = 1; page <= pagesToLoad; page++)
|
||||||
|
{
|
||||||
|
var result = await ProductService.GetProductsAsync(
|
||||||
|
page: page,
|
||||||
|
pageSize: PageSize,
|
||||||
|
search: search,
|
||||||
|
categoryId: _selectedCategoryId,
|
||||||
|
sortBy: DefaultSortBy);
|
||||||
|
|
||||||
|
if (page == 1)
|
||||||
|
{
|
||||||
|
_products = result.Products;
|
||||||
|
_totalCount = result.TotalCount;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_products.AddRange(result.Products);
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentPage = page;
|
||||||
|
_hasMore = result.CurrentPage < result.TotalPages;
|
||||||
|
if (!_hasMore)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReloadFromFilters()
|
||||||
{
|
{
|
||||||
_loading = true;
|
_loading = true;
|
||||||
_currentPage = 1;
|
_currentPage = 1;
|
||||||
_products.Clear();
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
|
await LoadPages(1);
|
||||||
var result = await ProductService.GetProductsAsync(
|
SyncUrl();
|
||||||
page: 1,
|
|
||||||
pageSize: PageSize,
|
|
||||||
search: string.IsNullOrWhiteSpace(_search) ? null : _search,
|
|
||||||
categoryId: _selectedCategoryId);
|
|
||||||
_products = result.Products;
|
|
||||||
_totalCount = result.TotalCount;
|
|
||||||
_hasMore = result.CurrentPage < result.TotalPages;
|
|
||||||
_loading = false;
|
_loading = false;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
@@ -72,28 +140,28 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
page: _currentPage,
|
page: _currentPage,
|
||||||
pageSize: PageSize,
|
pageSize: PageSize,
|
||||||
search: string.IsNullOrWhiteSpace(_search) ? null : _search,
|
search: string.IsNullOrWhiteSpace(_search) ? null : _search,
|
||||||
categoryId: _selectedCategoryId);
|
categoryId: _selectedCategoryId,
|
||||||
|
sortBy: DefaultSortBy);
|
||||||
_products.AddRange(result.Products);
|
_products.AddRange(result.Products);
|
||||||
_hasMore = result.CurrentPage < result.TotalPages;
|
_hasMore = result.CurrentPage < result.TotalPages;
|
||||||
|
SyncUrl();
|
||||||
|
|
||||||
_loadingMore = false;
|
_loadingMore = false;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SearchProducts() => await LoadInitial();
|
private async Task SearchProducts() => await ReloadFromFilters();
|
||||||
|
|
||||||
private async Task OnSearchKeyUp(KeyboardEventArgs e)
|
private async Task OnSearchKeyUp(KeyboardEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Key == "Enter")
|
if (e.Key == "Enter")
|
||||||
{
|
await ReloadFromFilters();
|
||||||
await LoadInitial();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnCategoryChanged(long? value)
|
private async Task OnCategoryChanged(long? value)
|
||||||
{
|
{
|
||||||
_selectedCategoryId = value;
|
_selectedCategoryId = value;
|
||||||
await LoadInitial();
|
await ReloadFromFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task AddToCart(DiscountProductCard p)
|
private async Task AddToCart(DiscountProductCard p)
|
||||||
@@ -101,15 +169,44 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
await GuestGate.RunAsync(() => DiscountCart.AddAsync(p.Id));
|
await GuestGate.RunAsync(() => DiscountCart.AddAsync(p.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void NavigateToProduct(long id)
|
private async Task NavigateToProduct(long id)
|
||||||
{
|
{
|
||||||
|
await ShopListScrollRestore.SaveAsync(Js, ShopListScrollRestore.DiscountKey, id);
|
||||||
Navigation.NavigateTo($"{RouteConstants.DiscountStore.ProductDetail}{id}");
|
Navigation.NavigateTo($"{RouteConstants.DiscountStore.ProductDetail}{id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HandleLocationChanged(object? sender, LocationChangedEventArgs args)
|
||||||
|
{
|
||||||
|
if (_ignoreNextLocationChange)
|
||||||
|
{
|
||||||
|
_ignoreNextLocationChange = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var uri = Navigation.ToAbsoluteUri(args.Location);
|
||||||
|
if (!uri.AbsolutePath.Equals(RouteConstants.DiscountStore.Products, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var incoming = ShopListQueryState.Parse(uri);
|
||||||
|
if (incoming.Matches(CaptureState()))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_ = InvokeAsync(async () =>
|
||||||
|
{
|
||||||
|
ApplyStateFromUri();
|
||||||
|
_loading = true;
|
||||||
|
await LoadPages(_currentPage);
|
||||||
|
_loading = false;
|
||||||
|
_pendingScrollRestore = true;
|
||||||
|
StateHasChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static string FormatPrice(long price) => $"{price:N0} تومان";
|
private static string FormatPrice(long price) => $"{price:N0} تومان";
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
DiscountCart.OnChange -= StateHasChanged;
|
DiscountCart.OnChange -= StateHasChanged;
|
||||||
|
Navigation.LocationChanged -= HandleLocationChanged;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -429,6 +429,8 @@ else
|
|||||||
@* ═══════════════════════════════════════════════
|
@* ═══════════════════════════════════════════════
|
||||||
6. TESTIMONIALS
|
6. TESTIMONIALS
|
||||||
═══════════════════════════════════════════════ *@
|
═══════════════════════════════════════════════ *@
|
||||||
|
@if (_testimonials.Count > 0)
|
||||||
|
{
|
||||||
<section class="section-landing">
|
<section class="section-landing">
|
||||||
<MudContainer MaxWidth="MaxWidth.Large">
|
<MudContainer MaxWidth="MaxWidth.Large">
|
||||||
<div class="text-center mb-8 fade-in-up">
|
<div class="text-center mb-8 fade-in-up">
|
||||||
@@ -465,6 +467,7 @@ else
|
|||||||
</MudGrid>
|
</MudGrid>
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
</section>
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
@* ═══════════════════════════════════════════════
|
@* ═══════════════════════════════════════════════
|
||||||
7. FAQ
|
7. FAQ
|
||||||
|
|||||||
@@ -193,9 +193,10 @@ public partial class Index : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Testimonials ──
|
// ── Testimonials ──
|
||||||
if (_settings?.Testimonials?.Any() == true)
|
// اگر تنظیمات از CMS لود شده، لیست خالی یعنی ادمین عمداً حذف کرده — بدون fallback hardcode
|
||||||
|
if (_settings is not null)
|
||||||
{
|
{
|
||||||
_testimonials = _settings.Testimonials
|
_testimonials = (_settings.Testimonials ?? [])
|
||||||
.Select((t, i) => new TestimonialItem(t.Quote ?? "", t.Name ?? "", t.Role ?? "", i * 150))
|
.Select((t, i) => new TestimonialItem(t.Quote ?? "", t.Name ?? "", t.Role ?? "", i * 150))
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
using FrontOffice.Main.Utilities;
|
||||||
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
||||||
using FrontOffice.Main.Pages.Profile.Components;
|
using FrontOffice.Main.Pages.Profile.Components;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
@@ -9,6 +9,7 @@ namespace FrontOffice.Main.Pages.Profile;
|
|||||||
public partial class Addresses : ComponentBase
|
public partial class Addresses : ComponentBase
|
||||||
{
|
{
|
||||||
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
||||||
|
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||||
|
|
||||||
private List<CustomerAddressModel> _addresses = new();
|
private List<CustomerAddressModel> _addresses = new();
|
||||||
private bool _isLoadingAddresses;
|
private bool _isLoadingAddresses;
|
||||||
@@ -42,7 +43,10 @@ public partial class Addresses : ComponentBase
|
|||||||
var dialog = await DialogService.ShowAsync<AddAddressDialog>("افزودن آدرس جدید");
|
var dialog = await DialogService.ShowAsync<AddAddressDialog>("افزودن آدرس جدید");
|
||||||
var result = await dialog.Result;
|
var result = await dialog.Result;
|
||||||
if (result is not null && !result.Canceled)
|
if (result is not null && !result.Canceled)
|
||||||
|
{
|
||||||
await LoadAddresses();
|
await LoadAddresses();
|
||||||
|
await AuthService.RefreshTokenAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OpenEditAddressDialog(CustomerAddressModel address)
|
private async Task OpenEditAddressDialog(CustomerAddressModel address)
|
||||||
@@ -80,6 +84,7 @@ public partial class Addresses : ComponentBase
|
|||||||
await UserAddressContract.DeleteCustomerAddressAsync(new() { Id = id });
|
await UserAddressContract.DeleteCustomerAddressAsync(new() { Id = id });
|
||||||
Snackbar.Add("آدرس حذف شد.", Severity.Success);
|
Snackbar.Add("آدرس حذف شد.", Severity.Success);
|
||||||
await LoadAddresses();
|
await LoadAddresses();
|
||||||
|
await AuthService.RefreshTokenAsync();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -35,12 +35,97 @@
|
|||||||
</MudAlert>
|
</MudAlert>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (_authLoaded && _redirectingToMagic)
|
||||||
|
{
|
||||||
|
<MudAlert Severity="MudBlazor.Severity.Info" Class="rounded-lg" Variant="Variant.Outlined">
|
||||||
|
<MudStack Spacing="2" AlignItems="AlignItems.Center">
|
||||||
|
<MudProgressCircular Size="Size.Small" Indeterminate="true" Color="Color.Primary" />
|
||||||
|
<MudText>
|
||||||
|
کیف پول شما در حالت جادویی است. در حال انتقال به صفحه شارژ کیف پول جادویی...
|
||||||
|
</MudText>
|
||||||
|
<MudButton Size="Size.Small" Variant="Variant.Filled" Color="Color.Primary"
|
||||||
|
Href="@RouteConstants.Profile.MagicWallet"
|
||||||
|
StartIcon="@Icons.Material.Filled.AutoAwesome">
|
||||||
|
رفتن به شارژ کیف جادویی
|
||||||
|
</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
</MudAlert>
|
||||||
|
}
|
||||||
|
else if (_authLoaded && !_isClubMemberActive)
|
||||||
|
{
|
||||||
|
<MudAlert Severity="MudBlazor.Severity.Warning" Class="rounded-lg" Variant="Variant.Outlined">
|
||||||
|
<MudStack Spacing="2">
|
||||||
|
<MudText>
|
||||||
|
برای شارژ کیف پول اصلی ابتدا باید پکیج را خریداری کرده و قرارداد باشگاه مشتریان را امضا کنید.
|
||||||
|
</MudText>
|
||||||
|
<MudStack Row="true" Spacing="1" Class="flex-wrap">
|
||||||
|
@if (!_hasPurchasedPackage)
|
||||||
|
{
|
||||||
|
<MudButton Size="Size.Small" Variant="Variant.Filled" Color="Color.Primary"
|
||||||
|
Href="@RouteConstants.Package.List"
|
||||||
|
StartIcon="@Icons.Material.Filled.CardGiftcard">
|
||||||
|
مشاهده پکیجها
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudButton Size="Size.Small" Variant="Variant.Filled" Color="Color.Primary"
|
||||||
|
Href="@RouteConstants.Club.Membership"
|
||||||
|
StartIcon="@Icons.Material.Filled.Handshake">
|
||||||
|
امضای قرارداد باشگاه
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
<MudButton Size="Size.Small" Variant="Variant.Text"
|
||||||
|
Href="@RouteConstants.Profile.Wallet">
|
||||||
|
بازگشت به کیف پول
|
||||||
|
</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
</MudStack>
|
||||||
|
</MudAlert>
|
||||||
|
}
|
||||||
|
else if (_authLoaded && _needsMagicCeilingConsent && !_allowNormalDespiteMagic && !_redirectingToMagic)
|
||||||
|
{
|
||||||
|
<MudAlert Severity="MudBlazor.Severity.Warning" Class="rounded-lg" Variant="Variant.Outlined">
|
||||||
|
<MudStack Spacing="3">
|
||||||
|
<MudText Typo="Typo.subtitle1">
|
||||||
|
سقف شارژ کیف پول جادویی در این دور پر شده است.
|
||||||
|
</MudText>
|
||||||
|
<MudText Typo="Typo.body2">
|
||||||
|
شارژ از این صفحه بدون ضریب (۱:۱) به موجودی اضافه میشود و ضریب جادویی اعمال نمیشود.
|
||||||
|
برای ادامه، تأیید کنید.
|
||||||
|
</MudText>
|
||||||
|
<MudStack Row="true" Spacing="1" Class="flex-wrap">
|
||||||
|
<MudButton Variant="Variant.Filled"
|
||||||
|
Color="Color.Primary"
|
||||||
|
StartIcon="@Icons.Material.Filled.Check"
|
||||||
|
OnClick="AcceptMagicCeilingConsent">
|
||||||
|
ادامه شارژ بدون ضریب
|
||||||
|
</MudButton>
|
||||||
|
<MudButton Variant="Variant.Outlined"
|
||||||
|
Color="Color.Secondary"
|
||||||
|
StartIcon="@Icons.Material.Filled.AutoAwesome"
|
||||||
|
OnClick="DeclineMagicCeilingConsent">
|
||||||
|
بازگشت به کیف جادویی
|
||||||
|
</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
</MudStack>
|
||||||
|
</MudAlert>
|
||||||
|
}
|
||||||
|
else if (ShowChargeForm)
|
||||||
|
{
|
||||||
<MudPaper Elevation="2" Class="pa-5 rounded-lg">
|
<MudPaper Elevation="2" Class="pa-5 rounded-lg">
|
||||||
<MudText Typo="Typo.h6" Class="mb-3">
|
<MudText Typo="Typo.h6" Class="mb-3">
|
||||||
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Class="ml-1" />
|
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Class="ml-1" />
|
||||||
شارژ موجودی اصلی
|
شارژ موجودی اصلی
|
||||||
</MudText>
|
</MudText>
|
||||||
|
|
||||||
|
@if (_allowNormalDespiteMagic)
|
||||||
|
{
|
||||||
|
<MudAlert Severity="MudBlazor.Severity.Warning" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Warning">
|
||||||
|
این شارژ بدون ضریب جادویی است (۱:۱). سقف واریز جادویی شما در این دور پر شده است.
|
||||||
|
</MudAlert>
|
||||||
|
}
|
||||||
|
|
||||||
<MudAlert Severity="MudBlazor.Severity.Info" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info">
|
<MudAlert Severity="MudBlazor.Severity.Info" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info">
|
||||||
مبلغ وارد شده (به تومان) از طریق درگاه پرداخت به موجودی اصلی شما اضافه میشود.
|
مبلغ وارد شده (به تومان) از طریق درگاه پرداخت به موجودی اصلی شما اضافه میشود.
|
||||||
از موجودی اصلی برای خرید از فروشگاه عادی و پرداخت سفارش استفاده میشود.
|
از موجودی اصلی برای خرید از فروشگاه عادی و پرداخت سفارش استفاده میشود.
|
||||||
@@ -122,5 +207,6 @@
|
|||||||
</MudList>
|
</MudList>
|
||||||
</MudExpansionPanel>
|
</MudExpansionPanel>
|
||||||
</MudExpansionPanels>
|
</MudExpansionPanels>
|
||||||
|
}
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
|
|||||||
@@ -7,10 +7,18 @@ namespace FrontOffice.Main.Pages.Profile;
|
|||||||
|
|
||||||
public partial class ChargeCreditWallet : ComponentBase
|
public partial class ChargeCreditWallet : ComponentBase
|
||||||
{
|
{
|
||||||
|
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||||
|
|
||||||
private bool _isProcessing;
|
private bool _isProcessing;
|
||||||
private long _chargeAmount;
|
private long _chargeAmount;
|
||||||
private string? _paymentResult;
|
private string? _paymentResult;
|
||||||
private string? _pendingReturnUrl;
|
private string? _pendingReturnUrl;
|
||||||
|
private bool _isClubMemberActive;
|
||||||
|
private bool _hasPurchasedPackage;
|
||||||
|
private bool _authLoaded;
|
||||||
|
private bool _redirectingToMagic;
|
||||||
|
private bool _allowNormalDespiteMagic;
|
||||||
|
private bool _needsMagicCeilingConsent;
|
||||||
|
|
||||||
private readonly long[] _presetAmounts = { 500_000, 1_000_000, 5_000_000, 10_000_000, 20_000_000, 50_000_000 };
|
private readonly long[] _presetAmounts = { 500_000, 1_000_000, 5_000_000, 10_000_000, 20_000_000, 50_000_000 };
|
||||||
|
|
||||||
@@ -23,7 +31,13 @@ public partial class ChargeCreditWallet : ComponentBase
|
|||||||
[SupplyParameterFromQuery(Name = "returnUrl")]
|
[SupplyParameterFromQuery(Name = "returnUrl")]
|
||||||
public string? ReturnUrlQueryParam { get; set; }
|
public string? ReturnUrlQueryParam { get; set; }
|
||||||
|
|
||||||
protected override void OnInitialized()
|
private bool ShowChargeForm =>
|
||||||
|
_authLoaded
|
||||||
|
&& _isClubMemberActive
|
||||||
|
&& !_redirectingToMagic
|
||||||
|
&& (!_needsMagicCeilingConsent || _allowNormalDespiteMagic);
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
_paymentResult = PaymentQueryParam;
|
_paymentResult = PaymentQueryParam;
|
||||||
|
|
||||||
@@ -32,20 +46,63 @@ public partial class ChargeCreditWallet : ComponentBase
|
|||||||
|
|
||||||
if (CreditChargeNavigation.IsValidReturnUrl(ReturnUrlQueryParam))
|
if (CreditChargeNavigation.IsValidReturnUrl(ReturnUrlQueryParam))
|
||||||
_pendingReturnUrl = ReturnUrlQueryParam;
|
_pendingReturnUrl = ReturnUrlQueryParam;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userInfo = await AuthService.GetUserAuthInfo();
|
||||||
|
_isClubMemberActive = userInfo.IsClubMemberActive;
|
||||||
|
_hasPurchasedPackage = userInfo.HasPurchasedPackage;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_isClubMemberActive = false;
|
||||||
|
_hasPurchasedPackage = false;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_authLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isClubMemberActive)
|
||||||
|
await EvaluateMagicWalletGateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
if (firstRender && !string.IsNullOrEmpty(_pendingReturnUrl))
|
if (firstRender && !string.IsNullOrEmpty(_pendingReturnUrl) && _isClubMemberActive && !_redirectingToMagic)
|
||||||
{
|
|
||||||
await CreditChargeNavigation.SaveReturnUrlAsync(JSRuntime, _pendingReturnUrl);
|
await CreditChargeNavigation.SaveReturnUrlAsync(JSRuntime, _pendingReturnUrl);
|
||||||
}
|
|
||||||
|
|
||||||
await base.OnAfterRenderAsync(firstRender);
|
await base.OnAfterRenderAsync(firstRender);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AcceptMagicCeilingConsent()
|
||||||
|
{
|
||||||
|
_allowNormalDespiteMagic = true;
|
||||||
|
_redirectingToMagic = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DeclineMagicCeilingConsent()
|
||||||
|
{
|
||||||
|
_redirectingToMagic = true;
|
||||||
|
Navigation.NavigateTo(RouteConstants.Profile.MagicWallet);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task StartCreditCharge()
|
private async Task StartCreditCharge()
|
||||||
{
|
{
|
||||||
|
if (!_isClubMemberActive)
|
||||||
|
{
|
||||||
|
Snackbar.Add(
|
||||||
|
"برای شارژ کیف پول اصلی ابتدا باید پکیج را خریداری کرده و قرارداد باشگاه مشتریان را امضا کنید.",
|
||||||
|
Severity.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await EvaluateMagicWalletGateAsync())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_needsMagicCeilingConsent && !_allowNormalDespiteMagic)
|
||||||
|
return;
|
||||||
|
|
||||||
if (_chargeAmount <= 0 || _isProcessing) return;
|
if (_chargeAmount <= 0 || _isProcessing) return;
|
||||||
|
|
||||||
_isProcessing = true;
|
_isProcessing = true;
|
||||||
@@ -84,6 +141,50 @@ public partial class ChargeCreditWallet : ComponentBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Magic با ظرفیت باقیمانده → ریدایرکت به MagicWallet.
|
||||||
|
/// Magic با سقف پر → نمایش تأیید روی صفحه برای شارژ ۱:۱.
|
||||||
|
/// true یعنی مسیر شارژ فعلی باید متوقف شود.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<bool> EvaluateMagicWalletGateAsync()
|
||||||
|
{
|
||||||
|
if (_allowNormalDespiteMagic)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var status = await WalletService.GetMagicWalletStatusAsync();
|
||||||
|
if (status.WalletMode != 1)
|
||||||
|
{
|
||||||
|
_needsMagicCeilingConsent = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.MagicRemainingDeposit > 0)
|
||||||
|
{
|
||||||
|
var multiplier = status.MagicMultiplier > 0 ? status.MagicMultiplier : 2.5;
|
||||||
|
var multiplierText = multiplier.ToString("0.#");
|
||||||
|
|
||||||
|
_redirectingToMagic = true;
|
||||||
|
_needsMagicCeilingConsent = false;
|
||||||
|
Snackbar.Add(
|
||||||
|
$"کیف پول شما در حالت جادویی است. شارژ از این صفحه موجودی را چندبرابر نمیکند. برای دریافت ضریب جادویی (×{multiplierText}) به صفحه شارژ کیف پول جادویی هدایت میشوید.",
|
||||||
|
Severity.Info);
|
||||||
|
|
||||||
|
Navigation.NavigateTo(RouteConstants.Profile.MagicWallet);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_needsMagicCeilingConsent = true;
|
||||||
|
return !_allowNormalDespiteMagic;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// در صورت خطا در خواندن وضعیت، مانع شارژ عادی نشو
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ContinueShoppingAsync()
|
private async Task ContinueShoppingAsync()
|
||||||
{
|
{
|
||||||
var returnUrl = await CreditChargeNavigation.GetReturnUrlAsync(JSRuntime);
|
var returnUrl = await CreditChargeNavigation.GetReturnUrlAsync(JSRuntime);
|
||||||
|
|||||||
@@ -61,11 +61,11 @@
|
|||||||
<span class="stat-value">@_statistics.TotalMembers</span>
|
<span class="stat-value">@_statistics.TotalMembers</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<span class="stat-label">پای چپ:</span>
|
<span class="stat-label">سازمان چپ:</span>
|
||||||
<span class="stat-value left">@_statistics.LeftLegCount</span>
|
<span class="stat-value left">@_statistics.LeftLegCount</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<span class="stat-label">پای راست:</span>
|
<span class="stat-label">سازمان راست:</span>
|
||||||
<span class="stat-value right">@_statistics.RightLegCount</span>
|
<span class="stat-value right">@_statistics.RightLegCount</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using FrontOffice.Main.Utilities;
|
using FrontOffice.Main.Utilities;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
|
|||||||
@@ -333,6 +333,7 @@ public partial class Index
|
|||||||
if (!result.Canceled)
|
if (!result.Canceled)
|
||||||
{
|
{
|
||||||
await LoadAddresses();
|
await LoadAddresses();
|
||||||
|
await AuthService.RefreshTokenAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,6 +386,7 @@ public partial class Index
|
|||||||
});
|
});
|
||||||
Snackbar.Add("آدرس با موفقیت حذف شد.", Severity.Success);
|
Snackbar.Add("آدرس با موفقیت حذف شد.", Severity.Success);
|
||||||
await LoadAddresses();
|
await LoadAddresses();
|
||||||
|
await AuthService.RefreshTokenAsync();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -491,11 +493,6 @@ public partial class Index
|
|||||||
Snackbar.Add("لطفا اطلاعات شخصی خود را تکمیل کنید. (تاریخ تولد وارد نشده)", Severity.Error);
|
Snackbar.Add("لطفا اطلاعات شخصی خود را تکمیل کنید. (تاریخ تولد وارد نشده)", Severity.Error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!_addresses.Any())
|
|
||||||
{
|
|
||||||
Snackbar.Add("آدرس محل سکونت شما الزامی است!", Severity.Error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var url = "https://dayadiamond.ir/profile/creditpurchase/?merchantcode=56146364";
|
var url = "https://dayadiamond.ir/profile/creditpurchase/?merchantcode=56146364";
|
||||||
await JSRuntime.InvokeVoidAsync("open", url, "_blank");
|
await JSRuntime.InvokeVoidAsync("open", url, "_blank");
|
||||||
|
|||||||
@@ -166,8 +166,23 @@
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<MudAlert Severity="MudBlazor.Severity.Warning" Class="rounded-lg">
|
<MudAlert Severity="MudBlazor.Severity.Warning" Class="rounded-lg">
|
||||||
|
<MudStack Spacing="2">
|
||||||
|
<MudText>
|
||||||
سقف شارژ جادویی در این دور پر شده است.
|
سقف شارژ جادویی در این دور پر شده است.
|
||||||
با خرج کردن موجودی و خرید مجدد پکیج، سقف ریست میشود.
|
با خرج کردن موجودی و خرید مجدد پکیج، سقف ریست میشود.
|
||||||
|
</MudText>
|
||||||
|
<MudText Typo="Typo.body2">
|
||||||
|
اگر برای تکمیل خرید به موجودی بیشتری نیاز دارید، میتوانید بهصورت استثنایی کیف اصلی را بدون ضریب (۱:۱) شارژ کنید.
|
||||||
|
</MudText>
|
||||||
|
<MudButton Variant="Variant.Filled"
|
||||||
|
Color="Color.Primary"
|
||||||
|
Size="Size.Small"
|
||||||
|
StartIcon="@Icons.Material.Filled.AccountBalanceWallet"
|
||||||
|
Href="@RouteConstants.Profile.ChargeCreditWallet"
|
||||||
|
Class="rounded-lg align-self-start">
|
||||||
|
شارژ عادی بدون ضریب
|
||||||
|
</MudButton>
|
||||||
|
</MudStack>
|
||||||
</MudAlert>
|
</MudAlert>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,44 @@
|
|||||||
درخواستهای برداشت
|
درخواستهای برداشت
|
||||||
</MudButton>
|
</MudButton>
|
||||||
|
|
||||||
<!-- دکمه شارژ کیفپول اصلی -->
|
<!-- دکمه شارژ کیفپول اصلی / جادویی -->
|
||||||
|
@if (_isClubMemberActive)
|
||||||
|
{
|
||||||
|
@if (_isMagicWallet)
|
||||||
|
{
|
||||||
|
<MudButton Variant="Variant.Filled"
|
||||||
|
Color="Color.Secondary"
|
||||||
|
Size="Size.Large"
|
||||||
|
FullWidth="true"
|
||||||
|
StartIcon="@Icons.Material.Filled.AutoAwesome"
|
||||||
|
Href="@RouteConstants.Profile.MagicWallet"
|
||||||
|
Class="rounded-lg">
|
||||||
|
شارژ کیفپول جادویی
|
||||||
|
</MudButton>
|
||||||
|
@if (_magicCeilingFull)
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Warning" Dense="true" Variant="Variant.Outlined">
|
||||||
|
سقف شارژ جادویی این دور پر است. در صورت نیاز میتوانید بهصورت استثنایی بدون ضریب شارژ کنید.
|
||||||
|
</MudAlert>
|
||||||
|
<MudButton Variant="Variant.Outlined"
|
||||||
|
Color="Color.Primary"
|
||||||
|
Size="Size.Large"
|
||||||
|
FullWidth="true"
|
||||||
|
StartIcon="@Icons.Material.Filled.AccountBalanceWallet"
|
||||||
|
Href="@RouteConstants.Profile.ChargeCreditWallet"
|
||||||
|
Class="rounded-lg">
|
||||||
|
شارژ عادی بدون ضریب
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined">
|
||||||
|
کیف پول شما در حالت جادویی است؛ شارژ از مسیر جادویی با ضریب چندبرابر اعمال میشود.
|
||||||
|
</MudAlert>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
<MudButton Variant="Variant.Filled"
|
<MudButton Variant="Variant.Filled"
|
||||||
Color="Color.Primary"
|
Color="Color.Primary"
|
||||||
Size="Size.Large"
|
Size="Size.Large"
|
||||||
@@ -48,6 +85,45 @@
|
|||||||
Class="rounded-lg">
|
Class="rounded-lg">
|
||||||
شارژ کیفپول اصلی
|
شارژ کیفپول اصلی
|
||||||
</MudButton>
|
</MudButton>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudPaper Elevation="0" Class="pa-3 rounded-lg" Outlined="true">
|
||||||
|
<MudStack Spacing="2">
|
||||||
|
<MudButton Variant="Variant.Filled"
|
||||||
|
Color="Color.Primary"
|
||||||
|
Size="Size.Large"
|
||||||
|
FullWidth="true"
|
||||||
|
StartIcon="@Icons.Material.Filled.AccountBalanceWallet"
|
||||||
|
Disabled="true"
|
||||||
|
Class="rounded-lg">
|
||||||
|
شارژ کیفپول اصلی
|
||||||
|
</MudButton>
|
||||||
|
<MudAlert Severity="Severity.Warning" Dense="true" Variant="Variant.Outlined">
|
||||||
|
برای شارژ کیف پول اصلی ابتدا باید پکیج را خریداری کرده و قرارداد باشگاه مشتریان را امضا کنید.
|
||||||
|
</MudAlert>
|
||||||
|
<MudStack Row="true" Spacing="1" Class="flex-wrap">
|
||||||
|
@if (!_hasPurchasedPackage)
|
||||||
|
{
|
||||||
|
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
|
||||||
|
Href="@RouteConstants.Package.List"
|
||||||
|
StartIcon="@Icons.Material.Filled.CardGiftcard">
|
||||||
|
مشاهده پکیجها
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
|
||||||
|
Href="@RouteConstants.Club.Membership"
|
||||||
|
StartIcon="@Icons.Material.Filled.Handshake">
|
||||||
|
امضای قرارداد باشگاه
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
</MudStack>
|
||||||
|
</MudStack>
|
||||||
|
</MudPaper>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- دکمه شارژ کیفپول اعتباری -->
|
<!-- دکمه شارژ کیفپول اعتباری -->
|
||||||
<MudButton Variant="Variant.Filled"
|
<MudButton Variant="Variant.Filled"
|
||||||
|
|||||||
@@ -6,13 +6,43 @@ namespace FrontOffice.Main.Pages.Profile;
|
|||||||
|
|
||||||
public partial class Wallet : ComponentBase
|
public partial class Wallet : ComponentBase
|
||||||
{
|
{
|
||||||
|
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||||
|
|
||||||
private (string Credit, string Discount, string Network) _balances = ("-", "-", "-");
|
private (string Credit, string Discount, string Network) _balances = ("-", "-", "-");
|
||||||
private List<WalletTransaction> _txs = new();
|
private List<WalletTransaction> _txs = new();
|
||||||
private string? _filterReferenceId;
|
private string? _filterReferenceId;
|
||||||
private string _filterType = "all";
|
private string _filterType = "all";
|
||||||
|
private bool _isClubMemberActive;
|
||||||
|
private bool _hasPurchasedPackage;
|
||||||
|
private bool _isMagicWallet;
|
||||||
|
private bool _magicCeilingFull;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userInfo = await AuthService.GetUserAuthInfo();
|
||||||
|
_isClubMemberActive = userInfo.IsClubMemberActive;
|
||||||
|
_hasPurchasedPackage = userInfo.HasPurchasedPackage;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_isClubMemberActive = false;
|
||||||
|
_hasPurchasedPackage = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var magicStatus = await WalletService.GetMagicWalletStatusAsync();
|
||||||
|
_isMagicWallet = magicStatus.WalletMode == 1;
|
||||||
|
_magicCeilingFull = _isMagicWallet && magicStatus.MagicRemainingDeposit <= 0;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_isMagicWallet = false;
|
||||||
|
_magicCeilingFull = false;
|
||||||
|
}
|
||||||
|
|
||||||
var b = await WalletService.GetBalancesAsync();
|
var b = await WalletService.GetBalancesAsync();
|
||||||
_balances = (FormatPrice(b.CreditBalance), FormatPrice(b.DiscountBalance), FormatPrice(b.NetworkBalance));
|
_balances = (FormatPrice(b.CreditBalance), FormatPrice(b.DiscountBalance), FormatPrice(b.NetworkBalance));
|
||||||
_txs = await WalletService.GetTransactionsAsync();
|
_txs = await WalletService.GetTransactionsAsync();
|
||||||
|
|||||||
@@ -58,8 +58,19 @@ public partial class WithdrawalRequests : ComponentBase
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_isSubmittingWithdrawal = true;
|
_isSubmittingWithdrawal = true;
|
||||||
_withdrawIban=_withdrawIban!.Trim().ToUpper().Replace("IR", "").Replace(" ", "");
|
|
||||||
await WalletService.RequestWithdrawalAsync(_selectedPayout.Id, _withdrawMethod, "IR"+_withdrawIban);
|
string? normalizedIban = null;
|
||||||
|
if (_withdrawMethod == WithdrawalMethodClient.Cash)
|
||||||
|
{
|
||||||
|
normalizedIban = NormalizeIranianIban(_withdrawIban);
|
||||||
|
if (normalizedIban is null)
|
||||||
|
{
|
||||||
|
Snackbar.Add("فرمت شماره شبا معتبر نیست. باید IR و ۲۴ رقم باشد (خط تیره اختیاری است).", Severity.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await WalletService.RequestWithdrawalAsync(_selectedPayout.Id, _withdrawMethod, normalizedIban);
|
||||||
Snackbar.Add("درخواست برداشت ثبت شد.", Severity.Success);
|
Snackbar.Add("درخواست برداشت ثبت شد.", Severity.Success);
|
||||||
|
|
||||||
// بروزرسانی لیست
|
// بروزرسانی لیست
|
||||||
@@ -125,4 +136,26 @@ public partial class WithdrawalRequests : ComponentBase
|
|||||||
1 => "الماس",
|
1 => "الماس",
|
||||||
_ => "-"
|
_ => "-"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// فاصله و خط تیره را حذف میکند و شبا را به شکل IR + ۲۴ رقم برمیگرداند.
|
||||||
|
/// </summary>
|
||||||
|
private static string? NormalizeIranianIban(string? iban)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(iban))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var normalized = iban.Trim().ToUpperInvariant()
|
||||||
|
.Replace(" ", "", StringComparison.Ordinal)
|
||||||
|
.Replace("-", "", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
if (normalized.StartsWith("IR", StringComparison.Ordinal))
|
||||||
|
normalized = normalized[2..];
|
||||||
|
|
||||||
|
normalized = "IR" + normalized;
|
||||||
|
|
||||||
|
return System.Text.RegularExpressions.Regex.IsMatch(normalized, @"^IR\d{24}$")
|
||||||
|
? normalized
|
||||||
|
: null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,11 @@ public partial class CheckoutSummary : ComponentBase
|
|||||||
await AuthDialogService.ShowAuthDialogAsync();
|
await AuthDialogService.ShowAuthDialogAsync();
|
||||||
}
|
}
|
||||||
await Cart.EnsureInitializedAsync();
|
await Cart.EnsureInitializedAsync();
|
||||||
|
var userInfo = await AuthService.GetUserAuthInfo();
|
||||||
|
if (userInfo.HasAddress)
|
||||||
await LoadAddresses();
|
await LoadAddresses();
|
||||||
|
else
|
||||||
|
_addresses = new();
|
||||||
await LoadWalletBalance();
|
await LoadWalletBalance();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,11 +129,42 @@ public partial class CheckoutSummary : ComponentBase
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
var shortfall = totalRequired - walletBalance;
|
var shortfall = totalRequired - walletBalance;
|
||||||
|
var allowCharge = false;
|
||||||
|
var hasPurchasedPackage = false;
|
||||||
|
var isMagicWallet = false;
|
||||||
|
var magicCeilingFull = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userInfo = await AuthService.GetUserAuthInfo();
|
||||||
|
allowCharge = userInfo.IsClubMemberActive;
|
||||||
|
hasPurchasedPackage = userInfo.HasPurchasedPackage;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
allowCharge = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var magicStatus = await WalletService.GetMagicWalletStatusAsync();
|
||||||
|
isMagicWallet = magicStatus.WalletMode == 1;
|
||||||
|
magicCeilingFull = isMagicWallet && magicStatus.MagicRemainingDeposit <= 0;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
isMagicWallet = false;
|
||||||
|
magicCeilingFull = false;
|
||||||
|
}
|
||||||
|
|
||||||
var parameters = new DialogParameters<InsufficientCreditDialog>
|
var parameters = new DialogParameters<InsufficientCreditDialog>
|
||||||
{
|
{
|
||||||
{ x => x.CurrentBalance, walletBalance },
|
{ x => x.CurrentBalance, walletBalance },
|
||||||
{ x => x.RequiredAmount, totalRequired },
|
{ x => x.RequiredAmount, totalRequired },
|
||||||
{ x => x.ShortfallAmount, shortfall }
|
{ x => x.ShortfallAmount, shortfall },
|
||||||
|
{ x => x.AllowChargeCredit, allowCharge },
|
||||||
|
{ x => x.HasPurchasedPackage, hasPurchasedPackage },
|
||||||
|
{ x => x.IsMagicWallet, isMagicWallet },
|
||||||
|
{ x => x.MagicCeilingFull, magicCeilingFull }
|
||||||
};
|
};
|
||||||
|
|
||||||
var dialog = await DialogService.ShowAsync<InsufficientCreditDialog>(
|
var dialog = await DialogService.ShowAsync<InsufficientCreditDialog>(
|
||||||
@@ -143,7 +178,7 @@ public partial class CheckoutSummary : ComponentBase
|
|||||||
});
|
});
|
||||||
|
|
||||||
var result = await dialog.Result;
|
var result = await dialog.Result;
|
||||||
if (result is { Canceled: false } && result.Data is long chargeShortfall)
|
if (allowCharge && result is { Canceled: false } && result.Data is long chargeShortfall)
|
||||||
{
|
{
|
||||||
var chargeAmount = CreditChargeNavigation.NormalizeChargeAmount(chargeShortfall);
|
var chargeAmount = CreditChargeNavigation.NormalizeChargeAmount(chargeShortfall);
|
||||||
Navigation.NavigateTo(CreditChargeNavigation.BuildChargeUrl(
|
Navigation.NavigateTo(CreditChargeNavigation.BuildChargeUrl(
|
||||||
|
|||||||
@@ -47,11 +47,18 @@ public partial class OrderTracking : ComponentBase
|
|||||||
{
|
{
|
||||||
if (_order is null) return;
|
if (_order is null) return;
|
||||||
|
|
||||||
// Build tracking steps based on PaymentStatus
|
|
||||||
var isPaid = _order.PaymentStatus == Messages.PaymentStatus.Success;
|
var isPaid = _order.PaymentStatus == Messages.PaymentStatus.Success;
|
||||||
|
// Domain/public_messages: None=0, Pending=1, InTransit=2, Delivered=3, Returned=4, Cancelled=5, ReadyForOfficePickup=6
|
||||||
|
var delivery = (int)_order.DeliveryStatus;
|
||||||
|
var isCancelled = delivery == 5;
|
||||||
|
var isReturned = delivery == 4;
|
||||||
|
var isOfficePickup = delivery == 6;
|
||||||
|
var isInTransit = delivery == 2;
|
||||||
|
var isDelivered = delivery == 3;
|
||||||
|
var isPendingDelivery = delivery is 0 or 1;
|
||||||
|
|
||||||
_trackingSteps = new List<TrackingStep>
|
_trackingSteps =
|
||||||
{
|
[
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Title = "ثبت سفارش",
|
Title = "ثبت سفارش",
|
||||||
@@ -61,45 +68,77 @@ public partial class OrderTracking : ComponentBase
|
|||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Title = "در انتظار پرداخت",
|
Title = "پرداخت",
|
||||||
Description = "منتظر تایید پرداخت هستیم",
|
Description = isPaid ? "پرداخت شما تایید شد" : "منتظر تایید پرداخت هستیم",
|
||||||
IsCompleted = isPaid,
|
IsCompleted = isPaid,
|
||||||
IsCurrent = !isPaid,
|
IsCurrent = !isPaid && !isCancelled,
|
||||||
Date = isPaid ? FormatDate(_order.PaymentDate) : ""
|
Date = isPaid ? FormatDate(_order.PaymentDate) : ""
|
||||||
},
|
}
|
||||||
new()
|
];
|
||||||
|
|
||||||
|
if (isCancelled)
|
||||||
{
|
{
|
||||||
Title = "تایید پرداخت",
|
_trackingSteps.Add(new()
|
||||||
Description = "پرداخت شما تایید شد",
|
|
||||||
IsCompleted = isPaid,
|
|
||||||
IsCurrent = false,
|
|
||||||
Date = isPaid ? FormatDate(_order.PaymentDate) : ""
|
|
||||||
},
|
|
||||||
new()
|
|
||||||
{
|
{
|
||||||
Title = "در حال پردازش",
|
Title = "لغو شده",
|
||||||
Description = "سفارش شما در حال آمادهسازی است",
|
Description = "ارسال این سفارش لغو شده است",
|
||||||
IsCompleted = false,
|
IsCompleted = true,
|
||||||
IsCurrent = isPaid,
|
IsCurrent = true,
|
||||||
Date = ""
|
Date = ""
|
||||||
},
|
});
|
||||||
new()
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isReturned)
|
||||||
|
{
|
||||||
|
_trackingSteps.Add(new()
|
||||||
|
{
|
||||||
|
Title = "مرجوع شده",
|
||||||
|
Description = "سفارش مرجوع شده است",
|
||||||
|
IsCompleted = true,
|
||||||
|
IsCurrent = true,
|
||||||
|
Date = ""
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOfficePickup)
|
||||||
|
{
|
||||||
|
_trackingSteps.Add(new()
|
||||||
|
{
|
||||||
|
Title = "آماده تحویل در دفتر",
|
||||||
|
Description = "سفارش برای تحویل حضوری در دفتر آماده است",
|
||||||
|
IsCompleted = true,
|
||||||
|
IsCurrent = true,
|
||||||
|
Date = ""
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_trackingSteps.Add(new()
|
||||||
|
{
|
||||||
|
Title = "در حال آمادهسازی",
|
||||||
|
Description = "سفارش شما در حال آمادهسازی است",
|
||||||
|
IsCompleted = isInTransit || isDelivered,
|
||||||
|
IsCurrent = isPaid && isPendingDelivery,
|
||||||
|
Date = ""
|
||||||
|
});
|
||||||
|
_trackingSteps.Add(new()
|
||||||
{
|
{
|
||||||
Title = "ارسال شده",
|
Title = "ارسال شده",
|
||||||
Description = "سفارش شما به پست تحویل داده شد",
|
Description = "سفارش شما به پست تحویل داده شد",
|
||||||
IsCompleted = false,
|
IsCompleted = isDelivered,
|
||||||
IsCurrent = false,
|
IsCurrent = isInTransit,
|
||||||
Date = ""
|
Date = ""
|
||||||
},
|
});
|
||||||
new()
|
_trackingSteps.Add(new()
|
||||||
{
|
{
|
||||||
Title = "تحویل داده شده",
|
Title = "تحویل داده شده",
|
||||||
Description = "سفارش به دست شما رسید",
|
Description = "سفارش به دست شما رسید",
|
||||||
IsCompleted = false,
|
IsCompleted = isDelivered,
|
||||||
IsCurrent = false,
|
IsCurrent = false,
|
||||||
Date = ""
|
Date = ""
|
||||||
}
|
});
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string FormatDate(Google.Protobuf.WellKnownTypes.Timestamp? timestamp)
|
private static string FormatDate(Google.Protobuf.WellKnownTypes.Timestamp? timestamp)
|
||||||
|
|||||||
@@ -1,75 +1,96 @@
|
|||||||
@attribute [Route(RouteConstants.Store.Orders)]
|
@attribute [Route(RouteConstants.Store.Orders)]
|
||||||
@* Injection is handled in code-behind *@
|
|
||||||
|
|
||||||
<PageTitle>سفارشات من</PageTitle>
|
<PageTitle>سفارشات من</PageTitle>
|
||||||
|
|
||||||
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
|
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
|
||||||
|
<MudStack Spacing="3">
|
||||||
<PageHeader Title="سفارشات من" BackHref="@RouteConstants.Store.Products" />
|
<PageHeader Title="سفارشات من" BackHref="@RouteConstants.Store.Products" />
|
||||||
|
|
||||||
@if (_loading)
|
@if (_loading)
|
||||||
{
|
{
|
||||||
<LoadingState />
|
<LoadingState />
|
||||||
}
|
}
|
||||||
else if (_orders.Count == 0)
|
else if (_orders.Count == 0)
|
||||||
{
|
{
|
||||||
<MudAlert Severity="Severity.Info">هنوز سفارشی ثبت نکردهاید.</MudAlert>
|
<MudAlert Severity="Severity.Info">هنوز سفارشی ثبت نشده است.</MudAlert>
|
||||||
|
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Store"
|
||||||
|
Href="@RouteConstants.Store.Products">
|
||||||
|
مشاهده فروشگاه
|
||||||
|
</MudButton>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
<!-- Desktop Table -->
|
||||||
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
|
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
|
||||||
<MudTable Items="_orders" Dense="true">
|
<MudPaper Elevation="1" Class="pa-4 rounded-lg">
|
||||||
|
<MudTable Items="_orders">
|
||||||
<HeaderContent>
|
<HeaderContent>
|
||||||
<MudTh>شناسه</MudTh>
|
<MudTh>شماره سفارش</MudTh>
|
||||||
|
<MudTh>تعداد اقلام</MudTh>
|
||||||
|
<MudTh>مبلغ کل</MudTh>
|
||||||
|
<MudTh>وضعیت پرداخت</MudTh>
|
||||||
|
<MudTh>وضعیت ارسال</MudTh>
|
||||||
<MudTh>تاریخ</MudTh>
|
<MudTh>تاریخ</MudTh>
|
||||||
<MudTh>وضعیت</MudTh>
|
|
||||||
<MudTh>مبلغ</MudTh>
|
|
||||||
<MudTh></MudTh>
|
<MudTh></MudTh>
|
||||||
</HeaderContent>
|
</HeaderContent>
|
||||||
<RowTemplate>
|
<RowTemplate>
|
||||||
<MudTd>@context.Id</MudTd>
|
<MudTd>@context.Id</MudTd>
|
||||||
|
<MudTd>@GetItemsCount(context)</MudTd>
|
||||||
|
<MudTd>@FormatPrice(GetTotalAmount(context))</MudTd>
|
||||||
<MudTd>
|
<MudTd>
|
||||||
@if (context.PaymentDate != null)
|
<MudChip T="string" Size="Size.Small"
|
||||||
{
|
Color="@GetPaymentStatusColor(context.PaymentStatus)"
|
||||||
@context.PaymentDate.ToDateTime().MiladiToJalaliWithTime()
|
Variant="Variant.Filled">
|
||||||
}
|
@GetPaymentStatusText(context.PaymentStatus)
|
||||||
else
|
</MudChip>
|
||||||
{
|
|
||||||
<text>در انتظار پرداخت</text>
|
|
||||||
}
|
|
||||||
</MudTd>
|
</MudTd>
|
||||||
<MudTd>@GetStatusText(context.PaymentStatus)</MudTd>
|
|
||||||
<MudTd>@FormatPrice(context.FactorDetails.Sum(s=>(s.UnitPrice ?? 0) * (s.Count ?? 0)))</MudTd>
|
|
||||||
<MudTd>
|
<MudTd>
|
||||||
<MudButton Variant="Variant.Text" Href="@($"{RouteConstants.Store.OrderDetail}{context.Id}")" StartIcon="@Icons.Material.Filled.Receipt">جزئیات</MudButton>
|
<MudChip T="string" Size="Size.Small"
|
||||||
|
Color="@GetDeliveryStatusColor((int)context.DeliveryStatus)"
|
||||||
|
Variant="Variant.Outlined">
|
||||||
|
@GetDeliveryStatusText((int)context.DeliveryStatus)
|
||||||
|
</MudChip>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>@FormatDate(context)</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
|
||||||
|
OnClick="() => ViewOrder(context.Id)">
|
||||||
|
جزئیات
|
||||||
|
</MudButton>
|
||||||
</MudTd>
|
</MudTd>
|
||||||
</RowTemplate>
|
</RowTemplate>
|
||||||
</MudTable>
|
</MudTable>
|
||||||
|
</MudPaper>
|
||||||
</MudHidden>
|
</MudHidden>
|
||||||
|
|
||||||
|
<!-- Mobile Cards -->
|
||||||
<MudHidden Breakpoint="Breakpoint.MdAndUp">
|
<MudHidden Breakpoint="Breakpoint.MdAndUp">
|
||||||
<MudStack Spacing="2">
|
<MudStack Spacing="2">
|
||||||
@foreach (var o in _orders)
|
@foreach (var order in _orders)
|
||||||
{
|
{
|
||||||
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
|
<MudPaper Class="pa-3 rounded-lg cursor-pointer" Outlined="true"
|
||||||
|
@onclick="() => ViewOrder(order.Id)">
|
||||||
<MudStack Spacing="1">
|
<MudStack Spacing="1">
|
||||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||||
<MudText>سفارش #@o.Id</MudText>
|
<MudText Typo="Typo.subtitle2">سفارش #@order.Id</MudText>
|
||||||
<MudText Class="mud-text-secondary">
|
<MudChip T="string" Size="Size.Small"
|
||||||
@if (o.PaymentDate != null)
|
Color="@GetPaymentStatusColor(order.PaymentStatus)"
|
||||||
{
|
Variant="Variant.Filled">
|
||||||
@o.PaymentDate.ToDateTime().MiladiToJalaliWithTime()
|
@GetPaymentStatusText(order.PaymentStatus)
|
||||||
}
|
</MudChip>
|
||||||
else
|
|
||||||
{
|
|
||||||
<text>در انتظار پرداخت</text>
|
|
||||||
}
|
|
||||||
</MudText>
|
|
||||||
</MudStack>
|
</MudStack>
|
||||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||||
<MudText>وضعیت: @GetStatusText(o.PaymentStatus)</MudText>
|
<MudText Typo="Typo.caption" Class="mud-text-secondary">@FormatDate(order)</MudText>
|
||||||
<MudText Color="Color.Primary">@FormatPrice(o.FactorDetails.Sum(s=>(s.UnitPrice ?? 0) * (s.Count ?? 0)))</MudText>
|
<MudChip T="string" Size="Size.Small"
|
||||||
|
Color="@GetDeliveryStatusColor((int)order.DeliveryStatus)"
|
||||||
|
Variant="Variant.Outlined">
|
||||||
|
@GetDeliveryStatusText((int)order.DeliveryStatus)
|
||||||
|
</MudChip>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
<MudStack Row="true" Justify="Justify.FlexEnd">
|
<MudDivider />
|
||||||
<MudButton Size="Size.Small" Variant="Variant.Outlined" Href="@($"{RouteConstants.Store.OrderDetail}{o.Id}")" StartIcon="@Icons.Material.Filled.Receipt">جزئیات</MudButton>
|
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||||
|
<MudText Typo="Typo.body2">@GetItemsCount(order) قلم</MudText>
|
||||||
|
<MudText Typo="Typo.body2" Class="fw-semibold">@FormatPrice(GetTotalAmount(order)) تومان</MudText>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
@@ -77,4 +98,5 @@
|
|||||||
</MudStack>
|
</MudStack>
|
||||||
</MudHidden>
|
</MudHidden>
|
||||||
}
|
}
|
||||||
|
</MudStack>
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System;
|
|
||||||
using CMSMicroservice.Protobuf.Protos.UserOrder;
|
using CMSMicroservice.Protobuf.Protos.UserOrder;
|
||||||
|
using DateTimeConverterCL;
|
||||||
using FrontOffice.Main.Utilities;
|
using FrontOffice.Main.Utilities;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Messages = CMSMicroservice.Protobuf.Protos;
|
using Messages = CMSMicroservice.Protobuf.Protos;
|
||||||
@@ -10,7 +10,6 @@ namespace FrontOffice.Main.Pages.Store;
|
|||||||
public partial class Orders : ComponentBase
|
public partial class Orders : ComponentBase
|
||||||
{
|
{
|
||||||
[Inject] private OrderService OrderService { get; set; } = default!;
|
[Inject] private OrderService OrderService { get; set; } = default!;
|
||||||
[Inject] private VATService VAT { get; set; } = default!;
|
|
||||||
|
|
||||||
private List<GetUserOrderResponse> _orders = new();
|
private List<GetUserOrderResponse> _orders = new();
|
||||||
private bool _loading;
|
private bool _loading;
|
||||||
@@ -33,28 +32,81 @@ public partial class Orders : ComponentBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
private void ViewOrder(long orderId)
|
||||||
{
|
{
|
||||||
if (firstRender)
|
Navigation.NavigateTo($"{RouteConstants.Store.OrderDetail}{orderId}");
|
||||||
{
|
|
||||||
// بارگذاری نرخ VAT
|
|
||||||
await VAT.LoadAsync();
|
|
||||||
}
|
|
||||||
await base.OnAfterRenderAsync(firstRender);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
|
private static string FormatPrice(long price) => $"{price:N0}";
|
||||||
|
|
||||||
|
private static string FormatDate(GetUserOrderResponse order)
|
||||||
private string GetStatusText(Messages.PaymentStatus contextPaymentStatus)
|
|
||||||
{
|
{
|
||||||
return contextPaymentStatus switch
|
try
|
||||||
|
{
|
||||||
|
var timestamp = order.PaymentDate;
|
||||||
|
if (timestamp is null)
|
||||||
|
return "در انتظار پرداخت";
|
||||||
|
|
||||||
|
return timestamp.ToDateTime().ToLocalTime().MiladiToJalaliWithTime();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetItemsCount(GetUserOrderResponse order) =>
|
||||||
|
order.FactorDetails.Sum(f => f.Count ?? 0);
|
||||||
|
|
||||||
|
private static long GetTotalAmount(GetUserOrderResponse order)
|
||||||
|
{
|
||||||
|
if (order.VatInfo is not null && order.VatInfo.TotalAmount > 0)
|
||||||
|
return order.VatInfo.TotalAmount;
|
||||||
|
|
||||||
|
if (order.Amount > 0)
|
||||||
|
return order.Amount;
|
||||||
|
|
||||||
|
return order.FactorDetails.Sum(f => (f.UnitPrice ?? 0) * (f.Count ?? 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetPaymentStatusText(Messages.PaymentStatus status) => status switch
|
||||||
{
|
{
|
||||||
Messages.PaymentStatus.Pending => "در انتظار پرداخت",
|
Messages.PaymentStatus.Pending => "در انتظار پرداخت",
|
||||||
Messages.PaymentStatus.Success => "پرداخت شده",
|
Messages.PaymentStatus.Success => "پرداخت شده",
|
||||||
Messages.PaymentStatus.Reject => "پرداخت ناموفق",
|
Messages.PaymentStatus.Reject => "پرداخت ناموفق",
|
||||||
_ => "نامشخص",
|
_ => "نامشخص"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Color GetPaymentStatusColor(Messages.PaymentStatus status) => status switch
|
||||||
|
{
|
||||||
|
Messages.PaymentStatus.Pending => Color.Warning,
|
||||||
|
Messages.PaymentStatus.Success => Color.Success,
|
||||||
|
Messages.PaymentStatus.Reject => Color.Error,
|
||||||
|
_ => Color.Default
|
||||||
|
};
|
||||||
|
|
||||||
|
// Domain/public_messages: None=0, Pending=1, InTransit=2, Delivered=3, Returned=4, Cancelled=5, ReadyForOfficePickup=6
|
||||||
|
private static string GetDeliveryStatusText(int status) => status switch
|
||||||
|
{
|
||||||
|
0 => "بدون ارسال",
|
||||||
|
1 => "در انتظار ارسال",
|
||||||
|
2 => "ارسال شده",
|
||||||
|
3 => "تحویل داده شده",
|
||||||
|
4 => "مرجوع شده",
|
||||||
|
5 => "لغو شده",
|
||||||
|
6 => "آماده تحویل در دفتر",
|
||||||
|
_ => "نامشخص"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Color GetDeliveryStatusColor(int status) => status switch
|
||||||
|
{
|
||||||
|
0 => Color.Default,
|
||||||
|
1 => Color.Warning,
|
||||||
|
2 => Color.Primary,
|
||||||
|
3 => Color.Success,
|
||||||
|
4 => Color.Error,
|
||||||
|
5 => Color.Dark,
|
||||||
|
6 => Color.Primary,
|
||||||
|
_ => Color.Default
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -118,7 +118,8 @@
|
|||||||
@foreach (var p in _products)
|
@foreach (var p in _products)
|
||||||
{
|
{
|
||||||
<MudItem xs="6" sm="6" md="3"
|
<MudItem xs="6" sm="6" md="3"
|
||||||
onclick="@(() => Navigation.NavigateTo($"{RouteConstants.Store.ProductDetail}{p.Id}"))">
|
onclick="@(() => OpenProduct(p.Id))">
|
||||||
|
<div id="@($"shop-product-{p.Id}")" class="h-100">
|
||||||
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden"
|
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden"
|
||||||
Style="cursor:pointer;">
|
Style="cursor:pointer;">
|
||||||
|
|
||||||
@@ -164,6 +165,7 @@
|
|||||||
</MudButton>
|
</MudButton>
|
||||||
</MudCardActions>
|
</MudCardActions>
|
||||||
</MudCard>
|
</MudCard>
|
||||||
|
</div>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
}
|
}
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
|
|||||||
@@ -2,19 +2,11 @@ using System.Linq;
|
|||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.AspNetCore.Components.Routing;
|
using Microsoft.AspNetCore.Components.Routing;
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
using Microsoft.AspNetCore.WebUtilities;
|
using Microsoft.JSInterop;
|
||||||
using FrontOffice.Main.Utilities;
|
using FrontOffice.Main.Utilities;
|
||||||
|
|
||||||
namespace FrontOffice.Main.Pages.Store;
|
namespace FrontOffice.Main.Pages.Store;
|
||||||
|
|
||||||
public enum ProductSortOption
|
|
||||||
{
|
|
||||||
PriceDesc, // گرانترین (پیشفرض)
|
|
||||||
PriceAsc, // ارزانترین
|
|
||||||
Newest, // جدیدترین
|
|
||||||
Title // الفبایی
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class Products : ComponentBase, IDisposable
|
public partial class Products : ComponentBase, IDisposable
|
||||||
{
|
{
|
||||||
[Inject] private ProductService ProductService { get; set; } = default!;
|
[Inject] private ProductService ProductService { get; set; } = default!;
|
||||||
@@ -23,6 +15,7 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
[Inject] private VATService VAT { get; set; } = default!;
|
[Inject] private VATService VAT { get; set; } = default!;
|
||||||
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
||||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||||
|
[Inject] private IJSRuntime Js { get; set; } = default!;
|
||||||
|
|
||||||
private bool _isAuthenticated;
|
private bool _isAuthenticated;
|
||||||
private string _query = string.Empty;
|
private string _query = string.Empty;
|
||||||
@@ -35,8 +28,9 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
private List<Product> _products = new();
|
private List<Product> _products = new();
|
||||||
private long? _activeCategoryId;
|
private long? _activeCategoryId;
|
||||||
private string? _activeCategoryTitle;
|
private string? _activeCategoryTitle;
|
||||||
|
private ProductSortOption _sortOption = ProductSortOption.PriceDesc;
|
||||||
private ProductSortOption _sortOption = ProductSortOption.PriceDesc; // پیشفرض: گرانترین
|
private bool _ignoreNextLocationChange;
|
||||||
|
private bool _pendingScrollRestore;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
@@ -44,36 +38,98 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
await Cart.EnsureInitializedAsync();
|
await Cart.EnsureInitializedAsync();
|
||||||
Cart.OnChange += StateHasChanged;
|
Cart.OnChange += StateHasChanged;
|
||||||
Navigation.LocationChanged += HandleLocationChanged;
|
Navigation.LocationChanged += HandleLocationChanged;
|
||||||
await LoadInitial();
|
ApplyStateFromUri();
|
||||||
|
await LoadPages(_currentPage);
|
||||||
|
_pendingScrollRestore = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (firstRender)
|
||||||
{
|
|
||||||
// بارگذاری نرخ VAT
|
|
||||||
await VAT.LoadAsync();
|
await VAT.LoadAsync();
|
||||||
|
|
||||||
|
if (_pendingScrollRestore && !_loading && _products.Count > 0)
|
||||||
|
{
|
||||||
|
_pendingScrollRestore = false;
|
||||||
|
var payload = await ShopListScrollRestore.TakeAsync(Js, ShopListScrollRestore.StoreKey);
|
||||||
|
if (payload is not null)
|
||||||
|
await ShopListScrollRestore.RestoreAsync(Js, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
await base.OnAfterRenderAsync(firstRender);
|
await base.OnAfterRenderAsync(firstRender);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadInitial()
|
private void ApplyStateFromUri()
|
||||||
|
{
|
||||||
|
var state = ShopListQueryState.Parse(Navigation.ToAbsoluteUri(Navigation.Uri));
|
||||||
|
_query = state.Query;
|
||||||
|
_sortOption = ShopListQueryState.ParseSortOption(state.Sort);
|
||||||
|
_activeCategoryId = state.CategoryId;
|
||||||
|
_currentPage = state.Pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ShopListQueryState CaptureState() => new()
|
||||||
|
{
|
||||||
|
Query = _query,
|
||||||
|
Sort = ShopListQueryState.ToSortKey(_sortOption),
|
||||||
|
CategoryId = _activeCategoryId,
|
||||||
|
Pages = Math.Max(1, _currentPage)
|
||||||
|
};
|
||||||
|
|
||||||
|
private void SyncUrl()
|
||||||
|
{
|
||||||
|
var target = CaptureState().ToRelativeUrl(RouteConstants.Store.Products);
|
||||||
|
var currentPathAndQuery = Navigation.ToAbsoluteUri(Navigation.Uri).PathAndQuery;
|
||||||
|
if (string.Equals(currentPathAndQuery, target, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(currentPathAndQuery, target + "/", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_ignoreNextLocationChange = true;
|
||||||
|
Navigation.NavigateTo(target, replace: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadPages(int pagesToLoad)
|
||||||
{
|
{
|
||||||
_loading = true;
|
_loading = true;
|
||||||
_currentPage = 1;
|
|
||||||
_products.Clear();
|
_products.Clear();
|
||||||
UpdateCategoryFilterFromUri();
|
_hasMore = true;
|
||||||
var sortBy = GetSortByValue();
|
pagesToLoad = Math.Max(1, pagesToLoad);
|
||||||
var result = await ProductService.GetProductsPagedAsync(_query, _activeCategoryId, sortBy, _currentPage, PageSize);
|
var sortBy = ShopListQueryState.ToApiSortBy(_sortOption);
|
||||||
|
|
||||||
|
for (var page = 1; page <= pagesToLoad; page++)
|
||||||
|
{
|
||||||
|
var result = await ProductService.GetProductsPagedAsync(
|
||||||
|
_query, _activeCategoryId, sortBy, page, PageSize);
|
||||||
|
if (page == 1)
|
||||||
|
{
|
||||||
_products = result.Products;
|
_products = result.Products;
|
||||||
_hasMore = result.HasNext;
|
|
||||||
_totalCount = result.TotalCount;
|
_totalCount = result.TotalCount;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_products.AddRange(result.Products);
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentPage = page;
|
||||||
|
_hasMore = result.HasNext;
|
||||||
|
if (!_hasMore)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
_activeCategoryTitle = _activeCategoryId is { } categoryId
|
_activeCategoryTitle = _activeCategoryId is { } categoryId
|
||||||
? (await CategoryService.GetByIdAsync(categoryId))?.Title
|
? (await CategoryService.GetByIdAsync(categoryId))?.Title
|
||||||
: null;
|
: null;
|
||||||
_loading = false;
|
_loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ReloadFromFilters()
|
||||||
|
{
|
||||||
|
_currentPage = 1;
|
||||||
|
await LoadPages(1);
|
||||||
|
SyncUrl();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task LoadMore()
|
private async Task LoadMore()
|
||||||
{
|
{
|
||||||
if (_loadingMore || !_hasMore) return;
|
if (_loadingMore || !_hasMore) return;
|
||||||
@@ -82,42 +138,32 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
|
|
||||||
_currentPage++;
|
_currentPage++;
|
||||||
var sortBy = GetSortByValue();
|
var sortBy = ShopListQueryState.ToApiSortBy(_sortOption);
|
||||||
var result = await ProductService.GetProductsPagedAsync(_query, _activeCategoryId, sortBy, _currentPage, PageSize);
|
var result = await ProductService.GetProductsPagedAsync(
|
||||||
|
_query, _activeCategoryId, sortBy, _currentPage, PageSize);
|
||||||
_products.AddRange(result.Products);
|
_products.AddRange(result.Products);
|
||||||
_hasMore = result.HasNext;
|
_hasMore = result.HasNext;
|
||||||
|
SyncUrl();
|
||||||
|
|
||||||
_loadingMore = false;
|
_loadingMore = false;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetSortByValue()
|
private async Task OnSortChanged() => await ReloadFromFilters();
|
||||||
{
|
|
||||||
return _sortOption switch
|
|
||||||
{
|
|
||||||
ProductSortOption.PriceDesc => "price desc",
|
|
||||||
ProductSortOption.PriceAsc => "price asc",
|
|
||||||
ProductSortOption.Newest => "id desc",
|
|
||||||
ProductSortOption.Title => "title asc",
|
|
||||||
_ => "price desc"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnSortChanged()
|
private async Task OnQueryChanged(KeyboardEventArgs _) => await ReloadFromFilters();
|
||||||
{
|
|
||||||
await LoadInitial();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnQueryChanged(KeyboardEventArgs _)
|
|
||||||
{
|
|
||||||
await LoadInitial();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddToCart(Product p)
|
private async Task AddToCart(Product p)
|
||||||
{
|
{
|
||||||
await GuestGate.RunAsync(() => Cart.Add(p, 1));
|
await GuestGate.RunAsync(() => Cart.Add(p, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task OpenProduct(long productId)
|
||||||
|
{
|
||||||
|
await ShopListScrollRestore.SaveAsync(Js, ShopListScrollRestore.StoreKey, productId);
|
||||||
|
Navigation.NavigateTo($"{RouteConstants.Store.ProductDetail}{productId}");
|
||||||
|
}
|
||||||
|
|
||||||
private string FormatPrice(long price) => $"{VAT.AddVAT(price):N0} تومان";
|
private string FormatPrice(long price) => $"{VAT.AddVAT(price):N0} تومان";
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@@ -128,24 +174,33 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
|
|
||||||
private void HandleLocationChanged(object? sender, LocationChangedEventArgs args)
|
private void HandleLocationChanged(object? sender, LocationChangedEventArgs args)
|
||||||
{
|
{
|
||||||
_ = InvokeAsync(LoadInitial);
|
if (_ignoreNextLocationChange)
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateCategoryFilterFromUri()
|
|
||||||
{
|
{
|
||||||
var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
|
_ignoreNextLocationChange = false;
|
||||||
if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("category", out var values) &&
|
|
||||||
long.TryParse(values.FirstOrDefault(), out var categoryId))
|
|
||||||
{
|
|
||||||
_activeCategoryId = categoryId;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_activeCategoryId = null;
|
var uri = Navigation.ToAbsoluteUri(args.Location);
|
||||||
|
if (!uri.AbsolutePath.Equals(RouteConstants.Store.Products, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var incoming = ShopListQueryState.Parse(uri);
|
||||||
|
if (incoming.Matches(CaptureState()))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_ = InvokeAsync(async () =>
|
||||||
|
{
|
||||||
|
ApplyStateFromUri();
|
||||||
|
await LoadPages(_currentPage);
|
||||||
|
_pendingScrollRestore = true;
|
||||||
|
StateHasChanged();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ClearCategoryFilter()
|
private async Task ClearCategoryFilter()
|
||||||
{
|
{
|
||||||
Navigation.NavigateTo(RouteConstants.Store.Products);
|
_activeCategoryId = null;
|
||||||
|
_activeCategoryTitle = null;
|
||||||
|
await ReloadFromFilters();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,10 +60,14 @@ webApp.MapBlazorHub();
|
|||||||
|
|
||||||
webApp.MapGet("/sitemap.xml", async (SitemapGenerator sitemapGenerator, CancellationToken cancellationToken) =>
|
webApp.MapGet("/sitemap.xml", async (SitemapGenerator sitemapGenerator, CancellationToken cancellationToken) =>
|
||||||
{
|
{
|
||||||
var xml = await sitemapGenerator.GenerateAsync(cancellationToken);
|
var bytes = await sitemapGenerator.GenerateBytesAsync(cancellationToken);
|
||||||
return Results.Content(xml, "application/xml; charset=utf-8");
|
return Results.File(bytes, "application/xml; charset=utf-8");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Legacy URLs from old site / common e-commerce paths — redirect to homepage
|
||||||
|
webApp.MapGet("/wishlist", () => Results.Redirect("/", permanent: true));
|
||||||
|
webApp.MapGet("/wishlist/", () => Results.Redirect("/", permanent: true));
|
||||||
|
|
||||||
webApp.MapFallbackToPage("/_Host");
|
webApp.MapFallbackToPage("/_Host");
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@using FrontOffice.Main.Utilities
|
||||||
|
|
||||||
<MudDialog>
|
<MudDialog>
|
||||||
<TitleContent>
|
<TitleContent>
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||||
@@ -7,9 +9,34 @@
|
|||||||
</TitleContent>
|
</TitleContent>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<MudStack Spacing="2">
|
<MudStack Spacing="2">
|
||||||
|
@if (AllowChargeCredit)
|
||||||
|
{
|
||||||
|
@if (IsMagicWallet && !MagicCeilingFull)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body2" Class="mud-text-secondary">
|
||||||
|
برای تکمیل این سفارش موجودی شما کافی نیست. کیف پول شما در حالت جادویی است؛ برای دریافت ضریب، از مسیر شارژ جادویی اقدام کنید.
|
||||||
|
</MudText>
|
||||||
|
}
|
||||||
|
else if (IsMagicWallet && MagicCeilingFull)
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Warning" Dense="true" Variant="Variant.Outlined">
|
||||||
|
سقف شارژ جادویی شما در این دور پر است. میتوانید بهصورت استثنایی کیف اصلی را بدون ضریب (۱:۱) شارژ کنید.
|
||||||
|
</MudAlert>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">
|
<MudText Typo="Typo.body2" Class="mud-text-secondary">
|
||||||
برای تکمیل این سفارش، موجودی اصلی شما کافی نیست. میتوانید کیف پول را شارژ کنید و سپس سفارش را ثبت کنید.
|
برای تکمیل این سفارش، موجودی اصلی شما کافی نیست. میتوانید کیف پول را شارژ کنید و سپس سفارش را ثبت کنید.
|
||||||
</MudText>
|
</MudText>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Warning" Dense="true" Variant="Variant.Outlined">
|
||||||
|
برای شارژ کیف پول اصلی ابتدا باید پکیج را خریداری کرده و قرارداد باشگاه مشتریان را امضا کنید.
|
||||||
|
</MudAlert>
|
||||||
|
}
|
||||||
|
|
||||||
<MudPaper Outlined="true" Class="pa-3 rounded-lg">
|
<MudPaper Outlined="true" Class="pa-3 rounded-lg">
|
||||||
<MudStack Spacing="1">
|
<MudStack Spacing="1">
|
||||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||||
@@ -31,12 +58,33 @@
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<MudButton OnClick="Cancel">بستن</MudButton>
|
<MudButton OnClick="Cancel">بستن</MudButton>
|
||||||
|
@if (AllowChargeCredit)
|
||||||
|
{
|
||||||
|
<MudButton Variant="Variant.Filled"
|
||||||
|
Color="@(IsMagicWallet && !MagicCeilingFull ? Color.Secondary : Color.Primary)"
|
||||||
|
StartIcon="@(IsMagicWallet && !MagicCeilingFull ? Icons.Material.Filled.AutoAwesome : Icons.Material.Filled.Payment)"
|
||||||
|
OnClick="GoToCharge">
|
||||||
|
@ChargeButtonLabel
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
else if (!HasPurchasedPackage)
|
||||||
|
{
|
||||||
<MudButton Variant="Variant.Filled"
|
<MudButton Variant="Variant.Filled"
|
||||||
Color="Color.Primary"
|
Color="Color.Primary"
|
||||||
StartIcon="@Icons.Material.Filled.Payment"
|
StartIcon="@Icons.Material.Filled.CardGiftcard"
|
||||||
OnClick="GoToCharge">
|
OnClick="GoToPackages">
|
||||||
شارژ حساب اصلی
|
مشاهده پکیجها
|
||||||
</MudButton>
|
</MudButton>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudButton Variant="Variant.Filled"
|
||||||
|
Color="Color.Primary"
|
||||||
|
StartIcon="@Icons.Material.Filled.Handshake"
|
||||||
|
OnClick="GoToClubMembership">
|
||||||
|
امضای قرارداد باشگاه
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</MudDialog>
|
</MudDialog>
|
||||||
|
|
||||||
@@ -53,9 +101,52 @@
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public long ShortfallAmount { get; set; }
|
public long ShortfallAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>اگر false باشد، به صفحه شارژ هدایت نمیشود.</summary>
|
||||||
|
[Parameter]
|
||||||
|
public bool AllowChargeCredit { get; set; } = true;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool HasPurchasedPackage { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool IsMagicWallet { get; set; }
|
||||||
|
|
||||||
|
/// <summary>سقف واریز جادویی این دور پر شده است.</summary>
|
||||||
|
[Parameter]
|
||||||
|
public bool MagicCeilingFull { get; set; }
|
||||||
|
|
||||||
|
private string ChargeButtonLabel =>
|
||||||
|
IsMagicWallet && !MagicCeilingFull
|
||||||
|
? "شارژ کیف جادویی"
|
||||||
|
: IsMagicWallet && MagicCeilingFull
|
||||||
|
? "شارژ عادی بدون ضریب (سقف جادویی پر است)"
|
||||||
|
: "شارژ حساب اصلی";
|
||||||
|
|
||||||
private void Cancel() => MudDialog.Cancel();
|
private void Cancel() => MudDialog.Cancel();
|
||||||
|
|
||||||
private void GoToCharge() => MudDialog.Close(DialogResult.Ok(ShortfallAmount));
|
private void GoToCharge()
|
||||||
|
{
|
||||||
|
if (IsMagicWallet && !MagicCeilingFull)
|
||||||
|
{
|
||||||
|
MudDialog.Cancel();
|
||||||
|
Navigation.NavigateTo(RouteConstants.Profile.MagicWallet);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MudDialog.Close(DialogResult.Ok(ShortfallAmount));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GoToPackages()
|
||||||
|
{
|
||||||
|
MudDialog.Cancel();
|
||||||
|
Navigation.NavigateTo(RouteConstants.Package.List);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GoToClubMembership()
|
||||||
|
{
|
||||||
|
MudDialog.Cancel();
|
||||||
|
Navigation.NavigateTo(RouteConstants.Club.Membership);
|
||||||
|
}
|
||||||
|
|
||||||
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
|
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ public class AuthService
|
|||||||
var isSignMainContractStr = claims.FirstOrDefault(c => c.key == "IsSignMainContract").value;
|
var isSignMainContractStr = claims.FirstOrDefault(c => c.key == "IsSignMainContract").value;
|
||||||
var hasPurchasedPackageStr = claims.FirstOrDefault(c => c.key == "HasPurchasedPackage").value;
|
var hasPurchasedPackageStr = claims.FirstOrDefault(c => c.key == "HasPurchasedPackage").value;
|
||||||
var isClubMemberActiveStr = claims.FirstOrDefault(c => c.key == "IsClubMemberActive").value;
|
var isClubMemberActiveStr = claims.FirstOrDefault(c => c.key == "IsClubMemberActive").value;
|
||||||
|
var hasAddressStr = claims.FirstOrDefault(c => c.key == "HasAddress").value;
|
||||||
|
|
||||||
_userAuthInfo.UserId = long.TryParse(userIdStr, out var userId) ? userId : 0;
|
_userAuthInfo.UserId = long.TryParse(userIdStr, out var userId) ? userId : 0;
|
||||||
_userAuthInfo.FirstName = firstName ?? string.Empty;
|
_userAuthInfo.FirstName = firstName ?? string.Empty;
|
||||||
@@ -88,6 +89,7 @@ public class AuthService
|
|||||||
_userAuthInfo.IsSignMainContract = bool.TryParse(isSignMainContractStr, out var isSignMainContract) && isSignMainContract;
|
_userAuthInfo.IsSignMainContract = bool.TryParse(isSignMainContractStr, out var isSignMainContract) && isSignMainContract;
|
||||||
_userAuthInfo.HasPurchasedPackage = bool.TryParse(hasPurchasedPackageStr, out var hasPurchased) && hasPurchased;
|
_userAuthInfo.HasPurchasedPackage = bool.TryParse(hasPurchasedPackageStr, out var hasPurchased) && hasPurchased;
|
||||||
_userAuthInfo.IsClubMemberActive = bool.TryParse(isClubMemberActiveStr, out var isClubActive) && isClubActive;
|
_userAuthInfo.IsClubMemberActive = bool.TryParse(isClubMemberActiveStr, out var isClubActive) && isClubActive;
|
||||||
|
_userAuthInfo.HasAddress = bool.TryParse(hasAddressStr, out var hasAddress) && hasAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<(string key, string value)>?> GetTokenClaimsAsync()
|
private async Task<List<(string key, string value)>?> GetTokenClaimsAsync()
|
||||||
|
|||||||
@@ -219,6 +219,8 @@ public class DiscountOrderService
|
|||||||
2 => "ارسال شده",
|
2 => "ارسال شده",
|
||||||
3 => "تحویل داده شده",
|
3 => "تحویل داده شده",
|
||||||
4 => "لغو شده",
|
4 => "لغو شده",
|
||||||
|
5 => "آماده تحویل در دفتر",
|
||||||
|
6 => "مرجوع شده",
|
||||||
_ => "نامشخص"
|
_ => "نامشخص"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -228,7 +230,9 @@ public class DiscountOrderService
|
|||||||
1 => Color.Info,
|
1 => Color.Info,
|
||||||
2 => Color.Primary,
|
2 => Color.Primary,
|
||||||
3 => Color.Success,
|
3 => Color.Success,
|
||||||
4 => Color.Error,
|
4 => Color.Dark,
|
||||||
|
5 => Color.Primary,
|
||||||
|
6 => Color.Error,
|
||||||
_ => Color.Default
|
_ => Color.Default
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace FrontOffice.Main.Utilities.Seo;
|
namespace FrontOffice.Main.Utilities.Seo;
|
||||||
@@ -30,6 +31,9 @@ public class SeoMetadataProvider
|
|||||||
[RouteConstants.Store.Products] = (
|
[RouteConstants.Store.Products] = (
|
||||||
"محصولات | کارابازار سلامت",
|
"محصولات | کارابازار سلامت",
|
||||||
"فروشگاه محصولات سلامت کارابازار — خرید آنلاین با پاداش باشگاه مشتریان."),
|
"فروشگاه محصولات سلامت کارابازار — خرید آنلاین با پاداش باشگاه مشتریان."),
|
||||||
|
[RouteConstants.Store.Categories] = (
|
||||||
|
"دستهبندیها | کارابازار سلامت",
|
||||||
|
"دستهبندی محصولات فروشگاه کارابازار سلامت."),
|
||||||
[RouteConstants.Package.List] = (
|
[RouteConstants.Package.List] = (
|
||||||
"پکیجها | کارابازار سلامت",
|
"پکیجها | کارابازار سلامت",
|
||||||
"پکیجهای عضویت باشگاه مشتریان کارابازار سلامت."),
|
"پکیجهای عضویت باشگاه مشتریان کارابازار سلامت."),
|
||||||
@@ -45,6 +49,18 @@ public class SeoMetadataProvider
|
|||||||
[RouteConstants.Registration.Wizard] = (
|
[RouteConstants.Registration.Wizard] = (
|
||||||
"ثبتنام | کارابازار سلامت",
|
"ثبتنام | کارابازار سلامت",
|
||||||
"ثبتنام سریع در باشگاه مشتریان کارابازار سلامت."),
|
"ثبتنام سریع در باشگاه مشتریان کارابازار سلامت."),
|
||||||
|
[RouteConstants.Gateway.StoreChooser] = (
|
||||||
|
"فروشگاهها | کارابازار سلامت",
|
||||||
|
"انتخاب فروشگاه اصلی یا اعتباری در کارابازار سلامت."),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly HashSet<string> NoIndexPaths = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"/wishlist",
|
||||||
|
"/wp-admin",
|
||||||
|
"/wp-login.php",
|
||||||
|
"/feed",
|
||||||
|
"/xmlrpc.php",
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly HashSet<string> NoIndexPrefixes = new(StringComparer.OrdinalIgnoreCase)
|
private static readonly HashSet<string> NoIndexPrefixes = new(StringComparer.OrdinalIgnoreCase)
|
||||||
@@ -62,8 +78,20 @@ public class SeoMetadataProvider
|
|||||||
"/discount-store/order/",
|
"/discount-store/order/",
|
||||||
RouteConstants.Gateway.OrdersChooser,
|
RouteConstants.Gateway.OrdersChooser,
|
||||||
RouteConstants.Gateway.CartChooser,
|
RouteConstants.Gateway.CartChooser,
|
||||||
|
RouteConstants.Commission.Dashboard,
|
||||||
|
"/commission/",
|
||||||
|
RouteConstants.Network.Statistics,
|
||||||
|
"/network/",
|
||||||
|
RouteConstants.Package.MyPackages,
|
||||||
|
"/wp-content/",
|
||||||
|
"/wp-includes/",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static readonly Regex ProductDetailPattern = new(@"^/product/\d+$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
private static readonly Regex DiscountProductPattern = new(@"^/discount-store/product/\d+$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
private static readonly Regex PackageDetailPattern = new(@"^/package/\d+$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
private static readonly Regex CheckoutPackagePattern = new(@"^/checkout/\d+$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
|
||||||
public SeoMetadataProvider(BlogPostService blogPostService, IOptions<SeoSettings> settings)
|
public SeoMetadataProvider(BlogPostService blogPostService, IOptions<SeoSettings> settings)
|
||||||
{
|
{
|
||||||
_blogPostService = blogPostService;
|
_blogPostService = blogPostService;
|
||||||
@@ -121,6 +149,8 @@ public class SeoMetadataProvider
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (IsIndexableDynamicRoute(normalizedPath))
|
||||||
|
{
|
||||||
return new SeoPageMetadata
|
return new SeoPageMetadata
|
||||||
{
|
{
|
||||||
Title = _settings.SiteName,
|
Title = _settings.SiteName,
|
||||||
@@ -129,6 +159,21 @@ public class SeoMetadataProvider
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return new SeoPageMetadata
|
||||||
|
{
|
||||||
|
Title = _settings.SiteName,
|
||||||
|
Description = _settings.DefaultDescription,
|
||||||
|
CanonicalUrl = $"{baseUrl}{normalizedPath}",
|
||||||
|
NoIndex = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsIndexableDynamicRoute(string path) =>
|
||||||
|
ProductDetailPattern.IsMatch(path)
|
||||||
|
|| DiscountProductPattern.IsMatch(path)
|
||||||
|
|| PackageDetailPattern.IsMatch(path)
|
||||||
|
|| CheckoutPackagePattern.IsMatch(path);
|
||||||
|
|
||||||
private static string NormalizePath(string path)
|
private static string NormalizePath(string path)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
@@ -146,6 +191,9 @@ public class SeoMetadataProvider
|
|||||||
|
|
||||||
private static bool IsNoIndexPath(string path)
|
private static bool IsNoIndexPath(string path)
|
||||||
{
|
{
|
||||||
|
if (NoIndexPaths.Contains(path))
|
||||||
|
return true;
|
||||||
|
|
||||||
foreach (var prefix in NoIndexPrefixes)
|
foreach (var prefix in NoIndexPrefixes)
|
||||||
{
|
{
|
||||||
if (path.Equals(prefix, StringComparison.OrdinalIgnoreCase)
|
if (path.Equals(prefix, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Xml;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
@@ -71,16 +72,29 @@ public class SitemapGenerator
|
|||||||
}
|
}
|
||||||
|
|
||||||
var document = new XDocument(
|
var document = new XDocument(
|
||||||
new XDeclaration("1.0", "UTF-8", null),
|
new XDeclaration("1.0", "utf-8", null),
|
||||||
new XElement(ns + "urlset", urls));
|
new XElement(ns + "urlset", urls));
|
||||||
|
|
||||||
var builder = new StringBuilder();
|
var settings = new XmlWriterSettings
|
||||||
using (var writer = new StringWriter(builder))
|
{
|
||||||
|
Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false),
|
||||||
|
Indent = true,
|
||||||
|
OmitXmlDeclaration = false
|
||||||
|
};
|
||||||
|
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
using (var writer = XmlWriter.Create(stream, settings))
|
||||||
{
|
{
|
||||||
document.Save(writer);
|
document.Save(writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.ToString();
|
return Encoding.UTF8.GetString(stream.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<byte[]> GenerateBytesAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var xml = await GenerateAsync(cancellationToken);
|
||||||
|
return Encoding.UTF8.GetBytes(xml);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static XElement CreateUrlElement(
|
private static XElement CreateUrlElement(
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
namespace FrontOffice.Main.Utilities;
|
||||||
|
|
||||||
|
public enum ProductSortOption
|
||||||
|
{
|
||||||
|
PriceDesc,
|
||||||
|
PriceAsc,
|
||||||
|
Newest,
|
||||||
|
Title
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// وضعیت لیست فروشگاه در query string — فقط برای /products و /discount-store.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ShopListQueryState
|
||||||
|
{
|
||||||
|
public string Query { get; init; } = string.Empty;
|
||||||
|
public string? Sort { get; init; }
|
||||||
|
public long? CategoryId { get; init; }
|
||||||
|
public int Pages { get; init; } = 1;
|
||||||
|
|
||||||
|
public static ShopListQueryState Parse(Uri uri)
|
||||||
|
{
|
||||||
|
var q = QueryHelpers.ParseQuery(uri.Query);
|
||||||
|
return new ShopListQueryState
|
||||||
|
{
|
||||||
|
Query = GetString(q, "q") ?? string.Empty,
|
||||||
|
Sort = GetString(q, "sort"),
|
||||||
|
CategoryId = GetLong(q, "category"),
|
||||||
|
Pages = Math.Max(1, GetInt(q, "pages") ?? 1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToRelativeUrl(string path)
|
||||||
|
{
|
||||||
|
var dict = new Dictionary<string, string?>();
|
||||||
|
if (!string.IsNullOrWhiteSpace(Query))
|
||||||
|
dict["q"] = Query.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(Sort) && !string.Equals(Sort, "price-desc", StringComparison.OrdinalIgnoreCase))
|
||||||
|
dict["sort"] = Sort;
|
||||||
|
if (CategoryId is > 0)
|
||||||
|
dict["category"] = CategoryId.Value.ToString();
|
||||||
|
if (Pages > 1)
|
||||||
|
dict["pages"] = Pages.ToString();
|
||||||
|
|
||||||
|
return dict.Count == 0
|
||||||
|
? path
|
||||||
|
: QueryHelpers.AddQueryString(path, dict!);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Matches(ShopListQueryState other) =>
|
||||||
|
string.Equals(Query?.Trim() ?? "", other.Query?.Trim() ?? "", StringComparison.Ordinal)
|
||||||
|
&& string.Equals(NormalizeSort(Sort), NormalizeSort(other.Sort), StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& CategoryId == other.CategoryId
|
||||||
|
&& Pages == other.Pages;
|
||||||
|
|
||||||
|
public static string NormalizeSort(string? sort) =>
|
||||||
|
string.IsNullOrWhiteSpace(sort) ? "price-desc" : sort.Trim().ToLowerInvariant();
|
||||||
|
|
||||||
|
public static string ToSortKey(ProductSortOption option) => option switch
|
||||||
|
{
|
||||||
|
ProductSortOption.PriceAsc => "price-asc",
|
||||||
|
ProductSortOption.Newest => "newest",
|
||||||
|
ProductSortOption.Title => "title",
|
||||||
|
_ => "price-desc"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static ProductSortOption ParseSortOption(string? sort) => NormalizeSort(sort) switch
|
||||||
|
{
|
||||||
|
"price-asc" => ProductSortOption.PriceAsc,
|
||||||
|
"newest" => ProductSortOption.Newest,
|
||||||
|
"title" => ProductSortOption.Title,
|
||||||
|
_ => ProductSortOption.PriceDesc
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string ToApiSortBy(ProductSortOption option) => option switch
|
||||||
|
{
|
||||||
|
ProductSortOption.PriceAsc => "price asc",
|
||||||
|
ProductSortOption.Newest => "id desc",
|
||||||
|
ProductSortOption.Title => "title asc",
|
||||||
|
_ => "price desc"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string? GetString(Dictionary<string, StringValues> q, string key) =>
|
||||||
|
q.TryGetValue(key, out var v) ? v.FirstOrDefault() : null;
|
||||||
|
|
||||||
|
private static long? GetLong(Dictionary<string, StringValues> q, string key) =>
|
||||||
|
long.TryParse(GetString(q, key), out var n) ? n : null;
|
||||||
|
|
||||||
|
private static int? GetInt(Dictionary<string, StringValues> q, string key) =>
|
||||||
|
int.TryParse(GetString(q, key), out var n) ? n : null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
|
namespace FrontOffice.Main.Utilities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// اسکرول/فوکوس محصول پس از بازگشت از جزئیات — کلیدهای محدود به لیست فروشگاه (نه لندینگ).
|
||||||
|
/// </summary>
|
||||||
|
public static class ShopListScrollRestore
|
||||||
|
{
|
||||||
|
public const string StoreKey = "fo:store:scroll";
|
||||||
|
public const string DiscountKey = "fo:discount:scroll";
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
};
|
||||||
|
|
||||||
|
public static async Task SaveAsync(IJSRuntime js, string storageKey, long productId)
|
||||||
|
{
|
||||||
|
double scrollY = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
scrollY = await js.InvokeAsync<double>("eval", "window.scrollY || window.pageYOffset || 0");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = JsonSerializer.Serialize(new ScrollPayload(productId, scrollY), JsonOptions);
|
||||||
|
await js.InvokeVoidAsync("sessionStorage.setItem", storageKey, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<ScrollPayload?> TakeAsync(IJSRuntime js, string storageKey)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var raw = await js.InvokeAsync<string?>("sessionStorage.getItem", storageKey);
|
||||||
|
await js.InvokeVoidAsync("sessionStorage.removeItem", storageKey);
|
||||||
|
if (string.IsNullOrWhiteSpace(raw))
|
||||||
|
return null;
|
||||||
|
return JsonSerializer.Deserialize<ScrollPayload>(raw, JsonOptions);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task RestoreAsync(IJSRuntime js, ScrollPayload payload)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await js.InvokeVoidAsync("eval",
|
||||||
|
$@"(function(){{
|
||||||
|
var el = document.getElementById('shop-product-{payload.ProductId}');
|
||||||
|
if (el) {{ el.scrollIntoView({{ block: 'center', behavior: 'instant' }}); return; }}
|
||||||
|
window.scrollTo(0, {payload.ScrollY.ToString(System.Globalization.CultureInfo.InvariantCulture)});
|
||||||
|
}})()");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record ScrollPayload(long ProductId, double ScrollY);
|
||||||
|
}
|
||||||
@@ -11,4 +11,5 @@ public class UserAuthInfo
|
|||||||
public bool IsSignMainContract { get; set; }
|
public bool IsSignMainContract { get; set; }
|
||||||
public bool HasPurchasedPackage { get; set; }
|
public bool HasPurchasedPackage { get; set; }
|
||||||
public bool IsClubMemberActive { get; set; }
|
public bool IsClubMemberActive { get; set; }
|
||||||
|
public bool HasAddress { get; set; }
|
||||||
}
|
}
|
||||||
@@ -13,5 +13,9 @@ Disallow: /discount-store/orders
|
|||||||
Disallow: /discount-store/order/
|
Disallow: /discount-store/order/
|
||||||
Disallow: /my-orders
|
Disallow: /my-orders
|
||||||
Disallow: /my-cart
|
Disallow: /my-cart
|
||||||
|
Disallow: /wishlist
|
||||||
|
Disallow: /commission/
|
||||||
|
Disallow: /network/
|
||||||
|
Disallow: /my-packages
|
||||||
|
|
||||||
Sitemap: https://kbs2.ir/sitemap.xml
|
Sitemap: https://kbs2.ir/sitemap.xml
|
||||||
|
|||||||
Reference in New Issue
Block a user