Add OtpDialogService for mobile-friendly OTP authentication dialog

This commit is contained in:
masoodafar-web
2025-11-17 02:53:51 +03:30
parent a0c1452a84
commit 52b8298a18
34 changed files with 1495 additions and 279 deletions

View File

@@ -37,6 +37,11 @@ public static class ConfigureServices
services.AddSingleton<UserAuthInfo>();
services.AddScoped<AuthService>();
services.AddScoped<AuthDialogService>();
// Storefront services
services.AddScoped<CartService>();
services.AddScoped<ProductService>();
services.AddScoped<OrderService>();
services.AddScoped<WalletService>();
// Device detection: very light, dependency-free
services.AddTransient<IDeviceDetector, DeviceDetector>();
// PDF generation (Chromium only)

View File

@@ -13,11 +13,22 @@ public partial class Index
private string? _email;
private bool _isLoadingPackages;
private List<Pack> _packs = new();
[Inject] private AuthService AuthService { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
await LoadPackagesAsync();
if (await AuthService.IsAuthenticatedAsync())
{
if ((await AuthService.IsCompleteRegisterAsync()))
{
Navigation.NavigateTo(RouteConstants.Profile.Index);
}
else
{
Navigation.NavigateTo(RouteConstants.Registration.Wizard);
}
}
//string mobileNumber = "09387342688";

View File

@@ -0,0 +1,73 @@
@attribute [Route(RouteConstants.Profile.Addresses)]
<PageTitle>آدرس‌ها</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">مدیریت آدرس‌ها</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="4">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.subtitle1">آدرس‌های شما</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="OpenAddAddressDialog">افزودن آدرس جدید</MudButton>
</MudStack>
@if (_isLoadingAddresses)
{
<MudStack AlignItems="AlignItems.Center" Class="py-8">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large" />
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">در حال بارگذاری آدرس‌ها...</MudText>
</MudStack>
}
else if (_addresses.Any())
{
<MudStack Spacing="3">
@foreach (var address in _addresses)
{
<MudPaper Elevation="1" Class="pa-4 rounded-xl" Outlined="true">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Start">
<MudStack Spacing="1">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h6">@address.Title</MudText>
@if (address.IsDefault)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Filled" Size="Size.Small">پیش‌فرض</MudChip>
}
</MudStack>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@address.Address</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">کد پستی: @address.PostalCode</MudText>
</MudStack>
<MudMenu Icon="@Icons.Material.Filled.MoreVert" Size="Size.Small" AnchorOrigin="Origin.TopRight">
<MudMenuItem OnClick="@(() => OpenEditAddressDialog(address))">ویرایش</MudMenuItem>
@if (!address.IsDefault)
{
<MudMenuItem OnClick="@(() => SetAsDefaultAddress(address.Id))">تنظیم به عنوان پیش‌فرض</MudMenuItem>
}
<MudDivider />
<MudMenuItem OnClick="@(() => DeleteAddress(address.Id))" Class="mud-text-error">حذف</MudMenuItem>
</MudMenu>
</MudStack>
</MudPaper>
}
</MudStack>
}
else
{
<MudStack AlignItems="AlignItems.Center" Class="py-8">
<MudIcon Icon="@Icons.Material.Filled.LocationOff" Size="Size.Large" Color="Color.Default" />
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">هنوز آدرسی ثبت نکرده‌اید.</MudText>
<MudButton Variant="Variant.Outlined" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" OnClick="OpenAddAddressDialog" Class="mt-2">افزودن اولین آدرس</MudButton>
</MudStack>
}
</MudStack>
</MudPaper>
</MudStack>
</MudContainer>

View File

@@ -0,0 +1,90 @@
using FrontOffice.BFF.UserAddress.Protobuf.Protos.UserAddress;
using FrontOffice.Main.Pages.Profile.Components;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace FrontOffice.Main.Pages.Profile;
public partial class Addresses : ComponentBase
{
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
private List<GetAllUserAddressByFilterResponseModel> _addresses = new();
private bool _isLoadingAddresses;
protected override async Task OnInitializedAsync()
{
await LoadAddresses();
}
private async Task LoadAddresses()
{
_isLoadingAddresses = true;
try
{
var response = await UserAddressContract.GetAllUserAddressByFilterAsync(new());
_addresses = response?.Models?.ToList() ?? new();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری آدرس‌ها: {ex.Message}", Severity.Error);
}
finally
{
_isLoadingAddresses = false;
await InvokeAsync(StateHasChanged);
}
}
private async Task OpenAddAddressDialog()
{
var dialog = await DialogService.ShowAsync<AddAddressDialog>("افزودن آدرس جدید");
var result = await dialog.Result;
if (!result.Canceled)
await LoadAddresses();
}
private async Task OpenEditAddressDialog(GetAllUserAddressByFilterResponseModel address)
{
var dialog = await DialogService.ShowAsync<EditAddressDialog>("ویرایش آدرس", new DialogParameters<EditAddressDialog>
{
{ x => x.Model, address }
});
var result = await dialog.Result;
if (!result.Canceled)
await LoadAddresses();
}
private async Task SetAsDefaultAddress(long id)
{
try
{
await UserAddressContract.SetAddressAsDefaultAsync(new() { Id = id });
Snackbar.Add("آدرس پیش‌فرض تغییر کرد.", Severity.Success);
await LoadAddresses();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در تغییر آدرس پیش‌فرض: {ex.Message}", Severity.Error);
}
}
private async Task DeleteAddress(long id)
{
var confirm = await DialogService.ShowMessageBox("تأیید حذف", "آیا از حذف این آدرس مطمئن هستید؟", yesText: "حذف", cancelText: "لغو");
if (confirm == true)
{
try
{
await UserAddressContract.DeleteUserAddressAsync(new() { Id = id });
Snackbar.Add("آدرس حذف شد.", Severity.Success);
await LoadAddresses();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در حذف آدرس: {ex.Message}", Severity.Error);
}
}
}
}

View File

@@ -62,262 +62,100 @@
</MudPaper>
</MudItem>
<!-- Profile Content -->
<!-- Wallet Highlight Row -->
<MudItem xs="12">
<MudPaper Elevation="4" Class=" ">
<MudTabs Elevation="0" Rounded="true" ApplyEffectsToContainer="true" Class="px-4 pt-4">
<!-- Personal Information Tab -->
<MudTabPanel Text="اطلاعات شخصی" Icon="@Icons.Material.Filled.Person">
<div class="pa-4">
<MudForm @ref="_personalForm" Model="_updateUserRequest" Validation="@(_personalValidator.ValidateValue)">
<MudGrid Spacing="3">
<MudItem xs="12" md="6">
<MudTextField @bind-Value="_updateUserRequest.FirstName"
For="@(() => _updateUserRequest.FirstName)"
Label="نام"
Variant="Variant.Outlined" />
<MudPaper Elevation="3" Class="pa-4 rounded-lg gradient-border">
<MudGrid Spacing="2" AlignItems="AlignItems.Center">
<MudItem xs="12" sm="6" md="4">
<MudStack>
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">موجودی اعتباری</MudText>
<MudText Typo="Typo.h5" Color="Color.Primary">@_walletCredit</MudText>
</MudStack>
</MudItem>
<MudItem xs="12" md="6">
<MudTextField @bind-Value="_updateUserRequest.LastName"
For="@(() => _updateUserRequest.LastName)"
Label="نام خانوادگی"
Variant="Variant.Outlined" />
<MudItem xs="12" sm="6" md="4">
<MudStack>
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">موجودی شبکه</MudText>
<MudText Typo="Typo.h5" Color="Color.Primary">@_walletNetwork</MudText>
</MudStack>
</MudItem>
<MudItem xs="12" md="6">
<MudTextField @bind-Value="_updateUserRequest.NationalCode"
For="@(() => _updateUserRequest.NationalCode)"
Label="کد ملی"
Variant="Variant.Outlined"
InputType="InputType.Text" />
</MudItem>
<MudItem xs="12" md="6">
<MudDatePicker Label="تاریخ تولد"
@bind-Date="_date"
OpenTo="OpenTo.Year"
Variant="Variant.Outlined"
Culture="@Extensions.GetPersianCulture()"
TitleDateFormat="dddd, dd MMMM" />
<MudItem xs="12" md="4" Class="d-flex justify-end">
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.AccountBalanceWallet" Href="@RouteConstants.Profile.Wallet">جزئیات کیف پول</MudButton>
</MudItem>
</MudGrid>
<MudStack Row="true" Spacing="2" Justify="Justify.FlexEnd" Class="mt-4">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SavePersonalInfo" Disabled="_isPersonalSaving">
ذخیره تغییرات
</MudButton>
</MudStack>
</MudForm>
</div>
</MudTabPanel>
<!-- Address Management Tab -->
<MudTabPanel Text="آدرس‌ها" Icon="@Icons.Material.Filled.LocationOn">
<div class="pa-4">
<MudStack Spacing="4">
<!-- Add New Address Button -->
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.subtitle1" >آدرس‌های شما</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="OpenAddAddressDialog">
افزودن آدرس جدید
</MudButton>
</MudStack>
<!-- Address List -->
@if (_isLoadingAddresses)
{
<MudStack AlignItems="AlignItems.Center" Class="py-8">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large" />
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">در حال بارگذاری آدرس‌ها...</MudText>
</MudStack>
}
else if (_addresses.Any())
{
<MudStack Spacing="3">
@foreach (var address in _addresses)
{
<MudPaper Elevation="2" Class="pa-4 rounded-xl " Outlined="true">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Start">
<MudStack Spacing="1">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h6" >@address.Title</MudText>
@if (address.IsDefault)
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Filled" Size="Size.Small">پیش‌فرض</MudChip>
}
</MudStack>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@(address.Address)</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">کد پستی: @(address.PostalCode)</MudText>
</MudStack>
<MudMenu Icon="@Icons.Material.Filled.MoreVert" Size="Size.Small" AnchorOrigin="Origin.TopRight">
<MudMenuItem OnClick="@(() => OpenEditAddressDialog(address))">ویرایش</MudMenuItem>
@if (!address.IsDefault)
{
<MudMenuItem OnClick="@(() => SetAsDefaultAddress(address.Id))">تنظیم به عنوان پیش‌فرض</MudMenuItem>
}
<MudDivider />
<MudMenuItem OnClick="@(() => DeleteAddress(address.Id))" Class="mud-text-error">حذف</MudMenuItem>
</MudMenu>
</MudStack>
</MudStack>
</MudPaper>
}
</MudStack>
}
else
{
<MudStack AlignItems="AlignItems.Center" Class="py-8">
<MudIcon Icon="@Icons.Material.Filled.LocationOff" Size="Size.Large" Color="Color.Default" />
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">هنوز آدرسی ثبت نکرده‌اید.</MudText>
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="OpenAddAddressDialog"
Class="mt-2">
افزودن اولین آدرس
</MudButton>
</MudStack>
}
</MudStack>
</div>
</MudTabPanel>
<!-- Organization Chart Tab -->
<MudTabPanel Text="شجره‌نامه" Icon="@Icons.Material.Filled.AccountTree">
<div class="pa-4">
<OrganizationChart />
</div>
</MudTabPanel>
<!-- Account Settings Tab -->
<MudTabPanel Text="تنظیمات حساب" Icon="@Icons.Material.Filled.Settings">
<div class="pa-4">
<MudStack Spacing="4">
<!-- Notification Settings -->
<MudPaper Outlined="true" Class="pa-4 rounded-lg">
<MudText Typo="Typo.subtitle1" Class="mb-3">اعلان‌ها</MudText>
<MudStack Spacing="2">
<MudSwitch T="bool"
@bind-Value="_updateUserRequest.EmailNotifications"
Color="Color.Primary"
Label="اعلان‌های ایمیلی" />
<MudSwitch T="bool"
@bind-Value="_updateUserRequest.SmsNotifications"
Color="Color.Primary"
Label="اعلان‌های پیامکی" />
<MudSwitch T="bool"
@bind-Value="_updateUserRequest.PushNotifications"
Color="Color.Primary"
Label="اعلان‌های پوش" />
</MudStack>
</MudPaper>
@*
<!-- Privacy Settings -->
<MudPaper Outlined="true" Class="pa-4 rounded-lg">
<MudText Typo="Typo.subtitle1" Class="mb-3">حریم خصوصی</MudText>
<MudStack Spacing="2">
<MudSwitch T="bool" @bind-Checked="_settings.ProfileVisibility" Label="نمایش پروفایل عمومی" />
<MudSwitch T="bool" @bind-Checked="_settings.ShowOnlineStatus" Label="نمایش وضعیت آنلاین" />
</MudStack>
</MudPaper>
<!-- Language & Theme -->
<MudPaper Outlined="true" Class="pa-4 rounded-lg">
<MudText Typo="Typo.subtitle1" Class="mb-3">زبان و تم</MudText>
<MudGrid Spacing="3">
<MudItem xs="12" md="6">
<MudSelect @bind-Value="_settings.Language" Label="زبان" Variant="Variant.Outlined">
<MudSelectItem Value="@("fa")">فارسی</MudSelectItem>
<MudSelectItem Value="@("en")">English</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" md="6">
<MudSelect @bind-Value="_settings.Theme" Label="تم" Variant="Variant.Outlined">
<MudSelectItem Value="@("light")">روشن</MudSelectItem>
<MudSelectItem Value="@("dark")">تیره</MudSelectItem>
<MudSelectItem Value="@("auto")">خودکار</MudSelectItem>
</MudSelect>
<!-- Profile Content as tiles -->
<MudItem xs="12">
<MudPaper Elevation="4" Class="pa-4">
<MudGrid Spacing="3">
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Profile.Personal" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.Person" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.subtitle1" Class="mt-2">اطلاعات شخصی</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">نمایش و ویرایش اطلاعات</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Profile.Addresses" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.LocationOn" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.subtitle1" Class="mt-2">آدرس‌ها</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">مدیریت آدرس‌های شما</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Profile.Tree" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.AccountTree" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.subtitle1" Class="mt-2">شجره‌نامه</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">ساختار شبکه</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Profile.Settings" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.Settings" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.subtitle1" Class="mt-2">تنظیمات حساب</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">اعلان‌ها و تنظیمات</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Store.Products" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.Storefront" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.subtitle1" Class="mt-2">فروشگاه</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">مشاهده و خرید محصولات</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
<MudItem xs="6" sm="6" md="3">
<MudLink Href="@RouteConstants.Profile.Wallet" Underline="Underline.None" Class="tile-link">
<MudCard Elevation="1" Class="rounded-lg profile-tile">
<MudCardContent Class="d-flex flex-column align-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Size="Size.Large" Color="Color.Primary" />
<MudText Typo="Typo.subtitle1" Class="mt-2">کیف پول</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">مدیریت و تاریخچه</MudText>
</MudCardContent>
</MudCard>
</MudLink>
</MudItem>
</MudGrid>
</MudPaper> *@
<MudStack Row="true" Spacing="2" Justify="Justify.FlexEnd">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveSettings" Disabled="_isSettingsSaving">
ذخیره تنظیمات
</MudButton>
</MudStack>
</MudStack>
</div>
</MudTabPanel>
<!-- Statistics Tab -->
@* <MudTabPanel Text="آمار و اطلاعات" Icon="@Icons.Material.Filled.BarChart">
<div class="pa-4">
<MudText Typo="Typo.h6" Class="mb-4">آمار حساب کاربری</MudText>
<MudGrid Spacing="3">
<MudItem xs="12" md="6" lg="3">
<MudPaper Elevation="3" Class="pa-4 text-center rounded-xl ">
<MudIcon Icon="@Icons.Material.Filled.DateRange" Size="Size.Large" Color="Color.Primary" Class="mb-2" />
<MudText Typo="Typo.h4">@_userProfile.JoinDate</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">تاریخ عضویت</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" md="6" lg="3">
<MudPaper Elevation="3" Class="pa-4 text-center rounded-xl ">
<MudIcon Icon="@Icons.Material.Filled.Login" Size="Size.Large" Color="Color.Success" Class="mb-2" />
<MudText Typo="Typo.h4">@_userProfile.LastLogin</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">آخرین ورود</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" md="6" lg="3">
<MudPaper Elevation="3" Class="pa-4 text-center rounded-xl ">
<MudIcon Icon="@Icons.Material.Filled.Group" Size="Size.Large" Color="Color.Info" Class="mb-2" />
<MudText Typo="Typo.h4">@_userProfile.TotalReferrals</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">معرف‌ها</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" md="6" lg="3">
<MudPaper Elevation="3" Class="pa-4 text-center rounded-xl ">
<MudIcon Icon="@Icons.Material.Filled.Star" Size="Size.Large" Color="Color.Warning" Class="mb-2" />
<MudText Typo="Typo.h4">@_userProfile.Level</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">سطح کاربری</MudText>
</MudPaper>
</MudItem>
</MudGrid>
<!-- Account Status -->
<MudPaper Elevation="2" Class="pa-4 mt-4 rounded-xl ">
<MudText Typo="Typo.subtitle1" Class="mb-3">وضعیت حساب</MudText>
<MudStack Spacing="2">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Size="Size.Small" Color="Color.Success" />
<MudText Typo="Typo.body2">حساب تأیید شده</MudText>
</MudStack>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Size="Size.Small" Color="Color.Success" />
<MudText Typo="Typo.body2">ایمیل تأیید شده</MudText>
</MudStack>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Size="Size.Small" Color="Color.Success" />
<MudText Typo="Typo.body2">شماره موبایل تأیید شده</MudText>
</MudStack>
</MudStack>
</MudPaper>
</div>
</MudTabPanel>
*@
</MudTabs>
</MudPaper>
</MudItem>
</MudGrid>

View File

@@ -18,6 +18,7 @@ public partial class Index
{
[Inject] private UserContract.UserContractClient UserContract { get; set; } = default!;
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
[Inject] private WalletService WalletService { get; set; } = default!;
private GetUserResponse _userProfile = new();
private UpdateUserRequest _updateUserRequest = new();
@@ -46,9 +47,22 @@ public partial class Index
{
await LoadUserProfile();
await LoadAddresses();
await LoadWallet();
}
}
private string _walletCredit = "-";
private string _walletNetwork = "-";
private async Task LoadWallet()
{
var b = await WalletService.GetBalancesAsync();
_walletCredit = FormatPrice(b.CreditBalance);
_walletNetwork = FormatPrice(b.NetworkBalance);
StateHasChanged();
}
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
private async Task LoadUserProfile()
{
try

View File

@@ -0,0 +1,52 @@
@attribute [Route(RouteConstants.Profile.Personal)]
<PageTitle>اطلاعات شخصی</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">اطلاعات شخصی</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudForm @ref="_personalForm" Model="_updateUserRequest" Validation="@(_personalValidator.ValidateValue)">
<MudGrid Spacing="3">
<MudItem xs="12" md="6">
<MudTextField @bind-Value="_updateUserRequest.FirstName"
For="@(() => _updateUserRequest.FirstName)"
Label="نام"
Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="12" md="6">
<MudTextField @bind-Value="_updateUserRequest.LastName"
For="@(() => _updateUserRequest.LastName)"
Label="نام خانوادگی"
Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="12" md="6">
<MudTextField @bind-Value="_updateUserRequest.NationalCode"
For="@(() => _updateUserRequest.NationalCode)"
Disabled="@(!string.IsNullOrWhiteSpace(_updateUserRequest.NationalCode))"
Label="کد ملی"
Variant="Variant.Outlined"
InputType="InputType.Text" />
</MudItem>
<MudItem xs="12" md="6">
<MudDatePicker Label="تاریخ تولد"
@bind-Date="_date"
OpenTo="OpenTo.Year"
Variant="Variant.Outlined"
Culture="@Extensions.GetPersianCulture()"
TitleDateFormat="dddd, dd MMMM" />
</MudItem>
</MudGrid>
<MudStack Row="true" Spacing="2" Justify="Justify.FlexEnd" Class="mt-4">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SavePersonalInfo" Disabled="_isPersonalSaving">
ذخیره تغییرات
</MudButton>
</MudStack>
</MudForm>
</MudPaper>
</MudStack>
</MudContainer>

View File

@@ -0,0 +1,73 @@
using FluentValidation;
using FrontOffice.BFF.User.Protobuf.Protos.User;
using FrontOffice.BFF.User.Protobuf.Validator;
using FrontOffice.Main.Utilities;
using Mapster;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using Severity = MudBlazor.Severity;
namespace FrontOffice.Main.Pages.Profile;
public partial class Personal : ComponentBase
{
[Inject] private UserContract.UserContractClient UserContract { get; set; } = default!;
private GetUserResponse _userProfile = new();
private UpdateUserRequest _updateUserRequest = new();
private readonly UpdateUserRequestValidator _personalValidator = new();
private MudForm? _personalForm;
private DateTime? _date;
private bool _isPersonalSaving;
protected override async Task OnInitializedAsync()
{
await LoadUserProfile();
}
private async Task LoadUserProfile()
{
try
{
_userProfile = await UserContract.GetUserAsync(new());
_updateUserRequest = _userProfile.Adapt<UpdateUserRequest>();
if (_userProfile.BirthDate != null)
_date = _userProfile.BirthDate.ToDateTime();
}
catch
{
_userProfile = new();
}
}
private async Task SavePersonalInfo()
{
if (_personalForm is null) return;
await _personalForm.Validate();
if (!_personalForm.IsValid) return;
_isPersonalSaving = true;
try
{
if (!string.IsNullOrWhiteSpace(_updateUserRequest.NationalCode))
_updateUserRequest.NationalCode = _updateUserRequest.NationalCode.PersianToEnglish();
if (_date != null)
_updateUserRequest.BirthDate = _date.Value.DateTimeToTimestamp();
await UserContract.UpdateUserAsync(_updateUserRequest);
Snackbar.Add("اطلاعات شخصی با موفقیت ذخیره شد.", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در ذخیره اطلاعات: {ex.Message}", Severity.Error);
}
finally
{
_isPersonalSaving = false;
await InvokeAsync(StateHasChanged);
}
}
}

View File

@@ -0,0 +1,25 @@
@attribute [Route(RouteConstants.Profile.Settings)]
<PageTitle>تنظیمات حساب</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">تنظیمات حساب</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="4">
<MudText Typo="Typo.subtitle1" Class="mb-1">اعلان‌ها</MudText>
<MudSwitch T="bool" @bind-Value="_request.EmailNotifications" Color="Color.Primary" Label="اعلان‌های ایمیلی" />
<MudSwitch T="bool" @bind-Value="_request.SmsNotifications" Color="Color.Primary" Label="اعلان‌های پیامکی" />
<MudSwitch T="bool" @bind-Value="_request.PushNotifications" Color="Color.Primary" Label="اعلان‌های پوش" />
<MudStack Row="true" Justify="Justify.FlexEnd">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveSettings" Disabled="_saving">ذخیره تنظیمات</MudButton>
</MudStack>
</MudStack>
</MudPaper>
</MudStack>
</MudContainer>

View File

@@ -0,0 +1,46 @@
using FrontOffice.BFF.User.Protobuf.Protos.User;
using Mapster;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace FrontOffice.Main.Pages.Profile;
public partial class Settings : ComponentBase
{
[Inject] private UserContract.UserContractClient UserContract { get; set; } = default!;
private UpdateUserRequest _request = new();
private bool _saving;
protected override async Task OnInitializedAsync()
{
try
{
var user = await UserContract.GetUserAsync(new());
_request = user.Adapt<UpdateUserRequest>();
}
catch
{
_request = new();
}
}
private async Task SaveSettings()
{
_saving = true;
try
{
await UserContract.UpdateUserAsync(_request);
Snackbar.Add("تنظیمات ذخیره شد.", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در ذخیره تنظیمات: {ex.Message}", Severity.Error);
}
finally
{
_saving = false;
}
}
}

View File

@@ -0,0 +1,17 @@
@attribute [Route(RouteConstants.Profile.Tree)]
@using FrontOffice.Main.Pages.Profile.Components
<PageTitle>شجره‌نامه</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">شجره‌نامه</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<OrganizationChart />
</MudPaper>
</MudStack>
</MudContainer>

View File

@@ -0,0 +1,66 @@
@attribute [Route(RouteConstants.Profile.Wallet)]
<PageTitle>کیف پول</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">کیف پول</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Profile.Index">بازگشت</MudButton>
</MudStack>
<MudGrid Spacing="2">
<MudItem xs="12" sm="6">
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">موجودی اعتباری</MudText>
<MudText Typo="Typo.h4" Color="Color.Primary">@_balances.Credit</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6">
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">موجودی شبکه</MudText>
<MudText Typo="Typo.h4" Color="Color.Primary">@_balances.Network</MudText>
</MudPaper>
</MudItem>
</MudGrid>
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-2">تراکنش‌ها و مسیرهای شارژ</MudText>
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudTable Items="_txs" Dense="true">
<HeaderContent>
<MudTh>تاریخ</MudTh>
<MudTh>مبلغ</MudTh>
<MudTh>مسیر</MudTh>
<MudTh>توضیحات</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Date.ToString("yyyy/MM/dd HH:mm")</MudTd>
<MudTd Color="@(context.Amount > 0 ? Color.Success : Color.Error)">@FormatPrice(context.Amount)</MudTd>
<MudTd>@context.Channel</MudTd>
<MudTd>@context.Description</MudTd>
</RowTemplate>
</MudTable>
</MudHidden>
<MudHidden Breakpoint="Breakpoint.MdAndUp">
<MudStack Spacing="2">
@foreach (var tx in _txs)
{
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText>@tx.Date.ToString("yy/MM/dd HH:mm")</MudText>
<MudText Color="@(tx.Amount > 0 ? Color.Success : Color.Error)">@FormatPrice(tx.Amount)</MudText>
</MudStack>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@tx.Channel</MudText>
<MudText Typo="Typo.body2">@tx.Description</MudText>
</MudStack>
</MudPaper>
}
</MudStack>
</MudHidden>
</MudPaper>
</MudStack>
</MudContainer>

View File

@@ -0,0 +1,22 @@
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
namespace FrontOffice.Main.Pages.Profile;
public partial class Wallet : ComponentBase
{
[Inject] private WalletService WalletService { get; set; } = default!;
private (string Credit, string Network) _balances = ("-", "-");
private List<WalletTransaction> _txs = new();
protected override async Task OnInitializedAsync()
{
var b = await WalletService.GetBalancesAsync();
_balances = (FormatPrice(b.CreditBalance), FormatPrice(b.NetworkBalance));
_txs = await WalletService.GetTransactionsAsync();
}
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
}

View File

@@ -0,0 +1,80 @@
@attribute [Route(RouteConstants.Store.Cart)]
@* Injection is handled in code-behind *@
<PageTitle>سبد خرید</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudStack Spacing="3">
<MudText Typo="Typo.h4">سبد خرید</MudText>
@if (CartData.Items.Count == 0)
{
<MudAlert Severity="Severity.Info">سبد خرید شما خالی است.</MudAlert>
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.ArrowBack" OnClick="() => Navigation.NavigateTo(RouteConstants.Store.Products)">بازگشت به محصولات</MudButton>
}
else
{
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudPaper Elevation="1" Class="pa-4 rounded-lg">
<MudTable Items="CartData.Items">
<HeaderContent>
<MudTh>محصول</MudTh>
<MudTh>قیمت واحد</MudTh>
<MudTh>تعداد</MudTh>
<MudTh>قیمت کل</MudTh>
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudAvatar Image="@context.ImageUrl" Size="Size.Medium" />
<MudText>@context.Title</MudText>
</MudStack>
</MudTd>
<MudTd>@FormatPrice(context.UnitPrice)</MudTd>
<MudTd>
<MudNumericField T="int" Value="@context.Quantity" Min="1" Max="50" Immediate="true" HideSpinButtons="true" ValueChanged="(v) => ChangeQty(context.ProductId, v)" Style="max-width:120px" />
</MudTd>
<MudTd>@FormatPrice(context.LineTotal)</MudTd>
<MudTd>
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="() => Remove(context.ProductId)" />
</MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
</MudHidden>
<MudHidden Breakpoint="Breakpoint.MdAndUp">
<MudStack Spacing="2">
@foreach (var item in CartData.Items)
{
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
<MudStack Spacing="1">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudAvatar Image="@item.ImageUrl" />
<MudText Typo="Typo.subtitle2">@item.Title</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Class="mud-text-secondary">@FormatPrice(item.UnitPrice) واحد</MudText>
<MudNumericField T="int" Value="@item.Quantity" Min="1" Max="50" Immediate="true" HideSpinButtons="true" ValueChanged="(v) => ChangeQty(item.ProductId, v)" Class="qty-input" />
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText>جمع: @FormatPrice(item.LineTotal)</MudText>
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="() => Remove(item.ProductId)" />
</MudStack>
</MudStack>
</MudPaper>
}
</MudStack>
</MudHidden>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mobile-actions-stack">
<MudText Typo="Typo.h6">مبلغ قابل پرداخت: <b>@FormatPrice(CartData.Total)</b></MudText>
<MudStack Row="true" Spacing="2" Class="mobile-actions-stack">
<MudButton Variant="Variant.Text" OnClick="() => Navigation.NavigateTo(RouteConstants.Store.Products)">افزودن محصول</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ProceedCheckout" StartIcon="@Icons.Material.Filled.CreditCard">ادامه فرایند خرید</MudButton>
</MudStack>
</MudStack>
}
</MudStack>
</MudContainer>

View File

@@ -0,0 +1,38 @@
using Microsoft.AspNetCore.Components;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.Store;
public partial class Cart : ComponentBase, IDisposable
{
[Inject] private CartService CartService { get; set; } = default!;
// Navigation and Snackbar are available via _Imports.razor
private CartService CartData => CartService;
protected override void OnInitialized()
{
CartService.OnChange += StateHasChanged;
}
private void ChangeQty(long productId, int value)
{
CartService.UpdateQuantity(productId, value);
}
private void Remove(long productId)
{
CartService.Remove(productId);
}
private void ProceedCheckout()
{
Navigation.NavigateTo(RouteConstants.Store.CheckoutSummary);
}
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
public void Dispose()
{
CartService.OnChange -= StateHasChanged;
}
}

View File

@@ -0,0 +1,90 @@
@attribute [Route(RouteConstants.Store.CheckoutSummary)]
<PageTitle>خلاصه خرید</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudGrid Spacing="3">
<MudItem xs="12" md="8">
<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">افزودن آدرس</MudButton>
}
else
{
<MudStack Spacing="1">
@foreach (var address in _addresses)
{
<MudPaper Outlined="@(_selectedAddress?.Id != address.Id)"
Elevation="@(_selectedAddress?.Id == address.Id ? 4 : 0)"
Class="pa-3 rounded-xl cursor-pointer"
Style="@(_selectedAddress?.Id == address.Id ? "border: 2px solid var(--mud-palette-primary);" : "")"
@onclick="() => _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>
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-2">روش پرداخت</MudText>
<MudRadioGroup T="PaymentMethod" @bind-SelectedOption="_payment" Color="Color.Primary">
<MudRadio T="PaymentMethod" Option="@PaymentMethod.Wallet" Color="Color.Primary">کیف پول</MudRadio>
</MudRadioGroup>
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined">در حال حاضر تنها پرداخت از طریق کیف پول فعال است.</MudAlert>
</MudPaper>
</MudItem>
<MudItem xs="12" md="4">
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-2">خلاصه سفارش</MudText>
@if (Cart.Items.Count == 0)
{
<MudAlert Severity="Severity.Info">سبد خرید شما خالی است.</MudAlert>
}
else
{
<MudList T="CartItem" Dense="true">
@foreach (var item in Cart.Items)
{
<MudListItem T="CartItem">
<MudStack Row="true" Justify="Justify.SpaceBetween" Class="w-100">
<MudText>@item.Title x @item.Quantity</MudText>
<MudText>@FormatPrice(item.LineTotal)</MudText>
</MudStack>
</MudListItem>
}
</MudList>
<MudDivider Class="my-2" />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle2">مبلغ قابل پرداخت</MudText>
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatPrice(Cart.Total)</MudText>
</MudStack>
<MudButton Disabled="@(!CanPlaceOrder)" Class="mt-3 w-100-mobile" Variant="Variant.Filled" Color="Color.Primary" OnClick="PlaceOrder" StartIcon="@Icons.Material.Filled.CheckCircle">ثبت سفارش</MudButton>
}
</MudPaper>
</MudItem>
</MudGrid>
</MudContainer>

View File

@@ -0,0 +1,81 @@
using FrontOffice.BFF.UserAddress.Protobuf.Protos.UserAddress;
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace FrontOffice.Main.Pages.Store;
public partial class CheckoutSummary : ComponentBase
{
[Inject] private CartService Cart { get; set; } = default!;
[Inject] private OrderService OrderService { get; set; } = default!;
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
// Snackbar and Navigation are injected via _Imports.razor
private List<GetAllUserAddressByFilterResponseModel> _addresses = new();
private GetAllUserAddressByFilterResponseModel? _selectedAddress;
private bool _loadingAddresses;
private PaymentMethod _payment = PaymentMethod.Wallet;
private bool CanPlaceOrder => Cart.Items.Count > 0 && _selectedAddress != null;
protected override async Task OnInitializedAsync()
{
await LoadAddresses();
}
private async Task LoadAddresses()
{
_loadingAddresses = true;
try
{
var response = await UserAddressContract.GetAllUserAddressByFilterAsync(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;
}
// Simulate wallet payment success and create local order
var order = new Order
{
Status = OrderStatus.Paid,
PaymentMethod = _payment,
AddressId = _selectedAddress.Id,
AddressSummary = _selectedAddress.Address,
Items = Cart.Items.Select(i => new OrderItem(i.ProductId, i.Title, i.ImageUrl, i.UnitPrice, i.Quantity)).ToList()
};
var id = await OrderService.CreateOrderAsync(order);
Cart.Clear();
Snackbar.Add("سفارش با موفقیت ثبت شد.", Severity.Success);
Navigation.NavigateTo($"{RouteConstants.Store.OrderDetail}{id}");
}
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
}

View File

@@ -0,0 +1,79 @@
@attribute [Route(RouteConstants.Store.OrderDetail + "{id:long}")]
@* Injection is handled in code-behind *@
<PageTitle>جزئیات سفارش</PageTitle>
@if (_loading)
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudStack AlignItems="AlignItems.Center">
<MudProgressCircular Indeterminate="true" Color="Color.Primary" />
</MudStack>
</MudContainer>
}
else if (_order is null)
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudAlert Severity="Severity.Warning">سفارش یافت نشد.</MudAlert>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" OnClick="() => Navigation.NavigateTo(RouteConstants.Store.Orders)">بازگشت</MudButton>
</MudContainer>
}
else
{
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="2">
<MudText Typo="Typo.h5">سفارش #@_order.Id</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">تاریخ: @_order.CreatedAt.ToString("yyyy/MM/dd HH:mm")</MudText>
<MudText Typo="Typo.body2">وضعیت: @GetStatusText(_order.Status)</MudText>
<MudText Typo="Typo.body2">روش پرداخت: کیف پول</MudText>
<MudText Typo="Typo.body2">آدرس: @_order.AddressSummary</MudText>
<MudDivider Class="my-2" />
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudTable Items="_order.Items">
<HeaderContent>
<MudTh>محصول</MudTh>
<MudTh>قیمت واحد</MudTh>
<MudTh>تعداد</MudTh>
<MudTh>قیمت کل</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudAvatar Image="@context.ImageUrl" Size="Size.Medium" />
<MudText>@context.Title</MudText>
</MudStack>
</MudTd>
<MudTd>@FormatPrice(context.UnitPrice)</MudTd>
<MudTd>@context.Quantity</MudTd>
<MudTd>@FormatPrice(context.LineTotal)</MudTd>
</RowTemplate>
</MudTable>
</MudHidden>
<MudHidden Breakpoint="Breakpoint.MdAndUp">
<MudStack Spacing="2">
@foreach (var it in _order.Items)
{
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
<MudStack Spacing="1">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudAvatar Image="@it.ImageUrl" />
<MudText>@it.Title</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Class="mud-text-secondary">@FormatPrice(it.UnitPrice) واحد</MudText>
<MudText>تعداد: @it.Quantity</MudText>
</MudStack>
<MudText>جمع: @FormatPrice(it.LineTotal)</MudText>
</MudStack>
</MudPaper>
}
</MudStack>
</MudHidden>
<MudDivider Class="my-2" />
<MudText Typo="Typo.h6" Align="Align.End">مبلغ کل: @FormatPrice(_order.Total)</MudText>
</MudStack>
</MudPaper>
</MudContainer>
}

View File

@@ -0,0 +1,29 @@
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
namespace FrontOffice.Main.Pages.Store;
public partial class OrderDetail : ComponentBase
{
[Inject] private OrderService OrderService { get; set; } = default!;
[Parameter] public long id { get; set; }
private Order? _order;
private bool _loading;
protected override async Task OnParametersSetAsync()
{
_loading = true;
_order = await OrderService.GetOrderAsync(id);
_loading = false;
}
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
private static string GetStatusText(OrderStatus status) => status switch
{
OrderStatus.Paid => "پرداخت‌شده",
_ => "در انتظار",
};
}

View File

@@ -0,0 +1,64 @@
@attribute [Route(RouteConstants.Store.Orders)]
@* Injection is handled in code-behind *@
<PageTitle>سفارشات من</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudText Typo="Typo.h4" Class="mb-3">سفارشات من</MudText>
@if (_loading)
{
<MudStack AlignItems="AlignItems.Center" Class="py-4">
<MudProgressCircular Indeterminate="true" Color="Color.Primary" />
</MudStack>
}
else if (_orders.Count == 0)
{
<MudAlert Severity="Severity.Info">هنوز سفارشی ثبت نکرده‌اید.</MudAlert>
}
else
{
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
<MudTable Items="_orders" Dense="true">
<HeaderContent>
<MudTh>شناسه</MudTh>
<MudTh>تاریخ</MudTh>
<MudTh>وضعیت</MudTh>
<MudTh>مبلغ</MudTh>
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Id</MudTd>
<MudTd>@context.CreatedAt.ToString("yyyy/MM/dd HH:mm")</MudTd>
<MudTd>@GetStatusText(context.Status)</MudTd>
<MudTd>@FormatPrice(context.Total)</MudTd>
<MudTd>
<MudButton Variant="Variant.Text" Href="@($"{RouteConstants.Store.OrderDetail}{context.Id}")" StartIcon="@Icons.Material.Filled.Receipt">جزئیات</MudButton>
</MudTd>
</RowTemplate>
</MudTable>
</MudHidden>
<MudHidden Breakpoint="Breakpoint.MdAndUp">
<MudStack Spacing="2">
@foreach (var o in _orders)
{
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText>سفارش #@o.Id</MudText>
<MudText Class="mud-text-secondary">@o.CreatedAt.ToString("yy/MM/dd HH:mm")</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText>وضعیت: @GetStatusText(o.Status)</MudText>
<MudText Color="Color.Primary">@FormatPrice(o.Total)</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.FlexEnd">
<MudButton Size="Size.Small" Variant="Variant.Outlined" Href="@($"{RouteConstants.Store.OrderDetail}{o.Id}")" StartIcon="@Icons.Material.Filled.Receipt">جزئیات</MudButton>
</MudStack>
</MudStack>
</MudPaper>
}
</MudStack>
</MudHidden>
}
</MudContainer>

View File

@@ -0,0 +1,27 @@
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
namespace FrontOffice.Main.Pages.Store;
public partial class Orders : ComponentBase
{
[Inject] private OrderService OrderService { get; set; } = default!;
private List<Order> _orders = new();
private bool _loading;
protected override async Task OnInitializedAsync()
{
_loading = true;
_orders = await OrderService.GetOrdersAsync();
_loading = false;
}
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
private static string GetStatusText(OrderStatus status) => status switch
{
OrderStatus.Paid => "پرداخت‌شده",
_ => "در انتظار",
};
}

View File

@@ -0,0 +1,45 @@
@attribute [Route(RouteConstants.Store.ProductDetail + "{id:long}")]
@* Injection is handled in code-behind *@
<PageTitle>جزئیات محصول</PageTitle>
@if (_loading)
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudStack AlignItems="AlignItems.Center">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
</MudStack>
</MudContainer>
}
else if (_product is null)
{
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<MudAlert Severity="Severity.Warning">محصول یافت نشد.</MudAlert>
<MudButton Class="mt-2" Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" OnClick="() => Navigation.NavigateTo(RouteConstants.Store.Products)">بازگشت</MudButton>
</MudContainer>
}
else
{
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudGrid Spacing="3">
<MudItem xs="12" md="6">
<MudPaper Class="rounded-lg pa-2">
<MudImage Src="@_product.ImageUrl" Alt="@_product.Title" Style="width:100%" Class="rounded-lg" />
</MudPaper>
</MudItem>
<MudItem xs="12" md="6">
<MudStack Spacing="2">
<MudText Typo="Typo.h4">@_product.Title</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">@_product.Description</MudText>
<MudDivider Class="my-2" />
<MudText Typo="Typo.h5" Color="Color.Primary">@FormatPrice(_product.Price)</MudText>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mobile-actions-stack">
<MudNumericField T="int" @bind-Value="_qty" Min="1" Max="20" Immediate="true" HideSpinButtons="true" Class="qty-input" />
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.AddShoppingCart" OnClick="AddToCart">افزودن به سبد</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" OnClick="() => Navigation.NavigateTo(RouteConstants.Store.Cart)">مشاهده سبد</MudButton>
</MudStack>
</MudStack>
</MudItem>
</MudGrid>
</MudContainer>
}

View File

@@ -0,0 +1,31 @@
using Microsoft.AspNetCore.Components;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.Store;
public partial class ProductDetail : ComponentBase
{
[Inject] private ProductService ProductService { get; set; } = default!;
[Inject] private CartService Cart { get; set; } = default!;
[Parameter] public long id { get; set; }
private Product? _product;
private bool _loading;
private int _qty = 1;
protected override async Task OnParametersSetAsync()
{
_loading = true;
_product = await ProductService.GetByIdAsync(id);
_loading = false;
}
private void AddToCart()
{
if (_product is null) return;
Cart.Add(_product, _qty);
}
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
}

View File

@@ -0,0 +1,61 @@
@attribute [Route(RouteConstants.Store.Products)]
@* Injection is handled in code-behind *@
<PageTitle>محصولات</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-6">
<MudPaper Elevation="1" Class="pa-4 mb-4 rounded-lg">
<MudGrid Spacing="2" AlignItems="AlignItems.Center">
<MudItem xs="12" md="8">
<MudTextField @bind-Value="_query"
Placeholder="جستجو در محصولات..."
AdornmentIcon="@Icons.Material.Filled.Search"
Adornment="Adornment.Start"
Immediate="true"
OnKeyUp="OnQueryChanged"
Class="w-100" />
</MudItem>
<MudItem xs="12" md="4" Class="d-flex justify-end">
<MudButton Class="w-100-mobile" Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.ShoppingCart" Href="@RouteConstants.Store.Cart">
سبد خرید (@Cart.Count)
</MudButton>
</MudItem>
</MudGrid>
</MudPaper>
@if (_loading)
{
<MudStack AlignItems="AlignItems.Center" Class="py-6">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
<MudText Class="mt-2 mud-text-secondary">در حال بارگذاری...</MudText>
</MudStack>
}
else if (_products.Count == 0)
{
<MudAlert Severity="Severity.Info">محصولی یافت نشد.</MudAlert>
}
else
{
<MudGrid Spacing="3">
@foreach (var p in _products)
{
<MudItem xs="12" sm="6" md="4">
<MudCard Class="rounded-lg h-100 d-flex flex-column">
<MudPaper Class="pa-0">
<MudImage Src="@p.ImageUrl" Alt="@p.Title" Style="width:100%;height:160px;object-fit:cover" />
</MudPaper>
<MudCardContent Class="d-flex flex-column gap-2">
<MudText Typo="Typo.h6">@p.Title</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@p.Description</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Primary">@FormatPrice(p.Price)</MudText>
</MudCardContent>
<MudCardActions Class="mt-auto d-flex justify-space-between pa-3">
<MudButton Variant="Variant.Text" Color="Color.Secondary" Href="@($"{RouteConstants.Store.ProductDetail}{p.Id}")">جزئیات</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="() => AddToCart(p)" StartIcon="@Icons.Material.Filled.AddShoppingCart">افزودن</MudButton>
</MudCardActions>
</MudCard>
</MudItem>
}
</MudGrid>
}
</MudContainer>

View File

@@ -0,0 +1,45 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using FrontOffice.Main.Utilities;
namespace FrontOffice.Main.Pages.Store;
public partial class Products : ComponentBase, IDisposable
{
[Inject] private ProductService ProductService { get; set; } = default!;
[Inject] private CartService Cart { get; set; } = default!;
private string _query = string.Empty;
private bool _loading;
private List<Product> _products = new();
protected override async Task OnInitializedAsync()
{
Cart.OnChange += StateHasChanged;
await Load();
}
private async Task Load()
{
_loading = true;
_products = await ProductService.GetProductsAsync(_query);
_loading = false;
}
private async Task OnQueryChanged(KeyboardEventArgs _)
{
await Load();
}
private void AddToCart(Product p)
{
Cart.Add(p, 1);
}
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
public void Dispose()
{
Cart.OnChange -= StateHasChanged;
}
}

View File

@@ -48,7 +48,6 @@ public partial class AuthDialog : IDisposable
[Inject] private ILocalStorageService LocalStorage { get; set; } = default!;
[Inject] private UserContract.UserContractClient UserClient { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!;
[CascadingParameter]
private IMudDialogInstance MudDialog { get; set; }
@@ -66,7 +65,6 @@ public partial class AuthDialog : IDisposable
// {
GenerateCaptcha();
// }
var storedPhone = await LocalStorage.GetItemAsync<string>(PhoneStorageKey);
if (!string.IsNullOrWhiteSpace(storedPhone))
{
@@ -227,22 +225,14 @@ public partial class AuthDialog : IDisposable
_attemptsLeft = MaxVerificationAttempts;
_verifyRequest.Code = string.Empty;
await OnLoginSuccess.InvokeAsync();
if (!InlineMode)
{
MudDialog?.Close();
}
if (await AuthService.IsAuthenticatedAsync())
{
if (!(await AuthService.IsCompleteRegisterAsync()))
{
Navigation.NavigateTo(RouteConstants.Registration.Wizard);
}
else
{
Navigation.NavigateTo(RouteConstants.Profile.Index);
}
}
await OnLoginSuccess.InvokeAsync();
// await OnLoginSuccessAsync();
return true;
}
@@ -270,6 +260,7 @@ public partial class AuthDialog : IDisposable
return false;
}
private async Task HandleVerificationFailureAsync(RpcException rpcEx)
{
switch (rpcEx.Status.StatusCode)

View File

@@ -35,6 +35,12 @@
</div>
<div class="d-none d-md-flex align-center gap-10">
@if (_isAuthenticated)
{
<MudLink Href="@(RouteConstants.Store.Products)" Typo="Typo.subtitle1" Class="mud-link">محصولات</MudLink>
<MudLink Href="@(RouteConstants.Store.Cart)" Typo="Typo.subtitle1" Class="mud-link">سبد خرید</MudLink>
<MudLink Href="@(RouteConstants.Store.Orders)" Typo="Typo.subtitle1" Class="mud-link">سفارشات من</MudLink>
}
<MudLink Href="@(RouteConstants.FAQ.Index)" Typo="Typo.subtitle1" Class="mud-link">سوالات متداول</MudLink>
<MudLink Href="@(RouteConstants.Contact.Index)" Typo="Typo.subtitle1" Class="mud-link">ارتباط با ما</MudLink>
<MudLink Href="@(RouteConstants.About.Index)" Typo="Typo.subtitle1" Class="mud-link">درباره ما</MudLink>
@@ -44,7 +50,7 @@
@if (_isAuthenticated)
{
<MudMenu Icon="@Icons.Material.Filled.Person" Color="Color.Inherit" Size="Size.Medium">
<MudMenuItem OnClick="NavigateToProfile" Disabled="@(AuthService.IsCompleteRegister())">پروفایل</MudMenuItem>
<MudMenuItem OnClick="NavigateToProfile" Disabled="@(!AuthService.IsCompleteRegister())">پروفایل</MudMenuItem>
<MudDivider />
<MudMenuItem OnClick="Logout">خروج از حساب</MudMenuItem>
</MudMenu>
@@ -63,6 +69,12 @@
<!-- Mobile Drawer -->
<MudDrawer @bind-Open="_drawerOpen" Anchor="Anchor.Right" Elevation="1" Variant="DrawerVariant.Temporary" Class="pa-4">
<MudStack Spacing="2">
@if (_isAuthenticated)
{
<MudLink Href="@(RouteConstants.Store.Products)" Typo="Typo.subtitle1" OnClick="() => _drawerOpen=false">محصولات</MudLink>
<MudLink Href="@(RouteConstants.Store.Cart)" Typo="Typo.subtitle1" OnClick="() => _drawerOpen=false">سبد خرید</MudLink>
<MudLink Href="@(RouteConstants.Store.Orders)" Typo="Typo.subtitle1" OnClick="() => _drawerOpen=false">سفارشات من</MudLink>
}
<MudLink Href="@(RouteConstants.FAQ.Index)" Typo="Typo.subtitle1" OnClick="() => _drawerOpen=false">سوالات متداول</MudLink>
<MudLink Href="@(RouteConstants.About.Index)" Typo="Typo.subtitle1" OnClick="() => _drawerOpen=false">درباره ما</MudLink>
<MudLink Href="@(RouteConstants.Contact.Index)" Typo="Typo.subtitle1" OnClick="() => _drawerOpen=false">ارتباط با ما</MudLink>
@@ -87,4 +99,3 @@
</MudLayout>
</MudRTLProvider>

View File

@@ -35,21 +35,21 @@ public class AuthService
public async Task<bool> IsCompleteRegisterAsync()
{
await InitUserAuthInfo();
if (_userAuthInfo.NationalCode ==null || _userAuthInfo.FirstName == null || _userAuthInfo.LastName == null || !_userAuthInfo.IsSignMainContract)
if (!string.IsNullOrWhiteSpace(_userAuthInfo.NationalCode) && !string.IsNullOrWhiteSpace(_userAuthInfo.FirstName) && !string.IsNullOrWhiteSpace(_userAuthInfo.LastName) && _userAuthInfo.IsSignMainContract)
{
return false;
}
return true;
}
return false;
}
public bool IsCompleteRegister()
{
InitUserAuthInfo().GetAwaiter();
if (_userAuthInfo.NationalCode ==null || _userAuthInfo.FirstName == null || _userAuthInfo.LastName == null || _userAuthInfo.IsSignMainContract)
if (!string.IsNullOrWhiteSpace(_userAuthInfo.NationalCode) && !string.IsNullOrWhiteSpace(_userAuthInfo.FirstName) && !string.IsNullOrWhiteSpace(_userAuthInfo.LastName) && _userAuthInfo.IsSignMainContract)
{
return false;
}
return true;
}
return false;
}
public async Task<UserAuthInfo> GetUserAuthInfo()
{
await InitUserAuthInfo();

View File

@@ -0,0 +1,61 @@
namespace FrontOffice.Main.Utilities;
public record CartItem(long ProductId, string Title, string ImageUrl, long UnitPrice, int Quantity)
{
public long LineTotal => UnitPrice * Quantity;
}
public class CartService
{
private readonly List<CartItem> _items = new();
public event Action? OnChange;
public IReadOnlyList<CartItem> Items => _items.AsReadOnly();
public long Total => _items.Sum(i => i.LineTotal);
public int Count => _items.Sum(i => i.Quantity);
public void Add(Product product, int quantity = 1)
{
if (quantity <= 0) return;
var existing = _items.FirstOrDefault(i => i.ProductId == product.Id);
if (existing is null)
{
_items.Add(new CartItem(product.Id, product.Title, product.ImageUrl, product.Price, quantity));
}
else
{
var idx = _items.IndexOf(existing);
_items[idx] = existing with { Quantity = existing.Quantity + quantity };
}
Notify();
}
public void UpdateQuantity(long productId, int quantity)
{
var existing = _items.FirstOrDefault(i => i.ProductId == productId);
if (existing is null) return;
if (quantity <= 0)
{
Remove(productId);
return;
}
var idx = _items.IndexOf(existing);
_items[idx] = existing with { Quantity = quantity };
Notify();
}
public void Remove(long productId)
{
_items.RemoveAll(i => i.ProductId == productId);
Notify();
}
public void Clear()
{
_items.Clear();
Notify();
}
private void Notify() => OnChange?.Invoke();
}

View File

@@ -0,0 +1,47 @@
namespace FrontOffice.Main.Utilities;
public enum PaymentMethod
{
Wallet = 1,
}
public enum OrderStatus
{
Pending = 0,
Paid = 1,
}
public record OrderItem(long ProductId, string Title, string ImageUrl, long UnitPrice, int Quantity)
{
public long LineTotal => UnitPrice * Quantity;
}
public class Order
{
public long Id { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.Now;
public OrderStatus Status { get; set; } = OrderStatus.Pending;
public PaymentMethod PaymentMethod { get; set; } = PaymentMethod.Wallet;
public long AddressId { get; set; }
public string AddressSummary { get; set; } = string.Empty;
public List<OrderItem> Items { get; set; } = new();
public long Total => Items.Sum(i => i.LineTotal);
}
public class OrderService
{
private long _seq = 1000;
private readonly List<Order> _orders = new();
public Task<List<Order>> GetOrdersAsync() => Task.FromResult(_orders.OrderByDescending(o => o.CreatedAt).ToList());
public Task<Order?> GetOrderAsync(long id) => Task.FromResult(_orders.FirstOrDefault(o => o.Id == id));
public Task<long> CreateOrderAsync(Order order)
{
order.Id = Interlocked.Increment(ref _seq);
_orders.Add(order);
return Task.FromResult(order.Id);
}
}

View File

@@ -0,0 +1,44 @@
using System.Collections.Concurrent;
namespace FrontOffice.Main.Utilities;
public record Product(long Id, string Title, string Description, string ImageUrl, long Price);
public class ProductService
{
private static readonly List<Product> _seed = new()
{
new(1, "ماسک پزشکی سه‌لایه", "ماسک سه‌لایه با فیلتراسیون بالا مناسب برای محیط‌های عمومی.", "/images/store/mask.jpg", 49000),
new(2, "دستکش لاتکس", "دستکش لاتکس مناسب معاینات پزشکی، بدون پودر.", "/images/store/gloves.jpg", 89000),
new(3, "محلول ضدعفونی کننده", "محلول ضدعفونی بر پایه الکل ۷۰٪ مناسب دست و سطوح.", "/images/store/sanitizer.jpg", 69000),
new(4, "تب‌سنج دیجیتال", "تب‌سنج دیجیتال با دقت بالا و نمایشگر LCD.", "/images/store/thermometer.jpg", 299000),
new(5, "فشارسنج دیجیتال", "فشارسنج بازویی دیجیتال با حافظه داخلی.", "/images/store/bp-monitor.jpg", 1259000)
};
private readonly ConcurrentDictionary<long, Product> _products = new();
public ProductService()
{
foreach (var p in _seed) _products[p.Id] = p;
}
public Task<List<Product>> GetProductsAsync(string? query = null)
{
IEnumerable<Product> list = _products.Values.OrderBy(p => p.Id);
if (!string.IsNullOrWhiteSpace(query))
{
query = query.Trim();
list = list.Where(p =>
p.Title.Contains(query, StringComparison.OrdinalIgnoreCase) ||
p.Description.Contains(query, StringComparison.OrdinalIgnoreCase));
}
return Task.FromResult(list.ToList());
}
public Task<Product?> GetByIdAsync(long id)
{
_products.TryGetValue(id, out var result);
return Task.FromResult(result);
}
}

View File

@@ -15,6 +15,11 @@ public static class RouteConstants
public static class Profile
{
public const string Index = "/profile";
public const string Personal = "/profile/personal";
public const string Addresses = "/profile/addresses";
public const string Settings = "/profile/settings";
public const string Tree = "/profile/tree";
public const string Wallet = "/profile/wallet";
}
public static class Package
@@ -41,4 +46,14 @@ public static class RouteConstants
{
public const string Index = "/checkout";
}
public static class Store
{
public const string Products = "/products";
public const string ProductDetail = "/product/"; // usage: /product/{id}
public const string Cart = "/cart";
public const string CheckoutSummary = "/checkout-summary";
public const string Orders = "/orders";
public const string OrderDetail = "/order/"; // usage: /order/{id}
}
}

View File

@@ -0,0 +1,20 @@
namespace FrontOffice.Main.Utilities;
public record WalletBalances(long CreditBalance, long NetworkBalance);
public record WalletTransaction(DateTime Date, long Amount, string Channel, string Description);
public class WalletService
{
private WalletBalances _balances = new(350_000, 1_250_000);
private readonly List<WalletTransaction> _transactions = new()
{
new(DateTime.Now.AddDays(-1), 500_000, "درگاه بانکی", "شارژ کیف پول"),
new(DateTime.Now.AddDays(-2), 200_000, "شبکه/معرف", "پاداش شبکه"),
new(DateTime.Now.AddDays(-4), -120_000, "خرید", "برداشت بابت سفارش #1452"),
new(DateTime.Now.AddDays(-9), 900_000, "کیف پول شرکای تجاری", "اعتبار خرید"),
};
public Task<WalletBalances> GetBalancesAsync() => Task.FromResult(_balances);
public Task<List<WalletTransaction>> GetTransactionsAsync() => Task.FromResult(_transactions.OrderByDescending(t => t.Date).ToList());
}

View File

@@ -375,3 +375,28 @@ html, body {
padding: 0 10px !important;
}
}
/*#region Store - Mobile Helpers*/
@media (max-width: 600px) {
.w-100-mobile { width: 100% !important; }
.qty-input { width: 100% !important; max-width: 220px; }
.mobile-actions-stack { width: 100%; display: flex; gap: .5rem; }
.mobile-actions-stack > * { flex: 1; }
}
/*#endregion*/
/*#region Profile Tiles*/
.tile-link { display: block; color: inherit; }
.tile-link:hover { text-decoration: none; }
.profile-tile {
box-shadow: 0 2px 8px rgba(0,0,0,.06);
transition: box-shadow .2s ease, transform .1s ease;
}
.profile-tile:hover {
box-shadow: 0 4px 16px rgba(0,0,0,.08);
transform: translateY(-2px);
}
.mud-theme-dark .profile-tile {
box-shadow: 0 2px 8px rgba(0,0,0,.25);
}
/*#endregion*/