feat: full FrontOffice updates - discount store, blog, payment gateway, UI improvements
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 4m55s

- Discount store pages (products, cart, orders, order detail)
- Blog pages and services
- Payment gateway callback page
- AppImage component, EmptyState, LoadingState, PageHeader
- DiscountCartService, DiscountOrderService, DiscountProductService
- BlogCategoryService, BlogPostService, SitePageService, ImageCacheService
- PhoneVerifyForm component
- Profile Hub page
- UI/UX improvements across all pages
- landing.js for homepage
- Config and routing updates
This commit is contained in:
masoodafar-web
2026-02-16 00:51:39 +03:30
parent f0d1156457
commit 6db83ccd90
104 changed files with 7668 additions and 1370 deletions
@@ -0,0 +1,190 @@
@attribute [Route(RouteConstants.DiscountStore.Cart)]
<PageTitle>سبد خرید تخفیفی</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="سبد خرید تخفیفی" BackHref="@RouteConstants.DiscountStore.Products" />
@if (DiscountCart.Items.Count == 0)
{
<MudAlert Severity="Severity.Info">سبد خرید تخفیفی شما خالی است.</MudAlert>
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.ArrowBack"
OnClick="() => Navigation.NavigateTo(RouteConstants.DiscountStore.Products)">
بازگشت به فروشگاه تخفیفی
</MudButton>
}
else
{
<!-- Desktop Table -->
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudPaper Elevation="1" Class="pa-4 rounded-lg">
<MudTable Items="DiscountCart.Items">
<HeaderContent>
<MudTh>محصول</MudTh>
<MudTh>قیمت واحد</MudTh>
<MudTh>سقف تخفیف</MudTh>
<MudTh>تعداد</MudTh>
<MudTh>تخفیف</MudTh>
<MudTh>قیمت نهایی</MudTh>
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<AppImage Path="@GetImageUrl(context.ImageUrl)" Alt="@context.Title"
ImgWidth="64" ImgHeight="64" Class="product-thumb" />
<MudText>@context.Title</MudText>
</MudStack>
</MudTd>
<MudTd>@FormatPrice(context.UnitPrice)</MudTd>
<MudTd>
<MudChip T="string" Color="Color.Error" Variant="Variant.Outlined" Size="Size.Small">
تا @context.MaxDiscountPercent%
</MudChip>
</MudTd>
<MudTd>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIconButton Icon="@Icons.Material.Filled.Remove" Color="Color.Default"
OnClick="@(() => DecrementQty(context.ProductId))" />
<MudText>@context.Quantity</MudText>
<MudIconButton Icon="@Icons.Material.Filled.Add" Color="Color.Default"
OnClick="@(() => IncrementQty(context.ProductId))" />
</MudStack>
</MudTd>
<MudTd>
@if (context.DiscountAmount > 0)
{
<MudText Color="Color.Success">@FormatPrice(context.DiscountAmount)-</MudText>
}
else
{
<MudText Class="mud-text-secondary">—</MudText>
}
</MudTd>
<MudTd>@FormatPrice(context.FinalPrice)</MudTd>
<MudTd>
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error"
OnClick="() => Remove(context.ProductId)" />
</MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
</MudHidden>
<!-- Mobile Cards -->
<MudHidden Breakpoint="Breakpoint.MdAndUp">
<MudStack Spacing="2">
@foreach (var item in DiscountCart.Items)
{
<MudPaper Class="pa-3 rounded-lg w-100-mobile" Outlined="true">
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<AppImage Path="@GetImageUrl(item.ImageUrl)" Alt="@item.Title"
ImgWidth="50" ImgHeight="50" Class="rounded-circle" />
<MudText Typo="Typo.subtitle2">@item.Title</MudText>
</MudStack>
<MudChip T="string" Color="Color.Error" Variant="Variant.Outlined" Size="Size.Small">
تا @item.MaxDiscountPercent%
</MudChip>
</MudStack>
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Class="mud-text-secondary">@FormatPrice(item.UnitPrice) واحد</MudText>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIconButton
Icon="@(item.Quantity <= 1 ? Icons.Material.Filled.Delete : Icons.Material.Filled.Remove)"
Color="Color.Default"
OnClick="@(() => DecrementQty(item.ProductId))" />
<MudText>@item.Quantity</MudText>
<MudIconButton Icon="@Icons.Material.Filled.Add"
Color="Color.Default"
OnClick="@(() => IncrementQty(item.ProductId))" />
</MudStack>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
@if (item.DiscountAmount > 0)
{
<MudText Typo="Typo.caption" Color="Color.Success">تخفیف: @FormatPrice(item.DiscountAmount)-</MudText>
}
<MudText>جمع: @FormatPrice(item.FinalPrice)</MudText>
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error"
OnClick="() => Remove(item.ProductId)" />
</MudStack>
</MudStack>
</MudStack>
</MudPaper>
}
</MudStack>
</MudHidden>
<!-- Summary -->
@if (DeviceDetector.IsMobile())
{
<MudStack Spacing="1" Class="mobile-actions-stack px-5">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2">جمع کل:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(DiscountCart.TotalPrice) تومان</MudText>
</MudStack>
@if (DiscountCart.TotalDiscount > 0)
{
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.caption" Color="Color.Success">سقف تخفیف قابل اعمال:</MudText>
<MudText Typo="Typo.caption" Color="Color.Success">@FormatPrice(DiscountCart.TotalDiscount)- تومان</MudText>
</MudStack>
}
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h6" Class="fw-bold">حداقل پرداخت:</MudText>
<MudText Typo="Typo.h6" Color="Color.Primary" Class="fw-bold">@FormatPrice(DiscountCart.FinalPrice) تومان</MudText>
</MudStack>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween" Spacing="2" Class="mobile-actions-stack">
<MudButton Variant="Variant.Filled" Color="Color.Primary"
OnClick="() => Navigation.NavigateTo(RouteConstants.DiscountStore.Products)">
افزودن محصول
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Success" OnClick="ProceedCheckout"
StartIcon="@Icons.Material.Filled.CreditCard">ادامه خرید</MudButton>
</MudStack>
}
else
{
<MudPaper Class="pa-4 rounded-lg" Style="background-color: var(--mud-palette-background-grey);">
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">جمع کل:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(DiscountCart.TotalPrice) تومان</MudText>
</MudStack>
@if (DiscountCart.TotalDiscount > 0)
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Color="Color.Success">سقف تخفیف قابل اعمال:</MudText>
<MudText Typo="Typo.body2" Color="Color.Success">@FormatPrice(DiscountCart.TotalDiscount)- تومان</MudText>
</MudStack>
}
<MudDivider Class="my-1" />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1" Class="fw-bold">حداقل مبلغ پرداخت درگاه:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="fw-bold">@FormatPrice(DiscountCart.FinalPrice) تومان</MudText>
</MudStack>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
مبلغ دقیق پرداخت در مرحله تسویه مشخص خواهد شد.
</MudText>
</MudStack>
</MudPaper>
<MudStack Row="true" Justify="Justify.FlexEnd" Spacing="2" Class="mt-3">
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
OnClick="() => Navigation.NavigateTo(RouteConstants.DiscountStore.Products)">
افزودن محصول
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Success" OnClick="ProceedCheckout"
StartIcon="@Icons.Material.Filled.CreditCard">ادامه فرایند خرید</MudButton>
</MudStack>
}
}
</MudStack>
</MudContainer>
@@ -0,0 +1,56 @@
using Microsoft.AspNetCore.Components;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.DiscountStore;
public partial class Cart : IDisposable
{
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
await DiscountCart.EnsureInitializedAsync();
DiscountCart.OnChange += StateHasChanged;
}
private async Task IncrementQty(long productId)
{
var item = DiscountCart.Items.FirstOrDefault(i => i.ProductId == productId);
if (item is null) return;
await DiscountCart.UpdateQuantityAsync(productId, item.Quantity + 1);
}
private async Task DecrementQty(long productId)
{
var item = DiscountCart.Items.FirstOrDefault(i => i.ProductId == productId);
if (item is null) return;
if (item.Quantity <= 1)
{
await DiscountCart.RemoveAsync(productId);
}
else
{
await DiscountCart.UpdateQuantityAsync(productId, item.Quantity - 1);
}
}
private async Task Remove(long productId)
{
await DiscountCart.RemoveAsync(productId);
}
private void ProceedCheckout()
{
Navigation.NavigateTo(RouteConstants.DiscountStore.Checkout);
}
private static string FormatPrice(long price) => $"{price:N0} ";
private static string GetImageUrl(string? imageUrl)
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/');
public void Dispose()
{
DiscountCart.OnChange -= StateHasChanged;
}
}
@@ -0,0 +1,193 @@
@using CMSMicroservice.Protobuf.Protos.UserAddress
@attribute [Route(RouteConstants.DiscountStore.Checkout)]
<PageTitle>تسویه حساب فروشگاه تخفیفی</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<PageHeader Title="تسویه حساب تخفیفی" BackHref="@RouteConstants.DiscountStore.Cart" />
<MudGrid Spacing="3">
<!-- Left Column: Address + Payment Settings -->
<MudItem xs="12" md="8">
<!-- Address Selection -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg mb-3">
<MudText Typo="Typo.h6" Class="mb-2">انتخاب آدرس</MudText>
@if (_loadingAddresses)
{
<MudStack AlignItems="AlignItems.Center" Class="py-4">
<MudProgressCircular Indeterminate="true" Color="Color.Primary" />
<MudText Class="mt-2 mud-text-secondary">در حال بارگذاری آدرس‌ها...</MudText>
</MudStack>
}
else if (_addresses.Count == 0)
{
<MudAlert Severity="Severity.Warning">
هیچ آدرسی ثبت نشده است. لطفاً از بخش پروفایل آدرس خود را اضافه کنید.
</MudAlert>
<MudButton Class="mt-2" Variant="Variant.Outlined" Href="/profile/addresses">افزودن آدرس</MudButton>
}
else
{
<MudStack Spacing="1">
@foreach (var address in _addresses)
{
<MudPaper Outlined="@(_selectedAddress?.Id != address.Id)"
Elevation="@(_selectedAddress?.Id == address.Id ? 2 : 0)"
Class="pa-3 rounded-xl cursor-pointer"
Style="@(_selectedAddress?.Id == address.Id ? "border: 2px solid var(--mud-palette-primary);" : "")"
@onclick="() => _selectedAddress = address">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudStack>
<MudText Typo="Typo.subtitle2">@address.Title</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@address.Address</MudText>
</MudStack>
@if (address.IsDefault)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Outlined">پیش‌فرض</MudChip>
}
</MudStack>
</MudPaper>
}
</MudStack>
}
</MudPaper>
<!-- Discount Balance (Hybrid Payment) -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg mb-3">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center" Class="mb-3">
<MudIcon Icon="@Icons.Material.Filled.Loyalty" Color="Color.Secondary" />
<MudText Typo="Typo.h6">پرداخت از کیف تخفیفی</MudText>
</MudStack>
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="flex-wrap">
<MudText Typo="Typo.body2">موجودی کیف تخفیفی شما:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Secondary" Class="fw-bold">
@FormatPrice(_discountBalance) تومان
</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="flex-wrap">
<MudText Typo="Typo.body2">سقف مجاز تخفیف روی این سفارش:</MudText>
<MudText Typo="Typo.subtitle2" Color="Color.Success">
@FormatPrice(MaxAllowedDiscount) تومان
</MudText>
</MudStack>
<MudDivider />
<MudText Typo="Typo.body2" Class="mud-text-secondary">
مبلغی که از کیف تخفیفی کسر شود (باقیمانده از درگاه پرداخت می‌شود):
</MudText>
<MudSlider T="long" @bind-Value="_discountBalanceToUse"
Min="0" Max="@MaxUsable"
Step="1000"
Color="Color.Secondary"
Immediate="true">
استفاده از @FormatPrice(_discountBalanceToUse) تومان
</MudSlider>
<MudNumericField T="long" @bind-Value="_discountBalanceToUse"
Min="0" Max="@MaxUsable"
Label="مبلغ دلخواه (تومان)" Variant="Variant.Outlined"
Margin="Margin.Dense" Immediate="true" />
@if (_discountBalanceToUse > 0)
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true">
مبلغ @FormatPrice(_discountBalanceToUse) تومان از کیف تخفیفی کسر و
مبلغ @FormatPrice(GatewayAmount) تومان از درگاه پرداخت دریافت خواهد شد.
</MudAlert>
}
</MudStack>
</MudPaper>
<!-- Notes -->
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-2">توضیحات سفارش</MudText>
<MudTextField T="string" @bind-Value="_notes" Lines="3"
Placeholder="توضیحات اختیاری..."
Variant="Variant.Outlined" />
</MudPaper>
</MudItem>
<!-- Right Column: Order Summary -->
<MudItem xs="12" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-lg" Style="position:sticky; top:80px;">
<MudText Typo="Typo.h6" Class="mb-2">خلاصه سفارش</MudText>
@if (DiscountCart.Items.Count == 0)
{
<MudAlert Severity="Severity.Info">سبد خرید شما خالی است.</MudAlert>
}
else
{
<MudList T="string" Dense="true">
@foreach (var item in DiscountCart.Items)
{
<MudListItem T="string">
<MudListItemText>
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<AppImage Path="@GetImageUrl(item.ImageUrl)" Alt="@item.Title"
ImgWidth="40" ImgHeight="40" Class="rounded-circle" />
<MudText Typo="Typo.subtitle2" Style="flex:1; margin:0 8px;">@item.Title</MudText>
<MudText Typo="Typo.subtitle2">@FormatPrice(item.FinalPrice)</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.caption" Class="mud-text-secondary">
@item.Quantity × @FormatPrice(item.UnitPrice)
</MudText>
@if (item.MaxDiscountPercent > 0)
{
<MudText Typo="Typo.caption" Color="Color.Error">تا @item.MaxDiscountPercent% تخفیف</MudText>
}
</MudStack>
</MudStack>
</MudListItemText>
</MudListItem>
}
</MudList>
<MudDivider Class="my-2" />
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">جمع کل:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(DiscountCart.TotalPrice)</MudText>
</MudStack>
@if (_discountBalanceToUse > 0)
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Color="Color.Success">از کیف تخفیفی:</MudText>
<MudText Typo="Typo.body2" Color="Color.Success">@FormatPrice(_discountBalanceToUse)-</MudText>
</MudStack>
}
<MudDivider Class="my-1" />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1" Class="fw-bold">پرداخت از درگاه:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="fw-bold">
@FormatPrice(GatewayAmount) تومان
</MudText>
</MudStack>
<MudText Typo="Typo.caption" Class="mud-text-secondary">
مالیات بر ارزش افزوده صرفاً بر مبلغ درگاه محاسبه خواهد شد.
</MudText>
</MudStack>
<MudButton Disabled="@(!CanPlaceOrder || _placing)" Class="mt-3 w-100-mobile"
Variant="Variant.Filled" Color="Color.Primary"
OnClick="PlaceOrder" FullWidth="true"
StartIcon="@Icons.Material.Filled.CheckCircle">
@(_placing ? "در حال ثبت سفارش..." : "ثبت و پرداخت سفارش")
</MudButton>
}
</MudPaper>
</MudItem>
</MudGrid>
</MudContainer>
@@ -0,0 +1,146 @@
using Microsoft.AspNetCore.Components;
using CMSMicroservice.Protobuf.Protos.UserAddress;
using FrontOffice.Main.Utilities;
using MudBlazor;
namespace FrontOffice.Main.Pages.DiscountStore;
public partial class Checkout
{
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
[Inject] private DiscountOrderService DiscountOrderService { get; set; } = default!;
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
private List<CustomerAddressModel> _addresses = new();
private CustomerAddressModel? _selectedAddress;
private bool _loadingAddresses;
private long _discountBalance;
private long _discountBalanceToUse;
private string? _notes;
private bool _placing;
/// <summary>
/// Maximum discount allowed based on cart items' MaxDiscountPercent
/// </summary>
private long MaxAllowedDiscount => DiscountCart.TotalDiscount;
/// <summary>
/// Maximum the user can actually use = min(balance, allowed discount)
/// </summary>
private long MaxUsable => Math.Min(_discountBalance, MaxAllowedDiscount);
/// <summary>
/// Amount to be charged via payment gateway
/// </summary>
private long GatewayAmount => DiscountCart.TotalPrice - _discountBalanceToUse;
private bool CanPlaceOrder => DiscountCart.Items.Count > 0 && _selectedAddress is not null;
protected override async Task OnInitializedAsync()
{
await DiscountCart.EnsureInitializedAsync();
var addressTask = LoadAddresses();
var balanceTask = LoadDiscountBalance();
await Task.WhenAll(addressTask, balanceTask);
// Default: use maximum possible discount
_discountBalanceToUse = MaxUsable;
}
private async Task LoadDiscountBalance()
{
try
{
var balances = await WalletService.GetBalancesAsync();
_discountBalance = balances.DiscountBalance;
}
catch
{
_discountBalance = 0;
}
}
private async Task LoadAddresses()
{
_loadingAddresses = true;
try
{
var response = await UserAddressContract.GetCustomerAddressesAsync(new());
if (response?.Models?.Any() == true)
{
_addresses = response.Models.ToList();
_selectedAddress = _addresses.FirstOrDefault(a => a.IsDefault) ?? _addresses.First();
}
else
{
_addresses = new();
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری آدرس‌ها: {ex.Message}", Severity.Error);
_addresses = new();
}
finally
{
_loadingAddresses = false;
await InvokeAsync(StateHasChanged);
}
}
private async Task PlaceOrder()
{
if (!CanPlaceOrder || _selectedAddress is null)
{
Snackbar.Add("لطفاً آدرس را انتخاب کنید.", Severity.Warning);
return;
}
_placing = true;
try
{
// Clamp the discount balance to use
var discountToUse = Math.Min(_discountBalanceToUse, MaxUsable);
var result = await DiscountOrderService.PlaceOrderAsync(
_selectedAddress.Id,
discountToUse,
_notes);
if (!result.Success)
{
Snackbar.Add(result.Message, Severity.Error);
return;
}
// If there's a gateway payment URL, redirect to it
if (!string.IsNullOrWhiteSpace(result.PaymentUrl) && result.GatewayAmount > 0)
{
Snackbar.Add("در حال انتقال به درگاه پرداخت...", Severity.Info);
await DiscountCart.ClearAsync();
Navigation.NavigateTo(result.PaymentUrl, forceLoad: true);
return;
}
// If fully paid via discount balance (no gateway needed)
await DiscountCart.ClearAsync();
Snackbar.Add("سفارش با موفقیت ثبت شد!", Severity.Success);
Navigation.NavigateTo($"{RouteConstants.DiscountStore.OrderDetail}{result.OrderId}");
}
catch (Exception ex)
{
Snackbar.Add($"خطا در ثبت سفارش: {ex.Message}", Severity.Error);
}
finally
{
_placing = false;
}
}
private static string FormatPrice(long price) => $"{price:N0}";
private static string GetImageUrl(string? imageUrl)
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl.TrimStart('/');
}
@@ -0,0 +1,198 @@
@attribute [Route("/discount-store/order/{Id:long}")]
<PageTitle>جزئیات سفارش تخفیفی</PageTitle>
@if (!string.IsNullOrEmpty(_paymentMessage))
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="pt-4">
<MudAlert Severity="@_paymentSeverity" Variant="Variant.Filled" Class="mb-2"
CloseIconClicked="() => _paymentMessage = null" ShowCloseIcon="true">
@_paymentMessage
</MudAlert>
</MudContainer>
}
@if (_loading)
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<LoadingState />
</MudContainer>
}
else if (_order is null)
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<EmptyState Icon="@Icons.Material.Filled.SearchOff"
Title="سفارش مورد نظر یافت نشد."
ActionText="بازگشت به لیست سفارش‌ها"
ActionHref="@RouteConstants.DiscountStore.Orders" />
</MudContainer>
}
else
{
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<!-- Header -->
<PageHeader Title="@($"سفارش #{_order.OrderNumber}")" BackHref="@RouteConstants.DiscountStore.Orders" />
<MudStack Row="true" Spacing="2" Class="flex-wrap mb-4">
<MudChip T="string" Color="@(_order.PaymentCompleted ? Color.Success : Color.Warning)"
Variant="Variant.Filled">
@(_order.PaymentCompleted ? "پرداخت شده" : "در انتظار پرداخت")
</MudChip>
<MudChip T="string" Color="@DiscountOrderService.GetDeliveryStatusColor(_order.DeliveryStatus)"
Variant="Variant.Outlined">
@DiscountOrderService.GetDeliveryStatusText(_order.DeliveryStatus)
</MudChip>
</MudStack>
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="2">
<MudText Typo="Typo.caption" Class="mud-text-secondary">
تاریخ ثبت: @FormatDate(_order.Created)
</MudText>
@* ── Address ── *@
@if (_order.Address is not null)
{
<MudText Typo="Typo.body2">
<b>آدرس:</b> @_order.Address.Title — @_order.Address.Address
</MudText>
@if (!string.IsNullOrWhiteSpace(_order.Address.PostalCode))
{
<MudText Typo="Typo.caption" Class="mud-text-secondary">کد پستی: @_order.Address.PostalCode</MudText>
}
}
<MudDivider Class="my-2" />
@* ── Desktop: Table ── *@
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudText Typo="Typo.h6" Class="mb-2">اقلام سفارش</MudText>
<MudTable Items="_order.Items" Dense="true">
<HeaderContent>
<MudTh>محصول</MudTh>
<MudTh>قیمت واحد</MudTh>
<MudTh>تعداد</MudTh>
<MudTh>سقف تخفیف</MudTh>
<MudTh>تخفیف</MudTh>
<MudTh>قیمت نهایی</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Title</MudTd>
<MudTd>@FormatPrice(context.UnitPrice)</MudTd>
<MudTd>@context.Count</MudTd>
<MudTd>
@if (context.MaxDiscountPercent > 0)
{
<MudChip T="string" Size="Size.Small" Color="Color.Error" Variant="Variant.Outlined">@context.MaxDiscountPercent%</MudChip>
}
</MudTd>
<MudTd>
@if (context.DiscountAmount > 0)
{
<MudText Color="Color.Success">@FormatPrice(context.DiscountAmount)-</MudText>
}
else
{
<MudText Class="mud-text-secondary">—</MudText>
}
</MudTd>
<MudTd>@FormatPrice(context.FinalPrice)</MudTd>
</RowTemplate>
</MudTable>
</MudHidden>
@* ── Mobile: Cards ── *@
<MudHidden Breakpoint="Breakpoint.MdAndUp">
<MudText Typo="Typo.h6" Class="mb-2">اقلام سفارش</MudText>
<MudStack Spacing="2">
@foreach (var item in _order.Items)
{
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
<MudStack Spacing="1">
<MudText Typo="Typo.subtitle2">@item.Title</MudText>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">قیمت واحد:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(item.UnitPrice)</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">تعداد:</MudText>
<MudText Typo="Typo.body2">@item.Count</MudText>
</MudStack>
@if (item.DiscountAmount > 0)
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Color="Color.Success">تخفیف (@item.MaxDiscountPercent%):</MudText>
<MudText Typo="Typo.body2" Color="Color.Success">@FormatPrice(item.DiscountAmount)-</MudText>
</MudStack>
}
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle2">قیمت نهایی:</MudText>
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatPrice(item.FinalPrice)</MudText>
</MudStack>
</MudStack>
</MudPaper>
}
</MudStack>
</MudHidden>
<MudDivider Class="my-2" />
@* ── Financial Summary ── *@
<MudStack Spacing="1" Class="pa-3" Style="background-color: var(--mud-palette-background-grey); border-radius: 8px;">
<MudText Typo="Typo.h6" Class="mb-1">خلاصه مالی</MudText>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">جمع کل:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(_order.TotalPrice)</MudText>
</MudStack>
@if (_order.DiscountBalanceUsed > 0)
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Color="Color.Success">از کیف تخفیفی:</MudText>
<MudText Typo="Typo.body2" Color="Color.Success">@FormatPrice(_order.DiscountBalanceUsed)-</MudText>
</MudStack>
}
<MudDivider Class="my-1" />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1" Class="fw-bold">پرداخت درگاه:</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="fw-bold">
@FormatPrice(_order.GatewayAmount)
</MudText>
</MudStack>
</MudStack>
@* ── Tracking / Transaction ── *@
@if (!string.IsNullOrWhiteSpace(_order.TrackingCode))
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">کد رهگیری:</MudText>
<MudText Typo="Typo.body2" Class="fw-semibold">@_order.TrackingCode</MudText>
</MudStack>
}
@if (!string.IsNullOrWhiteSpace(_order.TransactionId))
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.caption" Class="mud-text-secondary">شناسه تراکنش:</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@_order.TransactionId</MudText>
</MudStack>
}
@* ── Notes ── *@
@if (!string.IsNullOrWhiteSpace(_order.Notes))
{
<MudDivider Class="my-1" />
<MudText Typo="Typo.body2" Class="mud-text-secondary">
<b>توضیحات:</b> @_order.Notes
</MudText>
}
@if (!string.IsNullOrWhiteSpace(_order.AdminNotes))
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true">
<b>پیام مدیر:</b> @_order.AdminNotes
</MudAlert>
}
</MudStack>
</MudPaper>
</MudContainer>
}
@@ -0,0 +1,76 @@
using Microsoft.AspNetCore.Components;
using DateTimeConverterCL;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.DiscountStore;
public partial class OrderDetail
{
[Parameter] public long Id { get; set; }
[SupplyParameterFromQuery(Name = "payment")]
public string? PaymentResult { get; set; }
[Inject] private DiscountOrderService DiscountOrderService { get; set; } = default!;
private DiscountOrderDetail? _order;
private bool _loading = true;
private string? _paymentMessage;
private MudBlazor.Severity _paymentSeverity;
protected override async Task OnInitializedAsync()
{
// نمایش پیام نتیجه پرداخت (بعد از بازگشت از درگاه)
if (!string.IsNullOrEmpty(PaymentResult))
{
switch (PaymentResult.ToLower())
{
case "success":
_paymentMessage = "پرداخت با موفقیت انجام شد! سفارش شما ثبت نهایی شد.";
_paymentSeverity = MudBlazor.Severity.Success;
break;
case "failed":
_paymentMessage = "پرداخت ناموفق بود. مبلغ رزرو شده آزاد خواهد شد.";
_paymentSeverity = MudBlazor.Severity.Error;
break;
case "error":
_paymentMessage = "خطایی در پردازش پرداخت رخ داد. لطفاً با پشتیبانی تماس بگیرید.";
_paymentSeverity = MudBlazor.Severity.Warning;
break;
}
}
await LoadOrderAsync();
}
private async Task LoadOrderAsync()
{
_loading = true;
try
{
_order = await DiscountOrderService.GetOrderByIdAsync(Id);
}
catch
{
Snackbar.Add("خطا در بارگذاری سفارش", MudBlazor.Severity.Error);
}
finally
{
_loading = false;
}
}
private static string FormatPrice(long price) => $"{price:N0} تومان";
private static string FormatDate(DateTime date)
{
try
{
return date.ToLocalTime().MiladiToJalaliWithTime();
}
catch
{
return date.ToString("yyyy/MM/dd");
}
}
}
@@ -0,0 +1,130 @@
@attribute [Route(RouteConstants.DiscountStore.Orders)]
<PageTitle>سفارش‌های فروشگاه تخفیفی</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<PageHeader Title="سفارش‌های تخفیفی" BackHref="@RouteConstants.DiscountStore.Products" />
@if (_loading)
{
<LoadingState />
}
else if (_orders.Count == 0)
{
<MudAlert Severity="Severity.Info">هنوز سفارشی ثبت نشده است.</MudAlert>
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Store"
Href="@RouteConstants.DiscountStore.Products">
مشاهده فروشگاه تخفیفی
</MudButton>
}
else
{
<!-- Desktop Table -->
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudPaper Elevation="1" Class="pa-4 rounded-lg">
<MudTable Items="_orders">
<HeaderContent>
<MudTh>شماره سفارش</MudTh>
<MudTh>تعداد اقلام</MudTh>
<MudTh>مبلغ کل</MudTh>
<MudTh>از کیف تخفیفی</MudTh>
<MudTh>درگاه</MudTh>
<MudTh>وضعیت پرداخت</MudTh>
<MudTh>وضعیت ارسال</MudTh>
<MudTh>تاریخ</MudTh>
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.OrderNumber</MudTd>
<MudTd>@context.ItemsCount</MudTd>
<MudTd>@FormatPrice(context.TotalPrice)</MudTd>
<MudTd>
@if (context.DiscountBalanceUsed > 0)
{
<MudText Color="Color.Success">@FormatPrice(context.DiscountBalanceUsed)</MudText>
}
else
{
<MudText Class="mud-text-secondary">—</MudText>
}
</MudTd>
<MudTd>@FormatPrice(context.GatewayAmount)</MudTd>
<MudTd>
<MudChip T="string" Size="Size.Small"
Color="@(context.PaymentCompleted ? Color.Success : Color.Warning)"
Variant="Variant.Filled">
@(context.PaymentCompleted ? "پرداخت شده" : "در انتظار پرداخت")
</MudChip>
</MudTd>
<MudTd>
<MudChip T="string" Size="Size.Small"
Color="@DiscountOrderService.GetDeliveryStatusColor(context.DeliveryStatus)"
Variant="Variant.Outlined">
@DiscountOrderService.GetDeliveryStatusText(context.DeliveryStatus)
</MudChip>
</MudTd>
<MudTd>@FormatDate(context.Created)</MudTd>
<MudTd>
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
OnClick="() => ViewOrder(context.Id)">
جزئیات
</MudButton>
</MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
</MudHidden>
<!-- Mobile Cards -->
<MudHidden Breakpoint="Breakpoint.MdAndUp">
<MudStack Spacing="2">
@foreach (var order in _orders)
{
<MudPaper Class="pa-3 rounded-lg cursor-pointer" Outlined="true"
@onclick="() => ViewOrder(order.Id)">
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.subtitle2">سفارش #@order.OrderNumber</MudText>
<MudChip T="string" Size="Size.Small"
Color="@(order.PaymentCompleted ? Color.Success : Color.Warning)"
Variant="Variant.Filled">
@(order.PaymentCompleted ? "پرداخت شده" : "در انتظار")
</MudChip>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.caption" Class="mud-text-secondary">@FormatDate(order.Created)</MudText>
<MudChip T="string" Size="Size.Small"
Color="@DiscountOrderService.GetDeliveryStatusColor(order.DeliveryStatus)"
Variant="Variant.Outlined">
@DiscountOrderService.GetDeliveryStatusText(order.DeliveryStatus)
</MudChip>
</MudStack>
<MudDivider />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">@order.ItemsCount قلم</MudText>
<MudText Typo="Typo.body2" Class="fw-semibold">@FormatPrice(order.TotalPrice) تومان</MudText>
</MudStack>
@if (order.DiscountBalanceUsed > 0)
{
<MudText Typo="Typo.caption" Color="Color.Success">
از کیف تخفیفی: @FormatPrice(order.DiscountBalanceUsed) تومان
</MudText>
}
</MudStack>
</MudPaper>
}
</MudStack>
</MudHidden>
@if (_totalPages > 1)
{
<MudStack AlignItems="AlignItems.Center" Class="mt-4">
<MudPagination Count="@_totalPages" Selected="@_currentPage"
SelectedChanged="OnPageChanged" Color="Color.Primary"
Variant="Variant.Outlined" />
</MudStack>
}
}
</MudStack>
</MudContainer>
@@ -0,0 +1,65 @@
using Microsoft.AspNetCore.Components;
using DateTimeConverterCL;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.DiscountStore;
public partial class Orders
{
[Inject] private DiscountOrderService DiscountOrderService { get; set; } = default!;
private List<DiscountOrderSummary> _orders = new();
private int _currentPage = 1;
private int _totalPages;
private bool _loading = true;
private const int PageSize = 10;
protected override async Task OnInitializedAsync()
{
await LoadOrdersAsync();
}
private async Task LoadOrdersAsync()
{
_loading = true;
try
{
var result = await DiscountOrderService.GetUserOrdersAsync(_currentPage, PageSize);
_orders = result.Orders;
_totalPages = result.TotalPages;
}
catch
{
Snackbar.Add("خطا در بارگذاری سفارش‌ها", MudBlazor.Severity.Error);
}
finally
{
_loading = false;
}
}
private async Task OnPageChanged(int page)
{
_currentPage = page;
await LoadOrdersAsync();
}
private void ViewOrder(long orderId)
{
Navigation.NavigateTo($"{RouteConstants.DiscountStore.OrderDetail}{orderId}");
}
private static string FormatPrice(long price) => $"{price:N0}";
private static string FormatDate(DateTime date)
{
try
{
return date.ToLocalTime().MiladiToJalaliWithTime();
}
catch
{
return date.ToString("yyyy/MM/dd");
}
}
}
@@ -0,0 +1,140 @@
@attribute [Route("/discount-store/product/{Id:long}")]
<PageTitle>@(_product?.Title ?? "محصول تخفیفی")</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-4 py-md-6">
@if (_loading)
{
<LoadingState />
}
else if (_product is null)
{
<EmptyState Icon="@Icons.Material.Filled.SearchOff"
Title="محصول مورد نظر یافت نشد."
ActionText="بازگشت به فروشگاه تخفیفی"
ActionHref="@RouteConstants.DiscountStore.Products" />
}
else
{
<!-- Breadcrumb -->
<PageHeader Title="جزئیات محصول تخفیفی" BackHref="@RouteConstants.DiscountStore.Products" />
<MudGrid Spacing="4">
<!-- Image Gallery -->
<MudItem xs="12" md="5">
<MudPaper Class="pa-2 rounded-xl" Elevation="1">
<AppImage Path="@_selectedImage" Alt="@_product.Title"
ObjectFit="ObjectFit.Contain"
Style="width:100%; max-height:400px; border-radius:12px;" />
@if (_product.Images.Count > 1)
{
<MudStack Row="true" Spacing="1" Class="mt-2 flex-wrap" Justify="Justify.Center">
@foreach (var img in _product.Images)
{
<span @onclick="() => _selectedImage = img.ImageUrl" style="cursor:pointer">
<AppImage Path="@img.ThumbnailUrl" Alt="@(img.Title ?? "")"
ImgWidth="60" ImgHeight="60"
ObjectFit="ObjectFit.Cover"
Class="rounded-lg"
Style="@($"border: 2px solid {(img.ImageUrl == _selectedImage ? "var(--mud-palette-primary)" : "transparent")};")"
/>
</span>
}
</MudStack>
}
</MudPaper>
</MudItem>
<!-- Product Info -->
<MudItem xs="12" md="7">
<MudStack Spacing="3">
<MudText Typo="Typo.h5" Class="fw-bold">@_product.Title</MudText>
<!-- Categories -->
@if (_product.Categories.Any())
{
<MudStack Row="true" Spacing="1" Class="flex-wrap">
@foreach (var cat in _product.Categories)
{
<MudChip T="string" Color="Color.Info" Variant="Variant.Outlined" Size="Size.Small">@cat.Title</MudChip>
}
</MudStack>
}
<!-- Price & Discount -->
<MudPaper Class="pa-4 rounded-lg discount-price-box" Elevation="0">
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2" Class="mud-text-secondary">قیمت محصول:</MudText>
<MudText Typo="Typo.h5" Color="Color.Primary" Class="fw-bold">
@($"{_product.Price:N0}") تومان
</MudText>
</MudStack>
@if (_product.MaxDiscountPercent > 0)
{
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2" Class="mud-text-secondary">سقف پرداخت از کیف تخفیفی:</MudText>
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">
تا @_product.MaxDiscountPercent% (تا @($"{_product.Price * _product.MaxDiscountPercent / 100:N0}") تومان)
</MudChip>
</MudStack>
}
</MudStack>
</MudPaper>
<!-- Stock & Stats -->
<MudStack Row="true" Spacing="3" Class="flex-wrap">
<MudChip T="string" Variant="Variant.Outlined" Size="Size.Small"
Color="@(_product.RemainingCount > 0 ? Color.Success : Color.Error)"
Icon="@Icons.Material.Filled.Inventory">
@(_product.RemainingCount > 0 ? $"موجود ({_product.RemainingCount} عدد)" : "ناموجود")
</MudChip>
<MudChip T="string" Variant="Variant.Outlined" Size="Size.Small" Color="Color.Default"
Icon="@Icons.Material.Filled.Visibility">
@_product.ViewCount بازدید
</MudChip>
</MudStack>
<!-- Short Info -->
@if (!string.IsNullOrWhiteSpace(_product.ShortInfo))
{
<MudText Typo="Typo.body1" Class="mud-text-secondary">@_product.ShortInfo</MudText>
}
<!-- Add to Cart -->
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center" Class="flex-wrap">
<MudNumericField T="int" @bind-Value="_quantity" Min="1"
Max="@(_product.RemainingCount > 0 ? _product.RemainingCount : 1)"
Label="تعداد" Variant="Variant.Outlined" Margin="Margin.Dense"
Style="width:100px;" />
<MudButton Variant="Variant.Filled" Color="Color.Success"
StartIcon="@Icons.Material.Filled.AddShoppingCart"
OnClick="AddToCart"
Disabled="@(_product.RemainingCount <= 0 || _addingToCart)"
Size="Size.Large">
@(_addingToCart ? "در حال افزودن..." : "افزودن به سبد")
</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.ShoppingCart"
Href="@RouteConstants.DiscountStore.Cart">
مشاهده سبد
</MudButton>
</MudStack>
</MudStack>
</MudItem>
</MudGrid>
<!-- Full Information -->
@if (!string.IsNullOrWhiteSpace(_product.FullInfo))
{
<MudPaper Elevation="1" Class="pa-4 pa-md-6 mt-6 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-3">توضیحات محصول</MudText>
<MudDivider Class="mb-4" />
<div class="blog-content">
@((MarkupString)_product.FullInfo)
</div>
</MudPaper>
}
}
</MudContainer>
@@ -0,0 +1,64 @@
using Microsoft.AspNetCore.Components;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.DiscountStore;
public partial class ProductDetail
{
[Parameter] public long Id { get; set; }
[Inject] private DiscountProductService DiscountProductService { get; set; } = default!;
[Inject] private DiscountCartService DiscountCartService { get; set; } = default!;
private DiscountProductDetail? _product;
private string _selectedImage = string.Empty;
private int _quantity = 1;
private bool _loading = true;
private bool _addingToCart;
protected override async Task OnInitializedAsync()
{
await LoadProductAsync();
}
private async Task LoadProductAsync()
{
_loading = true;
try
{
_product = await DiscountProductService.GetByIdAsync(Id);
if (_product is not null)
{
_selectedImage = _product.Images.FirstOrDefault()?.ImageUrl
?? _product.ThumbnailUrl;
}
}
catch
{
Snackbar.Add("خطا در بارگذاری محصول", MudBlazor.Severity.Error);
}
finally
{
_loading = false;
}
}
private async Task AddToCart()
{
if (_product is null) return;
_addingToCart = true;
try
{
await DiscountCartService.AddAsync(_product.Id, _quantity);
Snackbar.Add($"{_product.Title} به سبد خرید اضافه شد", MudBlazor.Severity.Success);
}
catch
{
Snackbar.Add("خطا در افزودن به سبد خرید", MudBlazor.Severity.Error);
}
finally
{
_addingToCart = false;
}
}
}
@@ -0,0 +1,133 @@
@attribute [Route(RouteConstants.DiscountStore.Products)]
<PageTitle>فروشگاه تخفیفی</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<!-- Hero -->
<MudPaper Class="discount-hero pa-6 pa-md-8 mb-4 rounded-xl" Elevation="0">
<MudStack AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Loyalty" Size="Size.Large" Class="dash-hero-name" />
<MudText Typo="Typo.h5" Align="Align.Center" Class="dash-hero-name">فروشگاه تخفیفی</MudText>
<MudText Typo="Typo.body2" Align="Align.Center" Class="dash-hero-sub">
خرید با استفاده از موجودی کیف پول تخفیفی
</MudText>
</MudStack>
</MudPaper>
<!-- Filters -->
<MudPaper Elevation="1" Class="pa-3 pa-md-4 mb-4 rounded-lg">
<MudGrid Spacing="2" AlignItems="AlignItems.Center">
<MudItem xs="12" sm="5">
<MudTextField @bind-Value="_search"
Placeholder="جستجو در محصولات..."
AdornmentIcon="@Icons.Material.Filled.Search"
Adornment="Adornment.Start"
Immediate="true"
OnKeyUp="OnSearchKeyUp"
Variant="Variant.Outlined"
Margin="Margin.Dense" />
</MudItem>
<MudItem xs="8" sm="5">
<MudSelect T="long?" Value="_selectedCategoryId" ValueChanged="OnCategoryChanged"
Label="دسته‌بندی" Variant="Variant.Outlined" Margin="Margin.Dense"
Clearable="true" AnchorOrigin="Origin.BottomCenter">
@foreach (var cat in _categories)
{
<MudSelectItem T="long?" Value="@((long?)cat.Id)">@cat.Title (@cat.ProductCount)</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="4" sm="2" Class="d-flex gap-2">
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
StartIcon="@Icons.Material.Filled.Search" OnClick="SearchProducts"
Style="height:40px;">جستجو</MudButton>
</MudItem>
<MudItem xs="12" Class="d-flex justify-start justify-md-end gap-2">
<MudButton Size="Size.Small" Variant="Variant.Filled" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.ShoppingCart"
Href="@RouteConstants.DiscountStore.Cart">
سبد خرید
@if (DiscountCart.Count > 0)
{
<MudBadge Content="@DiscountCart.Count" Color="Color.Error" Overlap="true" Class="ms-2" />
}
</MudButton>
</MudItem>
</MudGrid>
</MudPaper>
<!-- Loading -->
@if (_loading)
{
<MudStack AlignItems="AlignItems.Center" Class="py-8">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
<MudText Class="mt-2 mud-text-secondary">در حال بارگذاری...</MudText>
</MudStack>
}
else if (_result.Products.Count == 0)
{
<MudAlert Severity="Severity.Info" Class="my-4">محصولی یافت نشد.</MudAlert>
}
else
{
<!-- Products Grid -->
<MudGrid Spacing="1">
@foreach (var p in _result.Products)
{
<MudItem xs="6" sm="6" md="3"
onclick="@(() => NavigateToProduct(p.Id))">
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden"
Style="cursor:pointer;height: 300px">
<MudCardContent Class="d-flex flex-column pa-1 h-100">
<div style="height: 60%;background-image: url('@(string.IsNullOrWhiteSpace(p.ThumbnailUrl) ? "/images/product-placeholder.svg" : p.ThumbnailUrl)');background-size: cover; background-position: center;border-radius: 0.5rem; position: relative;">
@if (p.RemainingCount <= 0)
{
<div style="position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;border-radius:0.5rem;">
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">ناموجود</MudChip>
</div>
}
else if (p.RemainingCount <= 5)
{
<MudChip T="string" Color="Color.Warning" Variant="Variant.Filled" Size="Size.Small"
Style="position:absolute;top:8px;right:8px;">
فقط @p.RemainingCount عدد
</MudChip>
}
@if (p.MaxDiscountPercent > 0)
{
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small"
Style="position:absolute;top:8px;left:8px;">
تا @p.MaxDiscountPercent% تخفیف
</MudChip>
}
</div>
<div class="pa-1 flex-grow-1 d-flex flex-column justify-space-between">
<MudText Typo="Typo.subtitle1">@p.Title</MudText>
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatPrice(p.Price)</MudText>
</div>
</MudCardContent>
<MudCardActions Class="mt-auto d-flex justify-space-between pa-2">
<MudButton Variant="Variant.Filled" Color="Color.Primary"
OnClick="@(() => AddToCart(p))"
StartIcon="@Icons.Material.Filled.AddShoppingCart"
Disabled="@(p.RemainingCount <= 0)">
@(p.RemainingCount <= 0 ? "ناموجود" : "افزودن")
</MudButton>
</MudCardActions>
</MudCard>
</MudItem>
}
</MudGrid>
<!-- Pagination -->
@if (_result.TotalPages > 1)
{
<div class="d-flex justify-center mt-4">
<MudPagination Count="@_result.TotalPages" Selected="@_currentPage"
SelectedChanged="OnPageChanged"
Color="Color.Primary" Variant="Variant.Filled"
BoundaryCount="1" MiddleCount="3" />
</div>
}
}
</MudContainer>
@@ -0,0 +1,87 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.DiscountStore;
public partial class Products : ComponentBase, IDisposable
{
[Inject] private DiscountProductService ProductService { get; set; } = default!;
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
private string _search = string.Empty;
private long? _selectedCategoryId;
private int _currentPage = 1;
private bool _loading;
private const int PageSize = 12;
private DiscountProductListResult _result = new(new(), 0, 0, 1);
private List<DiscountCategoryNode> _categories = new();
protected override async Task OnInitializedAsync()
{
_loading = true;
await DiscountCart.EnsureInitializedAsync();
DiscountCart.OnChange += StateHasChanged;
var categoriesTask = ProductService.GetCategoriesAsync();
var productsTask = ProductService.GetProductsAsync(page: 1, pageSize: PageSize);
await Task.WhenAll(categoriesTask, productsTask);
_categories = categoriesTask.Result;
_result = productsTask.Result;
_loading = false;
}
private async Task LoadProductsAsync()
{
_loading = true;
StateHasChanged();
_result = await ProductService.GetProductsAsync(
page: _currentPage,
pageSize: PageSize,
search: string.IsNullOrWhiteSpace(_search) ? null : _search,
categoryId: _selectedCategoryId);
_loading = false;
StateHasChanged();
}
private async Task SearchProducts() => await LoadProductsAsync();
private async Task OnSearchKeyUp(KeyboardEventArgs e)
{
if (e.Key == "Enter")
{
_currentPage = 1;
await LoadProductsAsync();
}
}
private async Task OnCategoryChanged(long? value)
{
_selectedCategoryId = value;
_currentPage = 1;
await LoadProductsAsync();
}
private async Task OnPageChanged(int page)
{
_currentPage = page;
await LoadProductsAsync();
}
private async Task AddToCart(DiscountProductCard p)
{
await DiscountCart.AddAsync(p.Id);
}
private void NavigateToProduct(long id)
{
Navigation.NavigateTo($"{RouteConstants.DiscountStore.ProductDetail}{id}");
}
private static string FormatPrice(long price) => $"{price:N0} تومان";
public void Dispose()
{
DiscountCart.OnChange -= StateHasChanged;
}
}