feat: full FrontOffice updates - discount store, blog, payment gateway, UI improvements
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 4m55s
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user