Compare commits
4 Commits
3af564be73
...
025e8d3c3e
| Author | SHA1 | Date | |
|---|---|---|---|
| 025e8d3c3e | |||
| 9ee464b8b4 | |||
| 6b457d0ce6 | |||
| 27c2c0259b |
@@ -18,6 +18,8 @@ using FrontOffice.BFF.ClubMembership.Protobuf.Protos.ClubMembership;
|
||||
using FrontOffice.BFF.Commission.Protobuf.Protos.Commission;
|
||||
using FrontOffice.BFF.NetworkMembership.Protobuf.Protos.NetworkMembership;
|
||||
using FrontOffice.BFF.DiscountShop.Protobuf.Protos.DiscountShop;
|
||||
using FrontOffice.BFF.City.Protobuf;
|
||||
using FrontOffice.BFF.Configuration.Protobuf.Protos.Configuration;
|
||||
using FrontOffice.Main.Utilities;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
@@ -51,10 +53,12 @@ public static class ConfigureServices
|
||||
services.AddScoped<CategoryService>();
|
||||
services.AddScoped<OrderService>();
|
||||
services.AddScoped<WalletService>();
|
||||
services.AddScoped<VATService>(); // Singleton برای cache روزانه
|
||||
// Package service
|
||||
services.AddScoped<PackageService>();
|
||||
// New services for Club, Network, Commission
|
||||
services.AddScoped<ClubMembershipService>();
|
||||
services.AddScoped<ClubConfigurationService>();
|
||||
services.AddScoped<NetworkMembershipService>();
|
||||
services.AddScoped<CommissionService>();
|
||||
// Device detection: very light, dependency-free
|
||||
@@ -62,6 +66,9 @@ public static class ConfigureServices
|
||||
// PDF generation (Chromium only)
|
||||
services.AddSingleton<FrontOffice.Main.Utilities.Pdf.IChromiumPdfService, FrontOffice.Main.Utilities.Pdf.ChromiumPdfService>();
|
||||
|
||||
// SignalR Token Notification Service
|
||||
services.AddScoped<TokenNotificationService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -99,8 +106,10 @@ public static class ConfigureServices
|
||||
// New gRPC clients for Club, Network, Commission, DiscountShop
|
||||
services.AddScoped(CreateAuthenticatedClient<ClubMembershipContract.ClubMembershipContractClient>);
|
||||
services.AddScoped(CreateAuthenticatedClient<CommissionContract.CommissionContractClient>);
|
||||
services.AddScoped(CreateAuthenticatedClient<ConfigurationContract.ConfigurationContractClient>);
|
||||
services.AddScoped(CreateAuthenticatedClient<NetworkMembershipContract.NetworkMembershipContractClient>);
|
||||
services.AddScoped(CreateAuthenticatedClient<DiscountShopContract.DiscountShopContractClient>);
|
||||
services.AddScoped(CreateAuthenticatedClient<CityContract.CityContractClient>);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -10,20 +10,29 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.ClubMembership.Protobuf" Version="0.0.3" />
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.Commission.Protobuf" Version="0.0.2" />
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.DiscountShop.Protobuf" Version="0.0.2" />
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.NetworkMembership.Protobuf" Version="0.0.2" />
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.City.Protobuf" Version="0.0.2" />
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.ClubMembership.Protobuf" Version="0.0.4" />
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.Commission.Protobuf" Version="0.0.3" />
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.Configuration.Protobuf" Version="0.0.3" />
|
||||
<!-- <PackageReference Include="Foursat.FrontOffice.BFF.ClubMembership.Protobuf" Version="0.0.3" /> -->
|
||||
<!-- <PackageReference Include="Foursat.FrontOffice.BFF.Commission.Protobuf" Version="0.0.2" /> -->
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.DiscountShop.Protobuf" Version="0.0.3" />
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.NetworkMembership.Protobuf" Version="0.0.4" />
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.Package.Protobuf" Version="0.0.114" />
|
||||
<!-- <PackageReference Include="Foursat.FrontOffice.BFF.NetworkMembership.Protobuf" Version="0.0.2" />-->
|
||||
<!-- <PackageReference Include="Foursat.FrontOffice.BFF.Package.Protobuf" Version="0.0.113" /> -->
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.Products.Protobuf" Version="0.0.17" />
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.Transaction.Protobuf" Version="0.0.112" />
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.Category.Protobuf" Version="0.0.13" />
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.User.Protobuf" Version="0.0.117" />
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.UserAddress.Protobuf" Version="0.0.115" />
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.UserOrder.Protobuf" Version="0.0.115" />
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.ShopingCart.Protobuf" Version="0.0.16" />
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.UserWallet.Protobuf" Version="0.0.15" />
|
||||
<!-- UserWallet moved to ProjectReference for latest proto -->
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.Products.Protobuf" Version="0.0.18" />
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.Transaction.Protobuf" Version="0.0.113" />
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.Category.Protobuf" Version="0.0.14" />
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.User.Protobuf" Version="0.0.118" />
|
||||
<!-- <PackageReference Include="Foursat.FrontOffice.BFF.User.Protobuf" Version="0.0.117" /> -->
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.UserAddress.Protobuf" Version="0.0.116" />
|
||||
<!-- <PackageReference Include="Foursat.FrontOffice.BFF.UserOrder.Protobuf" Version="0.0.115" />-->
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.ShopingCart.Protobuf" Version="0.0.17" />
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.UserOrder.Protobuf" Version="0.0.116" />
|
||||
<PackageReference Include="Foursat.FrontOffice.BFF.UserWallet.Protobuf" Version="0.0.16" />
|
||||
<!-- UserWallet moved to ProjectReference for latest proto with WeekDefinitionId -->
|
||||
<!-- <PackageReference Include="Foursat.FrontOffice.BFF.UserWallet.Protobuf" Version="0.0.15" /> -->
|
||||
<PackageReference Include="MudBlazor" Version="8.14.0" />
|
||||
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
|
||||
<PackageReference Include="Mapster" Version="7.4.0" />
|
||||
@@ -32,12 +41,21 @@
|
||||
<!-- Keep only PuppeteerSharp for Chromium-based PDF generation -->
|
||||
<PackageReference Include="PuppeteerSharp" Version="11.0.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Local Proto Project Reference for development -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.Package.Protobuf\FrontOffice.BFF.Package.Protobuf.csproj" />
|
||||
</ItemGroup>
|
||||
<!-- <ItemGroup>-->
|
||||
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.Package.Protobuf\FrontOffice.BFF.Package.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.ClubMembership.Protobuf\FrontOffice.BFF.ClubMembership.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.City.Protobuf\FrontOffice.BFF.City.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.Configuration.Protobuf\FrontOffice.BFF.Configuration.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.User.Protobuf\FrontOffice.BFF.User.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.NetworkMembership.Protobuf\FrontOffice.BFF.NetworkMembership.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.UserOrder.Protobuf\FrontOffice.BFF.UserOrder.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.Commission.Protobuf\FrontOffice.BFF.Commission.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.UserWallet.Protobuf\FrontOffice.BFF.UserWallet.Protobuf.csproj" />-->
|
||||
<!-- </ItemGroup>-->
|
||||
|
||||
<!-- New Proto Projects (local references until NuGet publish) -->
|
||||
<!-- <ItemGroup>-->
|
||||
@@ -58,6 +76,12 @@
|
||||
<Content Include="..\.dockerignore">
|
||||
<Link>.dockerignore</Link>
|
||||
</Content>
|
||||
<Content Remove="Pages\Package\Packages.razor" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Pages\Package\Packages.razor.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
@attribute [Route(RouteConstants.Club.Features)]
|
||||
@using FrontOffice.Main.Utilities
|
||||
@using MudBlazor
|
||||
@inject ClubConfigurationService ClubConfigService
|
||||
@inject IDialogService DialogService
|
||||
|
||||
<PageTitle>ویژگیهای باشگاه مشتریان</PageTitle>
|
||||
|
||||
@@ -11,142 +13,181 @@
|
||||
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack" Href="@RouteConstants.Club.Membership">بازگشت</MudButton>
|
||||
</MudStack>
|
||||
|
||||
<!-- توضیحات کلی باشگاه -->
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
||||
<MudText Typo="Typo.h6" Class="mb-3">چرا باشگاه مشتریان؟</MudText>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mb-3">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Stars" Color="Color.Primary" Size="Size.Large" />
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary">ویژگیهای اختصاصی شما</MudText>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.body1" Class="mb-2">
|
||||
با عضویت در باشگاه مشتریان فورست، از تخفیفهای ویژه، امتیازات خرید و خدمات اختصاصی بهرهمند شوید.
|
||||
علاوه بر مزایای پایه عضویت در باشگاه مشتریان، شما به مجموعهای از ویژگیهای اختصاصی دسترسی دارید. برخی از این ویژگیها ممکن است نیاز به زمان برای فعالسازی داشته باشند.
|
||||
با کلیک روی دکمه «جزئیات» هر ویژگی، میتوانید اطلاعات بیشتری درباره نحوه استفاده از آن امکان را مشاهده کنید.
|
||||
</MudText>
|
||||
</MudPaper>
|
||||
|
||||
<MudGrid Spacing="3">
|
||||
<MudItem xs="12" md="6">
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg" Style="height: 100%;">
|
||||
<MudStack Spacing="2">
|
||||
<!-- لیست فیچرها -->
|
||||
<MudText Typo="Typo.h6" Class="mt-4">ویژگیهای شما</MudText>
|
||||
|
||||
@if (_isLoading)
|
||||
{
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
|
||||
}
|
||||
else if (_features == null || !_features.Any())
|
||||
{
|
||||
<MudAlert Severity="Severity.Info">
|
||||
هنوز ویژگیای برای شما فعال نشده است.
|
||||
</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper Elevation="2" Class="pa-2 rounded-lg">
|
||||
<MudList T="ClubFeatureDto" Dense="true">
|
||||
@foreach (var feature in _features)
|
||||
{
|
||||
<MudListItem>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Style="width: 100%;">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Discount" Color="Color.Primary" Size="Size.Large" />
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary">تخفیفهای ویژه</MudText>
|
||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle"
|
||||
Color="@(feature.IsEnabled ? Color.Success : Color.Default)" />
|
||||
<MudText>@feature.Title</MudText>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.body2">
|
||||
• تخفیف 10% تا 30% برای تمام محصولات<br />
|
||||
• تخفیفهای فصلی و مناسبتی اختصاصی<br />
|
||||
• پیشنهادات ویژه برای اعضای باشگاه<br />
|
||||
• کد تخفیف اختصاصی ماهانه
|
||||
</MudText>
|
||||
<MudButton Variant="Variant.Text"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
OnClick="@(() => ShowFeatureDetails(feature))">
|
||||
جزئیات
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudListItem>
|
||||
@if (feature != _features.Last())
|
||||
{
|
||||
<MudDivider />
|
||||
}
|
||||
}
|
||||
</MudList>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" md="6">
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg" Style="height: 100%;">
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Diamond" Color="Color.Secondary" Size="Size.Large" />
|
||||
<MudText Typo="Typo.h6" Color="Color.Secondary">امتیاز خرید</MudText>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.body2">
|
||||
• دریافت امتیاز برای هر خرید<br />
|
||||
• تبدیل امتیاز به تخفیف باشگاه<br />
|
||||
• امتیاز ویژه در روزهای خاص<br />
|
||||
• قابلیت انتقال امتیاز به دوستان
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" md="6">
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg" Style="height: 100%;">
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.LocalShipping" Color="Color.Success" Size="Size.Large" />
|
||||
<MudText Typo="Typo.h6" Color="Color.Success">ارسال رایگان</MudText>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.body2">
|
||||
• ارسال رایگان برای خریدهای بالای 500 هزار تومان<br />
|
||||
• اولویت در ارسال سفارشات<br />
|
||||
• ارسال اکسپرس با تخفیف 50%<br />
|
||||
• امکان ارسال به چند آدرس
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" md="6">
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg" Style="height: 100%;">
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Support" Color="Color.Info" Size="Size.Large" />
|
||||
<MudText Typo="Typo.h6" Color="Color.Info">پشتیبانی اختصاصی</MudText>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.body2">
|
||||
• پشتیبانی 24/7 برای اعضای باشگاه<br />
|
||||
• مشاوره خرید تخصصی<br />
|
||||
• خط ویژه پاسخگویی<br />
|
||||
• پیگیری سریعتر سفارشات
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" md="6">
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg" Style="height: 100%;">
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Groups" Color="Color.Warning" Size="Size.Large" />
|
||||
<MudText Typo="Typo.h6" Color="Color.Warning">درآمد شبکه</MudText>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.body2">
|
||||
• دریافت کمیسیون از خریدهای زیرمجموعه<br />
|
||||
• سیستم باینری درختی<br />
|
||||
• محاسبه خودکار درآمد هفتگی<br />
|
||||
• امکان برداشت درآمد شبکه
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" md="6">
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg" Style="height: 100%;">
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Event" Color="Color.Tertiary" Size="Size.Large" />
|
||||
<MudText Typo="Typo.h6" Color="Color.Tertiary">رویدادهای ویژه</MudText>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.body2">
|
||||
• دعوت به رویدادهای اختصاصی<br />
|
||||
• پیشفروش محصولات جدید<br />
|
||||
• وبینارهای آموزشی رایگان<br />
|
||||
• جوایز و مسابقات ماهانه
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
||||
<MudText Typo="Typo.h6" Class="mb-3">چگونه عضو شویم؟</MudText>
|
||||
<MudStepper Orientation="Orientation.Horizontal" Color="Color.Primary">
|
||||
<MudStep Title="انتخاب بسته">
|
||||
<MudText>بسته عضویت مناسب خود را انتخاب کنید</MudText>
|
||||
</MudStep>
|
||||
<MudStep Title="پرداخت">
|
||||
<MudText>هزینه عضویت را پرداخت کنید</MudText>
|
||||
</MudStep>
|
||||
<MudStep Title="فعالسازی">
|
||||
<MudText>عضویت شما فورا فعال میشود</MudText>
|
||||
</MudStep>
|
||||
<MudStep Title="استفاده">
|
||||
<MudText>از مزایای باشگاه لذت ببرید</MudText>
|
||||
</MudStep>
|
||||
</MudStepper>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
FullWidth="true"
|
||||
Size="Size.Large"
|
||||
StartIcon="@Icons.Material.Filled.CheckCircle"
|
||||
StartIcon="@Icons.Material.Filled.ArrowBack"
|
||||
Href="@RouteConstants.Club.Membership">
|
||||
همین حالا عضو شوید
|
||||
بازگشت به صفحه باشگاه
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudContainer>
|
||||
|
||||
<!-- مدال جزئیات فیچر -->
|
||||
<MudDialog @bind-Visible="_showDetailDialog" Options="@_dialogOptions">
|
||||
<TitleContent>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Star" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.h6">@_selectedFeature?.Title</MudText>
|
||||
</MudStack>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
@if (_selectedFeature != null)
|
||||
{
|
||||
<MudStack Spacing="3">
|
||||
<!-- وضعیت -->
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudText Typo="Typo.subtitle2">وضعیت:</MudText>
|
||||
@if (_selectedFeature.IsEnabled)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Success" Size="Size.Small">فعال</MudChip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChip T="string" Color="Color.Default" Size="Size.Small">غیرفعال</MudChip>
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
<!-- توضیحات کوتاه -->
|
||||
@if (!string.IsNullOrEmpty(_selectedFeature.Description))
|
||||
{
|
||||
<MudText Typo="Typo.body1">@_selectedFeature.Description</MudText>
|
||||
}
|
||||
|
||||
<!-- تاریخها -->
|
||||
<MudDivider />
|
||||
<MudGrid>
|
||||
<MudItem xs="6">
|
||||
<MudText Typo="Typo.caption" Color="Color.Default">تاریخ ایجاد:</MudText>
|
||||
<MudText Typo="Typo.body2">
|
||||
@(_selectedFeature.CreatedAt?.ToLocalTime().ToString("yyyy/MM/dd HH:mm") ?? "-")
|
||||
</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="6">
|
||||
<MudText Typo="Typo.caption" Color="Color.Default">تاریخ فعالسازی:</MudText>
|
||||
<MudText Typo="Typo.body2">
|
||||
@(_selectedFeature.GrantedAt.HasValue && _selectedFeature.GrantedAt.Value > DateTime.MinValue
|
||||
? _selectedFeature.GrantedAt.Value.ToLocalTime().ToString("yyyy/MM/dd HH:mm")
|
||||
: "-")
|
||||
</MudText>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<!-- یادداشت (Notes) - محتوای اصلی مدال -->
|
||||
@if (!string.IsNullOrEmpty(_selectedFeature.Notes))
|
||||
{
|
||||
<MudDivider />
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2">توضیحات تکمیلی:</MudText>
|
||||
<MudPaper Elevation="0" Class="pa-3 rounded" Style="background-color: var(--mud-palette-background-grey);">
|
||||
@((MarkupString)_selectedFeature.Notes)
|
||||
</MudPaper>
|
||||
}
|
||||
</MudStack>
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(() => _showDetailDialog = false)">
|
||||
بستن
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
private List<ClubFeatureDto>? _features;
|
||||
private bool _isLoading = true;
|
||||
private bool _showDetailDialog;
|
||||
private ClubFeatureDto? _selectedFeature;
|
||||
|
||||
private DialogOptions _dialogOptions = new()
|
||||
{
|
||||
MaxWidth = MaxWidth.Small,
|
||||
FullWidth = true,
|
||||
CloseButton = true,
|
||||
CloseOnEscapeKey = true
|
||||
};
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadFeatures();
|
||||
}
|
||||
|
||||
private async Task LoadFeatures()
|
||||
{
|
||||
_isLoading = true;
|
||||
try
|
||||
{
|
||||
_features = await ClubConfigService.GetClubFeaturesAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error loading features: {ex.Message}");
|
||||
_features = new List<ClubFeatureDto>();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowFeatureDetails(ClubFeatureDto feature)
|
||||
{
|
||||
_selectedFeature = feature;
|
||||
_showDetailDialog = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,19 @@
|
||||
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined">
|
||||
<MudText>شما هنوز عضو باشگاه مشتریان نیستید. برای استفاده از مزایای ویژه، اکنون فعال کنید!</MudText>
|
||||
</MudAlert>
|
||||
|
||||
@if (_clubConfig != null)
|
||||
{
|
||||
<MudPaper Elevation="0" Class="pa-3 mt-3 rounded-lg" Style="background-color: var(--mud-palette-info-lighten);">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.subtitle2" Color="Color.Info">هزینه عضویت در باشگاه مشتریان</MudText>
|
||||
<MudText Typo="Typo.caption">شامل @((_clubConfig.MembershipGiftValue / 10000).ToString("N0")) تومان هدیه</MudText>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.h6" Color="Color.Info">@((_clubConfig.ActivationFee / 10000).ToString("N0")) تومان</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
}
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
@@ -63,41 +76,31 @@
|
||||
<!-- مزایای باشگاه -->
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
||||
<MudText Typo="Typo.h6" Class="mb-3">مزایای عضویت باشگاه</MudText>
|
||||
<MudGrid Spacing="2">
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudPaper Elevation="0" Class="pa-3 rounded-lg" Style="background-color: var(--mud-palette-primary-lighten);">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Discount" Color="Color.Primary" Size="Size.Large" />
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="pa-2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Color="Color.Success" Size="Size.Large" />
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.subtitle1" Color="Color.Primary">تخفیف ویژه</MudText>
|
||||
<MudText Typo="Typo.caption">تا 30% تخفیف</MudText>
|
||||
<MudText Typo="Typo.subtitle1">شارژ کیف پول فروشگاه تخفیفی</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Default">شارژ ۵۶ میلیون تومان کیف پول فروشگاه تخفیفی</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudPaper Elevation="0" Class="pa-3 rounded-lg" Style="background-color: var(--mud-palette-secondary-lighten);">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Diamond" Color="Color.Secondary" Size="Size.Large" />
|
||||
<MudDivider />
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="pa-2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.TrendingUp" Color="Color.Primary" Size="Size.Large" />
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.subtitle1" Color="Color.Secondary">امتیاز خرید</MudText>
|
||||
<MudText Typo="Typo.caption">برای هر خرید</MudText>
|
||||
<MudText Typo="Typo.subtitle1">عضویت در شبکه بازاریابی</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Default">عضویت در شبکه بازاریابی و دریافت پورسانت</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudPaper Elevation="0" Class="pa-3 rounded-lg" Style="background-color: var(--mud-palette-success-lighten);">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.LocalShipping" Color="Color.Success" Size="Size.Large" />
|
||||
<MudDivider />
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="pa-2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.GroupAdd" Color="Color.Secondary" Size="Size.Large" />
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.subtitle1" Color="Color.Success">ارسال رایگان</MudText>
|
||||
<MudText Typo="Typo.caption">برای سفارشات بالای 500 هزار تومان</MudText>
|
||||
<MudText Typo="Typo.subtitle1">جذب زیرمجموعه</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Default">امکان جذب زیرمجموعه و گسترش شبکه</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
||||
<!-- فعالسازی یا تمدید عضویت -->
|
||||
@@ -120,7 +123,7 @@
|
||||
<MudAlert Severity="Severity.Error" Variant="Variant.Filled">
|
||||
خطا در دریافت اطلاعات عضویت. لطفا دوباره تلاش کنید.
|
||||
</MudAlert>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="LoadMembershipAsync">تلاش مجدد</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="LoadDataAsync">تلاش مجدد</MudButton>
|
||||
}
|
||||
</MudStack>
|
||||
</MudContainer>
|
||||
|
||||
@@ -7,23 +7,33 @@ namespace FrontOffice.Main.Pages.Club;
|
||||
public partial class MembershipPage : ComponentBase
|
||||
{
|
||||
[Inject] private ClubMembershipService ClubService { get; set; } = default!;
|
||||
[Inject] private ClubConfigurationService ConfigService { get; set; } = default!;
|
||||
|
||||
private ClubMembershipDto? _membership;
|
||||
private ClubConfigDto? _clubConfig;
|
||||
private bool _isLoading = true;
|
||||
private bool _hasError = false;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadMembershipAsync();
|
||||
await LoadDataAsync();
|
||||
}
|
||||
|
||||
private async Task LoadMembershipAsync()
|
||||
private async Task LoadDataAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_isLoading = true;
|
||||
_hasError = false;
|
||||
_membership = await ClubService.GetMyMembershipAsync();
|
||||
|
||||
// بارگذاری همزمان عضویت و تنظیمات
|
||||
var membershipTask = ClubService.GetMyMembershipAsync();
|
||||
var configTask = ConfigService.GetClubConfigurationAsync();
|
||||
|
||||
await Task.WhenAll(membershipTask, configTask);
|
||||
|
||||
_membership = await membershipTask;
|
||||
_clubConfig = await configTask;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -38,7 +48,7 @@ public partial class MembershipPage : ComponentBase
|
||||
|
||||
private async Task HandleActivationSuccess()
|
||||
{
|
||||
await LoadMembershipAsync();
|
||||
await LoadDataAsync();
|
||||
Snackbar.Add("عضویت باشگاه با موفقیت فعال شد!", Severity.Success);
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>
|
||||
<MudChip T="string" Color="Color.Default" Size="Size.Small">@context.WeekLabel</MudChip>
|
||||
<MudChip T="string" Color="Color.Default" Size="Size.Small">@context.WeekDisplayName</MudChip>
|
||||
</MudTd>
|
||||
<MudTd>@context.BalancesEarned</MudTd>
|
||||
<MudTd>
|
||||
@@ -75,7 +75,7 @@
|
||||
Variant="Variant.Text"
|
||||
Color="Color.Info"
|
||||
StartIcon="@Icons.Material.Filled.Info"
|
||||
Href="@($"{RouteConstants.Commission.WeeklyBalance}?week={context.WeekNumber}")">
|
||||
Href="@($"{RouteConstants.Commission.WeeklyBalance}?week={context.WeekDefinitionId}")">
|
||||
جزئیات
|
||||
</MudButton>
|
||||
</MudTd>
|
||||
@@ -90,7 +90,7 @@
|
||||
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
|
||||
<MudStack Spacing="1">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudChip T="string" Color="Color.Default" Size="Size.Small">@payout.WeekLabel</MudChip>
|
||||
<MudChip T="string" Color="Color.Default" Size="Size.Small">@payout.WeekDisplayName</MudChip>
|
||||
<MudChip T="string" Color="@GetStatusColor(payout.StatusBadgeColor)" Size="Size.Small" Variant="Variant.Outlined">
|
||||
@payout.Status
|
||||
</MudChip>
|
||||
@@ -103,7 +103,7 @@
|
||||
Color="Color.Info"
|
||||
FullWidth="true"
|
||||
StartIcon="@Icons.Material.Filled.Info"
|
||||
Href="@($"{RouteConstants.Commission.WeeklyBalance}?week={payout.WeekNumber}")">
|
||||
Href="@($"{RouteConstants.Commission.WeeklyBalance}?week={payout.WeekDefinitionId}")">
|
||||
مشاهده جزئیات هفته
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="هفته">
|
||||
<MudChip T="string" Color="Color.Primary" Size="Size.Small" Variant="Variant.Outlined">@context.WeekLabel</MudChip>
|
||||
<MudChip T="string" Color="Color.Primary" Size="Size.Small" Variant="Variant.Outlined">@context.WeekDisplayName</MudChip>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="تعادلها">
|
||||
<MudText>@context.BalancesEarned</MudText>
|
||||
@@ -73,7 +73,7 @@
|
||||
Variant="Variant.Text"
|
||||
Color="Color.Info"
|
||||
StartIcon="@Icons.Material.Filled.Visibility"
|
||||
Href="@($"{RouteConstants.Commission.WeeklyBalance}?week={context.WeekNumber}")">
|
||||
Href="@($"{RouteConstants.Commission.WeeklyBalance}?week={context.WeekDefinitionId}")">
|
||||
مشاهده
|
||||
</MudButton>
|
||||
</MudTd>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@attribute [Route(RouteConstants.Commission.WeeklyBalance)]
|
||||
@using FrontOffice.Main.Utilities
|
||||
@using FrontOffice.Main.Shared
|
||||
@using MudBlazor
|
||||
|
||||
<PageTitle>تعادل هفتگی</PageTitle>
|
||||
@@ -13,20 +14,38 @@
|
||||
|
||||
<!-- انتخاب هفته -->
|
||||
<MudPaper Elevation="2" Class="pa-3 rounded-lg">
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.End">
|
||||
<MudNumericField @bind-Value="_selectedWeekNumber"
|
||||
Label="شماره هفته"
|
||||
<MudGrid Spacing="2">
|
||||
<MudItem xs="8" sm="6" md="6">
|
||||
<WeekSelector @ref="_weekSelector"
|
||||
@bind-Value="_selectedWeekDefinition"
|
||||
Label="انتخاب هفته"
|
||||
Placeholder="هفته را جستجو یا انتخاب کنید"
|
||||
Variant="Variant.Outlined"
|
||||
Min="1"
|
||||
HelperText="هفته مورد نظر را انتخاب کنید (خالی = هفته جاری)"
|
||||
Style="max-width: 200px;" />
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="LoadWeeklyBalanceAsync" StartIcon="@Icons.Material.Filled.Search">
|
||||
مشاهده
|
||||
Dense="false"
|
||||
OnlyActive="true" />
|
||||
</MudItem>
|
||||
@* <MudItem xs="4" sm="3" md="3"> *@
|
||||
@* <MudButton Variant="Variant.Outlined" *@
|
||||
@* OnClick="LoadCurrentWeekAsync" *@
|
||||
@* StartIcon="@Icons.Material.Filled.Today" *@
|
||||
@* FullWidth="true" *@
|
||||
@* Style="height: 56px;"> *@
|
||||
@* هفته جاری *@
|
||||
@* </MudButton> *@
|
||||
@* </MudItem> *@
|
||||
<MudItem xs="4" sm="3" md="3" Class="pt-6">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
OnClick="LoadWeeklyBalanceAsync"
|
||||
StartIcon="@Icons.Material.Filled.Search"
|
||||
Disabled="@(_selectedWeekDefinition == null)"
|
||||
FullWidth="true"
|
||||
Style="height: 50px;">
|
||||
جستجو
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" OnClick="LoadCurrentWeekAsync" StartIcon="@Icons.Material.Filled.Today">
|
||||
هفته جاری
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
||||
@if (_isLoading)
|
||||
@@ -35,113 +54,89 @@
|
||||
}
|
||||
else if (_weeklyBalance is not null)
|
||||
{
|
||||
<!-- اطلاعات هفته -->
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
||||
<MudStack Spacing="2">
|
||||
<MudText Typo="Typo.h6">هفته @_weeklyBalance.WeekNumber - @_weeklyBalance.WeekLabel</MudText>
|
||||
<MudDivider />
|
||||
<MudGrid Spacing="2">
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">تاریخ شروع:</MudText>
|
||||
<MudText Typo="Typo.body1"><strong>@_weeklyBalance.StartDatePersian</strong></MudText>
|
||||
<!-- اطلاعات هفته - کامپکت -->
|
||||
<MudPaper Elevation="2" Class="pa-3 rounded-lg">
|
||||
<MudGrid Spacing="1">
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudIcon Icon="@Icons.Material.Filled.DateRange" Size="Size.Small" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.body2"><strong>@_weeklyBalance.WeekDisplayName</strong></MudText>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">تاریخ پایان:</MudText>
|
||||
<MudText Typo="Typo.body1"><strong>@_weeklyBalance.EndDatePersian</strong></MudText>
|
||||
<MudItem xs="6" sm="4">
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">شروع: <strong>@_weeklyBalance.StartDatePersian</strong></MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="6" sm="4">
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">پایان: <strong>@_weeklyBalance.EndDatePersian</strong></MudText>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<!-- تعادل چپ و راست -->
|
||||
<!-- تعادل چپ و راست - کارتهای بزرگ -->
|
||||
<MudGrid Spacing="2">
|
||||
<MudItem xs="12" md="6">
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg" Style="height: 100%;">
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudItem xs="6">
|
||||
<MudPaper Elevation="3" Class="pa-4 rounded-lg text-center" Style="background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);">
|
||||
<MudStack Spacing="1" AlignItems="AlignItems.Center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Color="Color.Info" Size="Size.Large" />
|
||||
<MudText Typo="Typo.h6" Color="Color.Info">شاخه چپ</MudText>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.h4" Color="Color.Info">@_weeklyBalance.LeftBalanceFormatted</MudText>
|
||||
<MudText Typo="Typo.subtitle2" Color="Color.Info">شاخه چپ</MudText>
|
||||
<MudText Typo="Typo.h5" Color="Color.Info" Style="font-weight: bold;">@_weeklyBalance.LeftBalanceFormatted</MudText>
|
||||
<MudProgressLinear Color="Color.Info"
|
||||
Value="@GetLeftPercentage()"
|
||||
Size="Size.Large"
|
||||
Class="rounded-lg" />
|
||||
Size="Size.Medium"
|
||||
Class="rounded-lg mt-2"
|
||||
Style="width: 100%;" />
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
||||
@GetLeftPercentage().ToString("F1")% از کل تعادل
|
||||
@GetLeftPercentage().ToString("F1")% از کل
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" md="6">
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg" Style="height: 100%;">
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudItem xs="6">
|
||||
<MudPaper Elevation="3" Class="pa-4 rounded-lg text-center" Style="background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);">
|
||||
<MudStack Spacing="1" AlignItems="AlignItems.Center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ChevronRight" Color="Color.Success" Size="Size.Large" />
|
||||
<MudText Typo="Typo.h6" Color="Color.Success">شاخه راست</MudText>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.h4" Color="Color.Success">@_weeklyBalance.RightBalanceFormatted</MudText>
|
||||
<MudText Typo="Typo.subtitle2" Color="Color.Success">شاخه راست</MudText>
|
||||
<MudText Typo="Typo.h5" Color="Color.Success" Style="font-weight: bold;">@_weeklyBalance.RightBalanceFormatted</MudText>
|
||||
<MudProgressLinear Color="Color.Success"
|
||||
Value="@GetRightPercentage()"
|
||||
Size="Size.Large"
|
||||
Class="rounded-lg" />
|
||||
Size="Size.Medium"
|
||||
Class="rounded-lg mt-2"
|
||||
Style="width: 100%;" />
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
||||
@GetRightPercentage().ToString("F1")% از کل تعادل
|
||||
@GetRightPercentage().ToString("F1")% از کل
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<!-- محاسبات کمیسیون -->
|
||||
<!-- محاسبات کمیسیون - فشردهتر -->
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
||||
<MudText Typo="Typo.h6" Class="mb-3">محاسبات کمیسیون</MudText>
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.body1">تعادل چپ:</MudText>
|
||||
<MudText Typo="Typo.body1" Color="Color.Info"><strong>@_weeklyBalance.LeftBalanceFormatted</strong></MudText>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.body1">تعادل راست:</MudText>
|
||||
<MudText Typo="Typo.body1" Color="Color.Success"><strong>@_weeklyBalance.RightBalanceFormatted</strong></MudText>
|
||||
</MudStack>
|
||||
<MudDivider />
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.body1">حداقل تعادل (پایه کمیسیون):</MudText>
|
||||
<MudText Typo="Typo.h6" Color="Color.Warning"><strong>@_weeklyBalance.MinBalanceFormatted</strong></MudText>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.body1">تعداد تعادل (دفعات):</MudText>
|
||||
<MudText Typo="Typo.h6" Color="Color.Secondary"><strong>@_weeklyBalance.BalanceCount</strong></MudText>
|
||||
</MudStack>
|
||||
<MudDivider />
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.h6">کمیسیون محاسبه شده:</MudText>
|
||||
<MudText Typo="Typo.h5" Color="Color.Primary"><strong>@_weeklyBalance.CalculatedCommissionFormatted</strong></MudText>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.subtitle1" Class="mb-2" Style="font-weight: bold;">محاسبات کمیسیون</MudText>
|
||||
<MudSimpleTable Dense="true" Hover="true" Style="background: transparent;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><MudText Typo="Typo.body2">حداقل تعادل (پایه کمیسیون):</MudText></td>
|
||||
<td class="text-end"><MudText Typo="Typo.body1" Color="Color.Warning"><strong>@_weeklyBalance.MinBalanceFormatted</strong></MudText></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><MudText Typo="Typo.body2">تعداد تعادل (دفعات):</MudText></td>
|
||||
<td class="text-end"><MudText Typo="Typo.body1" Color="Color.Secondary"><strong>@_weeklyBalance.BalanceCount</strong></MudText></td>
|
||||
</tr>
|
||||
<tr style="background-color: var(--mud-palette-primary-lighten);">
|
||||
<td><MudText Typo="Typo.subtitle1"><strong>کمیسیون محاسبه شده:</strong></MudText></td>
|
||||
<td class="text-end"><MudText Typo="Typo.h6" Color="Color.Primary"><strong>@_weeklyBalance.CalculatedCommissionFormatted</strong></MudText></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
|
||||
@if (_weeklyBalance.LeftCarryover > 0 || _weeklyBalance.RightCarryover > 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined">
|
||||
<MudText Typo="Typo.body2">
|
||||
<strong>Carryover (انتقال به هفته بعد):</strong><br />
|
||||
چپ: @_weeklyBalance.LeftCarryoverFormatted | راست: @_weeklyBalance.RightCarryoverFormatted
|
||||
</MudText>
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mt-2">
|
||||
<strong>انتقال به هفته بعد:</strong> چپ: @_weeklyBalance.LeftCarryoverFormatted | راست: @_weeklyBalance.RightCarryoverFormatted
|
||||
</MudAlert>
|
||||
}
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<!-- نمودار مقایسه -->
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
||||
<MudText Typo="Typo.h6" Class="mb-3">نمودار مقایسه</MudText>
|
||||
<MudChart ChartType="ChartType.Bar"
|
||||
Width="100%"
|
||||
Height="300px"
|
||||
InputData="@(new double[] { _weeklyBalance.LeftBalance, _weeklyBalance.RightBalance, _weeklyBalance.MinBalance })"
|
||||
InputLabels="@(new[] { "چپ", "راست", "حداقل" })"
|
||||
ChartOptions="@_chartOptions" />
|
||||
</MudPaper>
|
||||
}
|
||||
else if (_hasError)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using FrontOffice.Main.Utilities;
|
||||
using FrontOffice.Main.Shared;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
@@ -7,13 +8,15 @@ namespace FrontOffice.Main.Pages.Commission;
|
||||
public partial class WeeklyBalancePage : ComponentBase
|
||||
{
|
||||
[Inject] private CommissionService CommissionService { get; set; } = default!;
|
||||
[Inject] private ISnackbar SnackbarService { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
[SupplyParameterFromQuery(Name = "week")]
|
||||
public int? QueryWeekNumber { get; set; }
|
||||
public string? QueryWeekNumber { get; set; }
|
||||
|
||||
private WeekSelector? _weekSelector;
|
||||
private WeekDefinitionDto? _selectedWeekDefinition;
|
||||
private WeeklyBalanceDto? _weeklyBalance;
|
||||
private int? _selectedWeekNumber;
|
||||
private bool _isLoading = true;
|
||||
private bool _hasError = false;
|
||||
|
||||
@@ -27,13 +30,45 @@ public partial class WeeklyBalancePage : ComponentBase
|
||||
}
|
||||
};
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (QueryWeekNumber.HasValue)
|
||||
_selectedWeekNumber = QueryWeekNumber;
|
||||
if (firstRender)
|
||||
{
|
||||
// Wait for WeekSelector to initialize and load its cached weeks
|
||||
if (_weekSelector != null)
|
||||
{
|
||||
await _weekSelector.EnsureLoadedAsync();
|
||||
|
||||
// Handle query parameter ?week=46 (WeekOrder) or ?week=123 (Id)
|
||||
if (!string.IsNullOrEmpty(QueryWeekNumber))
|
||||
{
|
||||
// Try to parse as integer (first try as WeekOrder, then as Id)
|
||||
if (int.TryParse(QueryWeekNumber, out int weekOrder))
|
||||
{
|
||||
_selectedWeekDefinition = _weekSelector.FindByWeekOrder(weekOrder);
|
||||
if (_selectedWeekDefinition == null && long.TryParse(QueryWeekNumber, out long id))
|
||||
{
|
||||
_selectedWeekDefinition = _weekSelector.FindById(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no week selected and no query param, select current week
|
||||
if (_selectedWeekDefinition == null)
|
||||
{
|
||||
_selectedWeekDefinition = _weekSelector.GetCurrentWeek();
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
await LoadWeeklyBalanceAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_isLoading = true;
|
||||
}
|
||||
|
||||
private async Task LoadWeeklyBalanceAsync()
|
||||
{
|
||||
@@ -41,12 +76,27 @@ public partial class WeeklyBalancePage : ComponentBase
|
||||
{
|
||||
_isLoading = true;
|
||||
_hasError = false;
|
||||
_weeklyBalance = await CommissionService.GetMyWeeklyBalanceAsync(_selectedWeekNumber);
|
||||
|
||||
long? weekDefinitionId = _selectedWeekDefinition?.Id;
|
||||
|
||||
_weeklyBalance = await CommissionService.GetMyWeeklyBalanceAsync(weekDefinitionId);
|
||||
|
||||
// Update week display name from selected definition if available
|
||||
if (_selectedWeekDefinition != null && _weeklyBalance != null)
|
||||
{
|
||||
_weeklyBalance.WeekDisplayName = _selectedWeekDefinition.DisplayName;
|
||||
return;
|
||||
}
|
||||
|
||||
_weeklyBalance = new WeeklyBalanceDto()
|
||||
{
|
||||
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_hasError = true;
|
||||
Snackbar.Add($"خطا در دریافت تعادل: {ex.Message}", Severity.Error);
|
||||
SnackbarService.Add($"خطا در دریافت تعادل: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -56,7 +106,11 @@ public partial class WeeklyBalancePage : ComponentBase
|
||||
|
||||
private async Task LoadCurrentWeekAsync()
|
||||
{
|
||||
_selectedWeekNumber = null;
|
||||
if (_weekSelector != null)
|
||||
{
|
||||
await _weekSelector.SelectCurrentWeekAsync();
|
||||
}
|
||||
_selectedWeekDefinition = _weekSelector?.GetCurrentWeek();
|
||||
await LoadWeeklyBalanceAsync();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ public partial class Index:IDisposable
|
||||
{
|
||||
await InvokeAsync(StateHasChanged);
|
||||
};
|
||||
await LoadPackagesAsync();
|
||||
// await LoadPackagesAsync();
|
||||
|
||||
//string mobileNumber = "09387342688";
|
||||
|
||||
@@ -50,37 +50,37 @@ public partial class Index:IDisposable
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
private async Task LoadPackagesAsync()
|
||||
{
|
||||
_isLoadingPackages = true;
|
||||
try
|
||||
{
|
||||
var response = await PackageClient.GetAllPackageByFilterAsync(request: new());
|
||||
if (response?.Models?.Any() == true)
|
||||
{
|
||||
_packs = response.Models.Select(p => new Pack(
|
||||
Id: p.Id,
|
||||
Title: p.Title,
|
||||
Body: p.Description,
|
||||
Image: UrlUtility.DownloadUrl + p.ImagePath,
|
||||
Price: p.Price
|
||||
)).ToList();
|
||||
}
|
||||
else
|
||||
_packs = new List<Pack>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری پکیجها: {ex.Message}", Severity.Error);
|
||||
// Fallback to empty list
|
||||
_packs = new List<Pack>();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoadingPackages = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
// private async Task LoadPackagesAsync()
|
||||
// {
|
||||
// _isLoadingPackages = true;
|
||||
// try
|
||||
// {
|
||||
// var response = await PackageClient.GetAllPackageByFilterAsync(request: new());
|
||||
// if (response?.Models?.Any() == true)
|
||||
// {
|
||||
// _packs = response.Models.Select(p => new Pack(
|
||||
// Id: p.Id,
|
||||
// Title: p.Title,
|
||||
// Body: p.Description,
|
||||
// Image: UrlUtility.DownloadUrl + p.ImagePath,
|
||||
// Price: p.Price
|
||||
// )).ToList();
|
||||
// }
|
||||
// else
|
||||
// _packs = new List<Pack>();
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Snackbar.Add($"خطا در بارگذاری پکیجها: {ex.Message}", Severity.Error);
|
||||
// // Fallback to empty list
|
||||
// _packs = new List<Pack>();
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// _isLoadingPackages = false;
|
||||
// await InvokeAsync(StateHasChanged);
|
||||
// }
|
||||
// }
|
||||
|
||||
private void JoinWaitlist()
|
||||
{
|
||||
|
||||
@@ -40,7 +40,7 @@ public partial class Addresses : ComponentBase
|
||||
{
|
||||
var dialog = await DialogService.ShowAsync<AddAddressDialog>("افزودن آدرس جدید");
|
||||
var result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
if (result is not null && !result.Canceled)
|
||||
await LoadAddresses();
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ public partial class Addresses : ComponentBase
|
||||
{ x => x.Model, address }
|
||||
});
|
||||
var result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
if (result is not null && !result.Canceled)
|
||||
await LoadAddresses();
|
||||
}
|
||||
|
||||
|
||||
@@ -29,13 +29,34 @@
|
||||
Required="true"
|
||||
RequiredError="کد پستی الزامی است." />
|
||||
|
||||
<MudTextField @bind-Value="_request.CityId"
|
||||
For="@(() => _request.CityId)"
|
||||
Label="شناسه شهر"
|
||||
<MudAutocomplete T="CityProto.GetAllCitiesByFilterResponseModel"
|
||||
@bind-Value="_selectedCity"
|
||||
Label="شهر"
|
||||
Variant="Variant.Outlined"
|
||||
InputType="InputType.Number"
|
||||
SearchFunc="SearchCities"
|
||||
ToStringFunc="@(city => city != null ? $"{city.Native} ({city.StateName})" : string.Empty)"
|
||||
Required="true"
|
||||
RequiredError="شهر الزامی است." />
|
||||
RequiredError="شهر الزامی است."
|
||||
Clearable="true"
|
||||
ResetValueOnEmptyText="true"
|
||||
CoerceText="false"
|
||||
CoerceValue="false"
|
||||
Dense="true"
|
||||
DebounceInterval="300"
|
||||
MaxItems="20"
|
||||
MinCharacters="2"
|
||||
ProgressIndicatorColor="Color.Primary">
|
||||
<ItemTemplate Context="city">
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.LocationCity" Size="Size.Small" />
|
||||
<MudText>@city.Native</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">(@city.StateName)</MudText>
|
||||
</MudStack>
|
||||
</ItemTemplate>
|
||||
<NoItemsTemplate>
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary pa-2">شهری یافت نشد</MudText>
|
||||
</NoItemsTemplate>
|
||||
</MudAutocomplete>
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
|
||||
@@ -3,26 +3,57 @@ using FrontOffice.BFF.UserAddress.Protobuf.Validator;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
using Severity = MudBlazor.Severity;
|
||||
using CityProto = FrontOffice.BFF.City.Protobuf;
|
||||
|
||||
namespace FrontOffice.Main.Pages.Profile.Components;
|
||||
|
||||
public partial class AddAddressDialog : ComponentBase
|
||||
{
|
||||
[CascadingParameter] private IDialogReference MudDialog { get; set; } = default!;
|
||||
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
||||
[Inject] private CityProto.CityContract.CityContractClient CityContract { get; set; } = default!;
|
||||
|
||||
private MudForm? _form;
|
||||
private readonly CreateNewUserAddressRequestValidator _validator = new();
|
||||
private bool _isSaving;
|
||||
private CreateNewUserAddressRequest _request = new();
|
||||
private CityProto.GetAllCitiesByFilterResponseModel? _selectedCity;
|
||||
|
||||
private async Task<IEnumerable<CityProto.GetAllCitiesByFilterResponseModel>> SearchCities(string value,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value) || value.Length < 2)
|
||||
return Enumerable.Empty<CityProto.GetAllCitiesByFilterResponseModel>();
|
||||
|
||||
try
|
||||
{
|
||||
var response = await CityContract.GetAllCitiesByFilterAsync(new CityProto.GetAllCitiesByFilterRequest
|
||||
{
|
||||
PaginationState = new CityProto.PaginationState { PageNumber = 1, PageSize = 20 },
|
||||
Filter = new CityProto.GetAllCitiesByFilterFilter { Name = value }
|
||||
}, cancellationToken: ct);
|
||||
|
||||
return response?.Models?.ToList() ?? new List<CityProto.GetAllCitiesByFilterResponseModel>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Enumerable.Empty<CityProto.GetAllCitiesByFilterResponseModel>();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveAddress()
|
||||
{
|
||||
if (_form == null) return;
|
||||
|
||||
await _form.Validate();
|
||||
if (!_form.IsValid) return;
|
||||
if (!_form.IsValid || _selectedCity == null)
|
||||
{
|
||||
if (_selectedCity == null)
|
||||
Snackbar.Add("لطفاً شهر را انتخاب کنید.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
_request.CityId = _selectedCity.Id;
|
||||
_isSaving = true;
|
||||
try
|
||||
{
|
||||
@@ -41,5 +72,6 @@ public partial class AddAddressDialog : ComponentBase
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void Cancel() => MudDialog.Close(DialogResult.Cancel());
|
||||
}
|
||||
@@ -28,13 +28,34 @@
|
||||
Required="true"
|
||||
RequiredError="کد پستی الزامی است." />
|
||||
|
||||
<MudTextField @bind-Value="_request.CityId"
|
||||
For="@(() => _request.CityId)"
|
||||
Label="شناسه شهر"
|
||||
<MudAutocomplete T="CityProto.GetAllCitiesByFilterResponseModel"
|
||||
@bind-Value="_selectedCity"
|
||||
Label="شهر"
|
||||
Variant="Variant.Outlined"
|
||||
InputType="InputType.Number"
|
||||
SearchFunc="SearchCities"
|
||||
ToStringFunc="@(city => city != null ? $"{city.Native} ({city.StateName})" : string.Empty)"
|
||||
Required="true"
|
||||
RequiredError="شهر الزامی است." />
|
||||
RequiredError="شهر الزامی است."
|
||||
Clearable="true"
|
||||
ResetValueOnEmptyText="true"
|
||||
CoerceText="false"
|
||||
CoerceValue="false"
|
||||
Dense="true"
|
||||
DebounceInterval="300"
|
||||
MaxItems="20"
|
||||
MinCharacters="2"
|
||||
ProgressIndicatorColor="Color.Primary">
|
||||
<ItemTemplate Context="city">
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.LocationCity" Size="Size.Small" />
|
||||
<MudText>@city.Native</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">(@city.StateName)</MudText>
|
||||
</MudStack>
|
||||
</ItemTemplate>
|
||||
<NoItemsTemplate>
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary pa-2">شهری یافت نشد</MudText>
|
||||
</NoItemsTemplate>
|
||||
</MudAutocomplete>
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
|
||||
@@ -4,14 +4,15 @@ using Mapster;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
using Severity = MudBlazor.Severity;
|
||||
using CityProto = FrontOffice.BFF.City.Protobuf;
|
||||
|
||||
namespace FrontOffice.Main.Pages.Profile.Components;
|
||||
|
||||
public partial class EditAddressDialog : ComponentBase
|
||||
{
|
||||
[CascadingParameter] private IDialogReference MudDialog { get; set; } = default!; // updated type
|
||||
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
||||
// removed duplicate Snackbar injection; provided by Razor partial via _Imports
|
||||
[Inject] private CityProto.CityContract.CityContractClient CityContract { get; set; } = default!;
|
||||
|
||||
[Parameter] public GetAllUserAddressByFilterResponseModel? Model { get; set; }
|
||||
|
||||
@@ -19,12 +20,60 @@ public partial class EditAddressDialog : ComponentBase
|
||||
private readonly UpdateUserAddressRequestValidator _validator = new();
|
||||
private bool _isSaving;
|
||||
private UpdateUserAddressRequest _request = new();
|
||||
private CityProto.GetAllCitiesByFilterResponseModel? _selectedCity;
|
||||
|
||||
protected override void OnInitialized()
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
base.OnInitialized();
|
||||
await base.OnInitializedAsync();
|
||||
if (Model != null)
|
||||
{
|
||||
_request = Model.Adapt<UpdateUserAddressRequest>();
|
||||
|
||||
// Load selected city if CityId exists
|
||||
if (Model.CityId > 0)
|
||||
{
|
||||
await LoadCurrentCity(Model.CityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadCurrentCity(long cityId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await CityContract.GetAllCitiesByFilterAsync(new CityProto.GetAllCitiesByFilterRequest
|
||||
{
|
||||
PaginationState = new CityProto.PaginationState { PageNumber = 1, PageSize = 1 },
|
||||
Filter = new CityProto.GetAllCitiesByFilterFilter { Id = cityId }
|
||||
});
|
||||
|
||||
_selectedCity = response?.Models?.FirstOrDefault();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore error loading city
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<CityProto.GetAllCitiesByFilterResponseModel>> SearchCities(string value, CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value) || value.Length < 2)
|
||||
return Enumerable.Empty<CityProto.GetAllCitiesByFilterResponseModel>();
|
||||
|
||||
try
|
||||
{
|
||||
var response = await CityContract.GetAllCitiesByFilterAsync(new CityProto.GetAllCitiesByFilterRequest
|
||||
{
|
||||
PaginationState = new CityProto.PaginationState { PageNumber = 1, PageSize = 20 },
|
||||
Filter = new CityProto.GetAllCitiesByFilterFilter { Native = value }
|
||||
}, cancellationToken: ct);
|
||||
|
||||
return response?.Models?.ToList() ?? new List<CityProto.GetAllCitiesByFilterResponseModel>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Enumerable.Empty<CityProto.GetAllCitiesByFilterResponseModel>();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveAddress()
|
||||
@@ -32,8 +81,14 @@ public partial class EditAddressDialog : ComponentBase
|
||||
if (_form == null) return;
|
||||
|
||||
await _form.Validate();
|
||||
if (!_form.IsValid) return;
|
||||
if (!_form.IsValid || _selectedCity == null)
|
||||
{
|
||||
if (_selectedCity == null)
|
||||
Snackbar.Add("لطفاً شهر را انتخاب کنید.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
_request.CityId = _selectedCity.Id;
|
||||
_isSaving = true;
|
||||
try
|
||||
{
|
||||
|
||||
@@ -1,62 +1,82 @@
|
||||
@if (_currentUser != null)
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<div class="org-chart-wrapper">
|
||||
@* Toolbar *@
|
||||
<div class="org-chart-toolbar">
|
||||
|
||||
|
||||
<MudSelect T="int" @bind-Value="_selectedDepth" Label="عمق" Variant="Variant.Outlined"
|
||||
Dense="true" Margin="Margin.Dense" Style="width: 90px; min-width: 90px;">
|
||||
<MudSelectItem Value="2">2 سطح</MudSelectItem>
|
||||
<MudSelectItem Value="3">3 سطح</MudSelectItem>
|
||||
<MudSelectItem Value="4">4 سطح</MudSelectItem>
|
||||
<MudSelectItem Value="5">5 سطح</MudSelectItem>
|
||||
<MudSelectItem Value="6">6 سطح</MudSelectItem>
|
||||
<MudSelectItem Value="15">همه</MudSelectItem>
|
||||
</MudSelect>
|
||||
|
||||
<MudButtonGroup Variant="Variant.Outlined" Size="Size.Small" OverrideStyles="false">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.UnfoldMore" OnClick="ExpandAll" Title="باز کردن همه" Size="Size.Small" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.UnfoldLess" OnClick="CollapseAll" Title="بستن همه" Size="Size.Small" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.CenterFocusStrong" OnClick="CenterChart" Title="مرکز" Size="Size.Small" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ZoomOutMap" OnClick="FitToScreen" Title="نمایش کامل" Size="Size.Small" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="RefreshData" Color="Color.Primary" Title="بروزرسانی" Size="Size.Small" />
|
||||
</MudButtonGroup>
|
||||
@if (_currentViewUserId.HasValue)
|
||||
{
|
||||
<div class="org-chart-container">
|
||||
<div class="org-tree">
|
||||
<div class="org-node root-node" id="root-node">
|
||||
<div class="node-card">
|
||||
<div class="node-avatar">
|
||||
@if (!string.IsNullOrWhiteSpace(_currentUser.Avatar))
|
||||
<MudButtonGroup Variant="Variant.Outlined" Size="Size.Small" OverrideStyles="false">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ArrowForward" OnClick="GoBack" Title="بازگشت" Size="Size.Small" Color="Color.Secondary" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Home" OnClick="GoToMyTree" Title="درخت من" Size="Size.Small" Color="Color.Secondary" />
|
||||
</MudButtonGroup>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* Chart Container *@
|
||||
@if (_isLoading)
|
||||
{
|
||||
<MudAvatar Size="Size.Large">
|
||||
<MudImage ObjectFit="ObjectFit.Cover"
|
||||
ObjectPosition="ObjectPosition.Center"
|
||||
Src="@_currentUser.Avatar" />
|
||||
</MudAvatar>
|
||||
<div class="chart-loading">
|
||||
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
|
||||
<MudText Typo="Typo.body2" Class="mt-2">در حال بارگذاری...</MudText>
|
||||
</div>
|
||||
}
|
||||
else if (_hasError)
|
||||
{
|
||||
<div class="error-message">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Error" Color="Color.Error" Size="Size.Large" />
|
||||
<MudText Typo="Typo.body1" Class="mt-2">خطا در بارگذاری دادهها</MudText>
|
||||
<MudButton Variant="Variant.Text" Color="Color.Primary" OnClick="RefreshData">تلاش مجدد</MudButton>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudAvatar Size="Size.Large" Color="Color.Primary" Variant="Variant.Outlined">
|
||||
@(string.IsNullOrWhiteSpace(_currentUser.FirstName) ? "N" : _currentUser.FirstName.Substring(0, 1))
|
||||
</MudAvatar>
|
||||
<div id="org-chart-container" class="org-chart-container"></div>
|
||||
}
|
||||
|
||||
@if (_currentUser?.Children?.Any() == true)
|
||||
@* Stats Bar *@
|
||||
@if (_statistics != null && !_isLoading)
|
||||
{
|
||||
<button class="expand-btn" @onclick="ToggleExpand" data-user-id="@_currentUser.Id">
|
||||
<MudIcon Icon="@(_isExpanded ? Icons.Material.Filled.Remove : Icons.Material.Filled.Add)" Size="Size.Small" />
|
||||
</button>
|
||||
<div class="org-chart-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">کل اعضا:</span>
|
||||
<span class="stat-value">@_statistics.TotalMembers</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">پای چپ:</span>
|
||||
<span class="stat-value left">@_statistics.LeftLegCount</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">پای راست:</span>
|
||||
<span class="stat-value right">@_statistics.RightLegCount</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">عمق:</span>
|
||||
<span class="stat-value">@_statistics.TreeDepth</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">پای ضعیف:</span>
|
||||
<span class="stat-value">@(_statistics.WeakerLeg == "Left" ? "چپ" : "راست")</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="node-info">
|
||||
@if (!string.IsNullOrWhiteSpace(_currentUser.FirstName) || !string.IsNullOrWhiteSpace(_currentUser.LastName))
|
||||
{
|
||||
<div class="node-name">@_currentUser.FirstName @_currentUser.LastName</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="node-name">@_currentUser.Mobile</div>
|
||||
}
|
||||
@* <div class="node-amounts">
|
||||
<div class="personal-amount">
|
||||
<span class="label">خرید شخصی:</span>
|
||||
<span class="amount">@(_currentUser?.PersonalPurchase?.ToThousands().ToCurrencyUnitIRT() ?? "0 تومان")</span>
|
||||
</div>
|
||||
<div class="team-amount">
|
||||
<span class="label">خرید تیمی:</span>
|
||||
<span class="amount">@(_currentUser?.TeamPurchase?.ToThousands().ToCurrencyUnitIRT() ?? "0 تومان")</span>
|
||||
</div>
|
||||
</div> *@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (_isExpanded && _currentUser?.Children?.Any() == true)
|
||||
{
|
||||
<CascadingValue Value="this">
|
||||
<OrganizationChartLevel Nodes="_currentUser.Children" Level="1" />
|
||||
</CascadingValue>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,120 +1,262 @@
|
||||
using FrontOffice.BFF.User.Protobuf.Protos.User;
|
||||
using Mapster;
|
||||
using FrontOffice.Main.Utilities;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using static FrontOffice.Main.Pages.Profile.Components.OrganizationChartLevel;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace FrontOffice.Main.Pages.Profile.Components;
|
||||
public partial class OrganizationChart
|
||||
{
|
||||
private UserNode? _currentUser;
|
||||
private bool _isExpanded;
|
||||
|
||||
[Inject] private UserContract.UserContractClient UserContract { get; set; } = default!;
|
||||
private GetUserResponse _userProfile = new();
|
||||
public partial class OrganizationChart : IAsyncDisposable
|
||||
{
|
||||
private NetworkTreeDto? _networkTree;
|
||||
private NetworkStatisticsDto? _statistics;
|
||||
private bool _isLoading = true;
|
||||
private bool _hasError = false;
|
||||
private int _selectedDepthValue = 3;
|
||||
private DotNetObjectReference<OrganizationChart>? _dotNetHelper;
|
||||
private bool _chartNeedsInit = false;
|
||||
private bool _jsReady = false;
|
||||
|
||||
// برای نگهداری شناسه کاربر فعلی که درختش نمایش داده شده
|
||||
private long? _currentViewUserId = null;
|
||||
// Stack برای بازگشت به عقب
|
||||
private Stack<long> _navigationHistory = new();
|
||||
|
||||
private int _selectedDepth
|
||||
{
|
||||
get => _selectedDepthValue;
|
||||
set
|
||||
{
|
||||
if (_selectedDepthValue != value)
|
||||
{
|
||||
_selectedDepthValue = value;
|
||||
_ = OnDepthChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Inject] private NetworkMembershipService NetworkService { get; set; } = default!;
|
||||
// JSRuntime is injected globally via _Imports.razor;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadCurrentUser();
|
||||
await LoadData();
|
||||
}
|
||||
private async Task LoadUserProfile()
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
_jsReady = true;
|
||||
}
|
||||
|
||||
// Initialize chart after JS is ready and data is loaded
|
||||
if (_jsReady && _chartNeedsInit && _networkTree?.RootNode != null)
|
||||
{
|
||||
_chartNeedsInit = false;
|
||||
await InitializeChart();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadData()
|
||||
{
|
||||
_isLoading = true;
|
||||
_hasError = false;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
_userProfile = await UserContract.GetUserAsync(request: new());
|
||||
// Load tree and statistics in parallel
|
||||
var treeTask = NetworkService.GetMyNetworkTreeAsync(_selectedDepth);
|
||||
var statsTask = NetworkService.GetMyNetworkStatisticsAsync();
|
||||
|
||||
await Task.WhenAll(treeTask, statsTask);
|
||||
|
||||
_networkTree = await treeTask;
|
||||
_statistics = await statsTask;
|
||||
|
||||
// Mark that chart needs to be initialized
|
||||
_chartNeedsInit = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle the case when user is not authenticated or API fails
|
||||
_userProfile = new GetUserResponse();
|
||||
Console.WriteLine($"Error loading network data: {ex.Message}");
|
||||
_hasError = true;
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
private async Task LoadCurrentUser()
|
||||
finally
|
||||
{
|
||||
await LoadUserProfile();
|
||||
|
||||
|
||||
// Mock data - replace with actual API call
|
||||
_currentUser = new UserNode
|
||||
{
|
||||
Id = _userProfile.Id,
|
||||
FirstName = _userProfile.FirstName,
|
||||
LastName = _userProfile.LastName,
|
||||
Mobile = _userProfile.Mobile,
|
||||
Avatar = _userProfile.AvatarPath,
|
||||
PersonalPurchase = 0,
|
||||
TeamPurchase = 0,
|
||||
Children = await GetUserChildren(userId: _userProfile.Id)
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<List<UserNode>?> GetUserChildren(long userId)
|
||||
{
|
||||
Console.WriteLine("OK0");
|
||||
if (userId != 0)
|
||||
{
|
||||
Console.WriteLine("OK1");
|
||||
var children = await UserContract.GetAllUserByFilterAsync(request: new()
|
||||
{
|
||||
Filter = new()
|
||||
{
|
||||
ParentId = userId,
|
||||
}
|
||||
});
|
||||
if (children?.Models?.Any() == true)
|
||||
{
|
||||
Console.WriteLine("OK2");
|
||||
var result = new List<UserNode>();
|
||||
foreach (var item in children.Models)
|
||||
{
|
||||
var node = new UserNode
|
||||
{
|
||||
Id = item.Id,
|
||||
FirstName = item.FirstName,
|
||||
LastName = item.LastName,
|
||||
Mobile = item.Mobile,
|
||||
Avatar = item.AvatarPath,
|
||||
ReferralCode = item.ReferralCode,
|
||||
PersonalPurchase = 0,
|
||||
TeamPurchase = 0,
|
||||
Children = await GetUserChildren(userId: item.Id)
|
||||
};
|
||||
result.Add(node);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private void ToggleExpand()
|
||||
{
|
||||
_isExpanded = !_isExpanded;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public void ToggleNodeExpand(long userId)
|
||||
{
|
||||
var node = FindNode(_currentUser, userId);
|
||||
if (node != null)
|
||||
{
|
||||
node.IsExpanded = !node.IsExpanded;
|
||||
_isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private UserNode? FindNode(UserNode? node, long userId)
|
||||
private async Task InitializeChart()
|
||||
{
|
||||
if (node == null) return null;
|
||||
if (node.Id == userId) return node;
|
||||
if (_networkTree?.RootNode == null) return;
|
||||
|
||||
if (node.Children != null)
|
||||
try
|
||||
{
|
||||
foreach (var child in node.Children)
|
||||
_dotNetHelper = DotNetObjectReference.Create(this);
|
||||
var flatData = _networkTree.ToFlatArray();
|
||||
|
||||
await JSRuntime.InvokeVoidAsync("OrgChart.init", "org-chart-container", flatData, _dotNetHelper);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var found = FindNode(child, userId);
|
||||
if (found != null) return found;
|
||||
Console.WriteLine($"Error initializing chart: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
private async Task RefreshData()
|
||||
{
|
||||
await LoadData();
|
||||
|
||||
if (_networkTree?.RootNode != null)
|
||||
{
|
||||
await InitializeChart();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnDepthChanged()
|
||||
{
|
||||
await RefreshData();
|
||||
}
|
||||
|
||||
private async Task ExpandAll()
|
||||
{
|
||||
try
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("OrgChart.expandAll");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private async Task CollapseAll()
|
||||
{
|
||||
try
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("OrgChart.collapseAll");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private async Task CenterChart()
|
||||
{
|
||||
try
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("OrgChart.center");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private async Task FitToScreen()
|
||||
{
|
||||
try
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("OrgChart.fitToScreen");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private async Task ExportPng()
|
||||
{
|
||||
try
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("OrgChart.exportPng");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called from JavaScript when a node is clicked
|
||||
/// </summary>
|
||||
[JSInvokable]
|
||||
public async Task OnNodeClicked(long userId)
|
||||
{
|
||||
// اگر روی همان نود کلیک شده، کاری نکن
|
||||
if (_currentViewUserId == userId) return;
|
||||
|
||||
// ذخیره نود فعلی در history برای بازگشت
|
||||
if (_currentViewUserId.HasValue)
|
||||
{
|
||||
_navigationHistory.Push(_currentViewUserId.Value);
|
||||
}
|
||||
|
||||
await LoadSubordinateTree(userId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// بارگذاری درخت یک زیرمجموعه
|
||||
/// </summary>
|
||||
private async Task LoadSubordinateTree(long targetUserId)
|
||||
{
|
||||
_isLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
_networkTree = await NetworkService.GetSubordinateTreeAsync(targetUserId, _selectedDepth);
|
||||
_currentViewUserId = targetUserId;
|
||||
_chartNeedsInit = true;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
// نمایش پیام خطا - این کاربر زیرمجموعه نیست
|
||||
Console.WriteLine("Unauthorized access to subordinate tree");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error loading subordinate tree: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// بازگشت به درخت قبلی
|
||||
/// </summary>
|
||||
private async Task GoBack()
|
||||
{
|
||||
if (_navigationHistory.Count > 0)
|
||||
{
|
||||
var previousUserId = _navigationHistory.Pop();
|
||||
await LoadSubordinateTree(previousUserId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// بازگشت به درخت خود کاربر
|
||||
_currentViewUserId = null;
|
||||
await LoadData();
|
||||
if (_networkTree?.RootNode != null)
|
||||
{
|
||||
await InitializeChart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// بازگشت به درخت اصلی (خود کاربر)
|
||||
/// </summary>
|
||||
private async Task GoToMyTree()
|
||||
{
|
||||
_navigationHistory.Clear();
|
||||
_currentViewUserId = null;
|
||||
await LoadData();
|
||||
if (_networkTree?.RootNode != null)
|
||||
{
|
||||
await InitializeChart();
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("OrgChart.dispose");
|
||||
}
|
||||
catch { }
|
||||
|
||||
_dotNetHelper?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,12 @@ using MudBlazor;
|
||||
using FrontOffice.Main.Utilities;
|
||||
|
||||
namespace FrontOffice.Main.Pages.Profile.Components;
|
||||
|
||||
/// <summary>
|
||||
/// DEPRECATED: This component is no longer used.
|
||||
/// d3-org-chart is now used for tree visualization.
|
||||
/// Kept for backward compatibility.
|
||||
/// </summary>
|
||||
public partial class OrganizationChartLevel
|
||||
{
|
||||
[Parameter] public List<UserNode>? Nodes { get; set; }
|
||||
@@ -25,18 +31,12 @@ public partial class OrganizationChartLevel
|
||||
|
||||
private void ToggleNodeExpand(long userId)
|
||||
{
|
||||
if (ParentChart != null)
|
||||
{
|
||||
ParentChart.ToggleNodeExpand(userId);
|
||||
}
|
||||
// No longer needed - d3-org-chart handles this internally
|
||||
}
|
||||
|
||||
private void SafeToggleNodeExpand(long userId)
|
||||
{
|
||||
if (ParentChart != null)
|
||||
{
|
||||
ParentChart.ToggleNodeExpand(userId);
|
||||
}
|
||||
// No longer needed - d3-org-chart handles this internally
|
||||
}
|
||||
|
||||
private async Task CopyReferralCode(string referralCode)
|
||||
|
||||
@@ -192,6 +192,17 @@
|
||||
</MudCard>
|
||||
</MudLink>
|
||||
</MudItem>
|
||||
<MudItem xs="6" sm="6" md="3">
|
||||
<MudLink Href="@RouteConstants.Profile.WithdrawalRequests" 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.RequestPage" Size="Size.Large" Color="Color.Warning" />
|
||||
<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.Club.Membership" Underline="Underline.None" Class="tile-link">
|
||||
<MudCard Elevation="1" Class="rounded-lg profile-tile">
|
||||
|
||||
@@ -56,10 +56,16 @@ public partial class Index
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
if (firstRender)
|
||||
{
|
||||
// Refresh token to get latest user claims
|
||||
await AuthService.RefreshTokenAsync();
|
||||
|
||||
await LoadUserAuthInfo();
|
||||
await LoadUserProfile();
|
||||
await LoadAddresses();
|
||||
await LoadWallet();
|
||||
|
||||
// نمایش Modal قرارداد باشگاه اگر پکیج خریده ولی باشگاه فعال نشده
|
||||
await CheckAndShowClubContractModal();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +85,39 @@ public partial class Index
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// بررسی و نمایش Modal قرارداد باشگاه مشتریان
|
||||
/// اگر کاربر پکیج خریده ولی هنوز قرارداد باشگاه را امضا نکرده
|
||||
/// </summary>
|
||||
private async Task CheckAndShowClubContractModal()
|
||||
{
|
||||
// اگر پکیج خریده ولی باشگاه فعال نیست = باید قرارداد امضا کند
|
||||
if (_hasPurchasedPackage && !_isClubMemberActive)
|
||||
{
|
||||
var options = new DialogOptions
|
||||
{
|
||||
BackdropClick = false, // غیرقابل بسته شدن با کلیک بیرون
|
||||
CloseOnEscapeKey = false, // غیرقابل بسته شدن با Escape
|
||||
CloseButton = false, // بدون دکمه بستن
|
||||
MaxWidth = MaxWidth.Medium,
|
||||
FullWidth = true
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<Shared.ClubMembershipContractDialog>(
|
||||
"قرارداد باشگاه مشتریان",
|
||||
options);
|
||||
|
||||
var result = await dialog.Result;
|
||||
|
||||
// اگر موفق بود، صفحه را refresh کنیم
|
||||
if (!result.Canceled)
|
||||
{
|
||||
await LoadUserAuthInfo();
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string _walletCredit = "-";
|
||||
private string _walletDiscount = "-";
|
||||
private string _walletNetwork = "-";
|
||||
|
||||
@@ -17,14 +17,9 @@
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg" Style="background-color: var(--mud-palette-warning-lighten);">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Discount" Color="Color.Warning" Size="Size.Large" />
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.subtitle2" Style="color: var(--mud-palette-warning-darken);">موجودی تخفیف باشگاه</MudText>
|
||||
<MudText Typo="Typo.h4" Style="color: var(--mud-palette-warning-darken);"><strong>@_balances.Discount</strong></MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
||||
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">موجودی فروشگاه تخفیفی</MudText>
|
||||
<MudText Typo="Typo.h4" Color="Color.Warning">@_balances.Discount</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
@@ -35,61 +30,45 @@
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
||||
<MudText Typo="Typo.h6" Class="mb-2">درخواست برداشت از موجودی شبکه</MudText>
|
||||
<MudStack Spacing="2">
|
||||
<MudTextField @bind-Value="_withdrawPayoutId"
|
||||
Label="شناسه واریز (PayoutId)"
|
||||
Variant="Variant.Outlined"
|
||||
Type="MudBlazor.InputType.Number"
|
||||
Required="true" />
|
||||
<MudRadioGroup T="WithdrawalMethodClient" @bind-SelectedOption="_withdrawMethod" Row="true">
|
||||
<MudRadio T="WithdrawalMethodClient" Option="@WithdrawalMethodClient.Cash" Label="برداشت نقدی (نیاز به شبا)" />
|
||||
<MudRadio T="WithdrawalMethodClient" Option="@WithdrawalMethodClient.Diamond" Label="الماس/غیرنقدی" />
|
||||
</MudRadioGroup>
|
||||
<MudTextField @bind-Value="_withdrawIban"
|
||||
Label="شماره شبا"
|
||||
Variant="Variant.Outlined"
|
||||
Disabled="_withdrawMethod == WithdrawalMethodClient.Diamond"
|
||||
Placeholder="IRxxxxxxxxxxxx"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentText="IR" />
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
||||
حداقل مبلغ برداشت: @FormatPrice(_minWithdrawalAmount)
|
||||
</MudText>
|
||||
<MudButton Disabled="_isSubmittingWithdrawal"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Outbound"
|
||||
OnClick="SubmitWithdrawal">
|
||||
@(_isSubmittingWithdrawal ? "در حال ثبت..." : "ثبت درخواست برداشت")
|
||||
<!-- دکمه دسترسی سریع به درخواست برداشت -->
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Warning"
|
||||
Size="Size.Large"
|
||||
FullWidth="true"
|
||||
StartIcon="@Icons.Material.Filled.RequestPage"
|
||||
Href="@RouteConstants.Profile.WithdrawalRequests"
|
||||
Class="rounded-lg">
|
||||
درخواستهای برداشت
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
||||
<MudText Typo="Typo.h6" Class="mb-2">تراکنشها و مسیرهای شارژ</MudText>
|
||||
|
||||
<MudStack Row="true" Spacing="2" Class="mb-3" AlignItems="AlignItems.End">
|
||||
<MudGrid Spacing="2" Class="mb-3">
|
||||
<MudItem xs="6" md="4">
|
||||
<MudTextField @bind-Value="_filterReferenceId"
|
||||
Label="شناسه ارجاع"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search" />
|
||||
<MudSelect T="string" @bind-Value="_filterType" Label="نوع تراکنش" Variant="Variant.Outlined">
|
||||
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||
FullWidth="true" />
|
||||
</MudItem>
|
||||
<MudItem xs="6" md="4">
|
||||
<MudSelect T="string" @bind-Value="_filterType" Label="نوع تراکنش" Variant="Variant.Outlined" FullWidth="true">
|
||||
<MudSelectItem T="string" Value="@("all")">همه</MudSelectItem>
|
||||
<MudSelectItem T="string" Value="@("in")">ورودی (شارژ)</MudSelectItem>
|
||||
<MudSelectItem T="string" Value="@("out")">خروجی (برداشت/خرید)</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudSelect T="string" @bind-Value="_withdrawStatusFilter" Label="وضعیت برداشت" Variant="Variant.Outlined">
|
||||
<MudSelectItem T="string" Value="@("all")">همه</MudSelectItem>
|
||||
<MudSelectItem T="string" Value="@("pending")">Pending</MudSelectItem>
|
||||
<MudSelectItem T="string" Value="@("requested")">Requested</MudSelectItem>
|
||||
<MudSelectItem T="string" Value="@("withdrawn")">Withdrawn</MudSelectItem>
|
||||
<MudSelectItem T="string" Value="@("cancelled")">Cancelled</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudButton Variant="Variant.Outlined" OnClick="ApplyFilters" StartIcon="@Icons.Material.Filled.FilterList">اعمال فیلتر</MudButton>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4" Class="d-flex align-end">
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
OnClick="ApplyFilters"
|
||||
StartIcon="@Icons.Material.Filled.FilterList"
|
||||
FullWidth="true">
|
||||
اعمال فیلتر
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
|
||||
<MudTable Items="_txs" Dense="true">
|
||||
@@ -126,49 +105,5 @@
|
||||
</MudStack>
|
||||
</MudHidden>
|
||||
</MudPaper>
|
||||
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
||||
<MudText Typo="Typo.h6" Class="mb-2">درخواستهای برداشت ثبتشده</MudText>
|
||||
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
|
||||
<MudTable Items="_withdrawals" Dense="true">
|
||||
<HeaderContent>
|
||||
<MudTh>هفته</MudTh>
|
||||
<MudTh>مبلغ</MudTh>
|
||||
<MudTh>وضعیت</MudTh>
|
||||
<MudTh>روش</MudTh>
|
||||
<MudTh>تاریخ</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.WeekNumber</MudTd>
|
||||
<MudTd>@FormatPrice(context.Amount)</MudTd>
|
||||
<MudTd>
|
||||
<MudChip T="string" Color="@(ResolveStatusColor(context.Status))" Variant="Variant.Outlined" Size="Size.Small">
|
||||
@ResolveStatusText(context.Status)
|
||||
</MudChip>
|
||||
</MudTd>
|
||||
<MudTd>@ResolveMethodText(context.Method)</MudTd>
|
||||
<MudTd>@context.Created</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
</MudHidden>
|
||||
|
||||
<MudHidden Breakpoint="Breakpoint.MdAndUp">
|
||||
<MudStack Spacing="2">
|
||||
@foreach (var wd in _withdrawals)
|
||||
{
|
||||
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
|
||||
<MudStack Spacing="1">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText>@wd.WeekNumber</MudText>
|
||||
<MudText Color="Color.Primary">@FormatPrice(wd.Amount)</MudText>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">وضعیت: @ResolveStatusText(wd.Status) | روش: @ResolveMethodText(wd.Method)</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">@wd.Created</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
</MudStack>
|
||||
</MudHidden>
|
||||
</MudPaper>
|
||||
</MudStack>
|
||||
</MudContainer>
|
||||
|
||||
@@ -6,71 +6,20 @@ namespace FrontOffice.Main.Pages.Profile;
|
||||
|
||||
public partial class Wallet : ComponentBase
|
||||
{
|
||||
private long _minWithdrawalAmount = 1_000_000; // مقدار پیشفرض، از CMS خوانده میشود
|
||||
private (string Credit, string Discount, string Network) _balances = ("-", "-", "-");
|
||||
private List<WalletTransaction> _txs = new();
|
||||
private List<WalletWithdrawal> _withdrawals = new();
|
||||
private string? _filterReferenceId;
|
||||
private string _filterType = "all";
|
||||
private string _withdrawStatusFilter = "all";
|
||||
private bool _isSubmittingWithdrawal;
|
||||
private long _withdrawPayoutId;
|
||||
private WithdrawalMethodClient _withdrawMethod = WithdrawalMethodClient.Cash;
|
||||
private string? _withdrawIban;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var b = await WalletService.GetBalancesAsync();
|
||||
_balances = (FormatPrice(b.CreditBalance), FormatPrice(b.DiscountBalance), FormatPrice(b.NetworkBalance));
|
||||
_txs = await WalletService.GetTransactionsAsync();
|
||||
_withdrawals = await WalletService.GetWithdrawalsAsync();
|
||||
var settings = await WalletService.GetWithdrawalSettingsAsync();
|
||||
if (settings.MinWithdrawalAmount > 0)
|
||||
_minWithdrawalAmount = settings.MinWithdrawalAmount;
|
||||
}
|
||||
|
||||
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
|
||||
|
||||
private async Task SubmitWithdrawal()
|
||||
{
|
||||
var target = _withdrawals.FirstOrDefault(x => x.Id == _withdrawPayoutId);
|
||||
if (target is null)
|
||||
{
|
||||
_withdrawals = await WalletService.GetWithdrawalsAsync();
|
||||
target = _withdrawals.FirstOrDefault(x => x.Id == _withdrawPayoutId);
|
||||
}
|
||||
if (target != null && target.Amount < _minWithdrawalAmount)
|
||||
{
|
||||
Snackbar.Add($"مبلغ این واریز کمتر از حداقل برداشت ({_minWithdrawalAmount:N0} تومان) است.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
if (_withdrawPayoutId <= 0)
|
||||
{
|
||||
Snackbar.Add("شناسه واریز (PayoutId) الزامی است.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
if (_withdrawMethod == WithdrawalMethodClient.Cash && string.IsNullOrWhiteSpace(_withdrawIban))
|
||||
{
|
||||
Snackbar.Add("برای برداشت نقدی، شماره شبا لازم است.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_isSubmittingWithdrawal = true;
|
||||
await WalletService.RequestWithdrawalAsync(_withdrawPayoutId, _withdrawMethod, _withdrawIban);
|
||||
Snackbar.Add("درخواست برداشت ثبت شد.", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ثبت برداشت: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSubmittingWithdrawal = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplyFilters()
|
||||
{
|
||||
long? refId = null;
|
||||
@@ -85,39 +34,5 @@ public partial class Wallet : ComponentBase
|
||||
_ => null
|
||||
};
|
||||
_txs = await WalletService.GetTransactionsAsync(refId, isIncrease);
|
||||
int? status = _withdrawStatusFilter switch
|
||||
{
|
||||
"pending" => 1,
|
||||
"requested" => 2,
|
||||
"withdrawn" => 3,
|
||||
"cancelled" => 4,
|
||||
_ => null
|
||||
};
|
||||
_withdrawals = await WalletService.GetWithdrawalsAsync(status);
|
||||
}
|
||||
|
||||
private static string ResolveStatusText(int status) => status switch
|
||||
{
|
||||
0 => "ایجاد شده",
|
||||
1 => "پرداخت شده",
|
||||
2 => "درخواست برداشت",
|
||||
3 => "برداشت شده",
|
||||
4 => "لغو شده",
|
||||
_ => status.ToString()
|
||||
};
|
||||
|
||||
private static Color ResolveStatusColor(int status) => status switch
|
||||
{
|
||||
2 => Color.Warning,
|
||||
3 => Color.Success,
|
||||
4 => Color.Error,
|
||||
_ => Color.Info
|
||||
};
|
||||
|
||||
private static string ResolveMethodText(int? method) => method switch
|
||||
{
|
||||
0 => "نقدی",
|
||||
1 => "الماس",
|
||||
_ => "-"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
@attribute [Route(RouteConstants.Profile.WithdrawalRequests)]
|
||||
|
||||
<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.Wallet">بازگشت به کیف پول</MudButton>
|
||||
</MudStack>
|
||||
|
||||
<!-- فرم درخواست برداشت جدید -->
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
||||
<MudText Typo="Typo.h6" Class="mb-2">ثبت درخواست برداشت جدید</MudText>
|
||||
<MudStack Spacing="2">
|
||||
<MudTextField @bind-Value="_withdrawPayoutId"
|
||||
Label="شناسه واریز (PayoutId)"
|
||||
Variant="Variant.Outlined"
|
||||
Type="MudBlazor.InputType.Number"
|
||||
Required="true" />
|
||||
<MudRadioGroup T="WithdrawalMethodClient" @bind-Value="_withdrawMethod" Row="true">
|
||||
<MudRadio T="WithdrawalMethodClient" Option="@WithdrawalMethodClient.Cash" Color="Color.Primary">برداشت نقدی (نیاز به شبا)</MudRadio>
|
||||
<MudRadio T="WithdrawalMethodClient" Option="@WithdrawalMethodClient.Diamond" Color="Color.Secondary">الماس/غیرنقدی</MudRadio>
|
||||
</MudRadioGroup>
|
||||
<MudTextField @bind-Value="_withdrawIban"
|
||||
Label="شماره شبا"
|
||||
Variant="Variant.Outlined"
|
||||
Disabled="_withdrawMethod == WithdrawalMethodClient.Diamond"
|
||||
Placeholder="IRxxxxxxxxxxxx"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentText="IR" />
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
||||
حداقل مبلغ برداشت: @FormatPrice(_minWithdrawalAmount)
|
||||
</MudText>
|
||||
<MudButton Disabled="_isSubmittingWithdrawal"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Outbound"
|
||||
OnClick="SubmitWithdrawal">
|
||||
@(_isSubmittingWithdrawal ? "در حال ثبت..." : "ثبت درخواست برداشت")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<!-- فیلتر وضعیت -->
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.End">
|
||||
<MudSelect T="string" @bind-Value="_statusFilter" Label="فیلتر وضعیت" Variant="Variant.Outlined" Style="min-width: 180px;">
|
||||
<MudSelectItem T="string" Value="@("all")">همه</MudSelectItem>
|
||||
<MudSelectItem T="string" Value="@("pending")">در انتظار</MudSelectItem>
|
||||
<MudSelectItem T="string" Value="@("requested")">درخواست شده</MudSelectItem>
|
||||
<MudSelectItem T="string" Value="@("withdrawn")">برداشت شده</MudSelectItem>
|
||||
<MudSelectItem T="string" Value="@("cancelled")">لغو شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudButton Variant="Variant.Outlined" OnClick="ApplyFilter" StartIcon="@Icons.Material.Filled.FilterList">اعمال فیلتر</MudButton>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<!-- لیست درخواستهای برداشت -->
|
||||
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
|
||||
<MudText Typo="Typo.h6" Class="mb-3">لیست درخواستها</MudText>
|
||||
|
||||
@if (_isLoading)
|
||||
{
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
|
||||
}
|
||||
else if (!_withdrawals.Any())
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined">
|
||||
هنوز درخواست برداشتی ثبت نشده است.
|
||||
</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- نمایش دسکتاپ -->
|
||||
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
|
||||
<MudTable Items="_withdrawals" Dense="true" Hover="true" Striped="true">
|
||||
<HeaderContent>
|
||||
<MudTh>هفته</MudTh>
|
||||
<MudTh>مبلغ</MudTh>
|
||||
<MudTh>وضعیت</MudTh>
|
||||
<MudTh>روش برداشت</MudTh>
|
||||
<MudTh>تاریخ ثبت</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="هفته">@context.WeekDisplayName</MudTd>
|
||||
<MudTd DataLabel="مبلغ">@FormatPrice(context.Amount)</MudTd>
|
||||
<MudTd DataLabel="وضعیت">
|
||||
<MudChip T="string" Color="@(ResolveStatusColor(context.Status))" Variant="Variant.Filled" Size="Size.Small">
|
||||
@ResolveStatusText(context.Status)
|
||||
</MudChip>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="روش">@ResolveMethodText(context.Method)</MudTd>
|
||||
<MudTd DataLabel="تاریخ">@context.Created</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
</MudHidden>
|
||||
|
||||
<!-- نمایش موبایل -->
|
||||
<MudHidden Breakpoint="Breakpoint.MdAndUp">
|
||||
<MudStack Spacing="2">
|
||||
@foreach (var wd in _withdrawals)
|
||||
{
|
||||
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
|
||||
<MudStack Spacing="1">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.subtitle2">@wd.WeekDisplayName</MudText>
|
||||
<MudChip T="string" Color="@(ResolveStatusColor(wd.Status))" Variant="Variant.Filled" Size="Size.Small">
|
||||
@ResolveStatusText(wd.Status)
|
||||
</MudChip>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary">@FormatPrice(wd.Amount)</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">@ResolveMethodText(wd.Method)</MudText>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">@wd.Created</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
</MudStack>
|
||||
</MudHidden>
|
||||
}
|
||||
</MudPaper>
|
||||
</MudStack>
|
||||
</MudContainer>
|
||||
@@ -0,0 +1,123 @@
|
||||
using FrontOffice.Main.Utilities;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace FrontOffice.Main.Pages.Profile;
|
||||
|
||||
public partial class WithdrawalRequests : ComponentBase
|
||||
{
|
||||
private long _minWithdrawalAmount = 1_000_000;
|
||||
private List<WalletWithdrawal> _withdrawals = new();
|
||||
private string _statusFilter = "all";
|
||||
private bool _isLoading = true;
|
||||
private bool _isSubmittingWithdrawal;
|
||||
private long _withdrawPayoutId;
|
||||
private WithdrawalMethodClient _withdrawMethod = WithdrawalMethodClient.Cash;
|
||||
private string? _withdrawIban;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadData();
|
||||
}
|
||||
|
||||
private async Task LoadData()
|
||||
{
|
||||
_isLoading = true;
|
||||
try
|
||||
{
|
||||
_withdrawals = await WalletService.GetWithdrawalsAsync();
|
||||
var settings = await WalletService.GetWithdrawalSettingsAsync();
|
||||
if (settings.MinWithdrawalAmount > 0)
|
||||
_minWithdrawalAmount = settings.MinWithdrawalAmount;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
|
||||
|
||||
private async Task SubmitWithdrawal()
|
||||
{
|
||||
if (_withdrawPayoutId <= 0)
|
||||
{
|
||||
Snackbar.Add("شناسه واریز (PayoutId) الزامی است.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
if (_withdrawMethod == WithdrawalMethodClient.Cash && string.IsNullOrWhiteSpace(_withdrawIban))
|
||||
{
|
||||
Snackbar.Add("برای برداشت نقدی، شماره شبا لازم است.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_isSubmittingWithdrawal = true;
|
||||
await WalletService.RequestWithdrawalAsync(_withdrawPayoutId, _withdrawMethod, _withdrawIban);
|
||||
Snackbar.Add("درخواست برداشت ثبت شد.", Severity.Success);
|
||||
|
||||
// بروزرسانی لیست
|
||||
await LoadData();
|
||||
|
||||
// ریست فرم
|
||||
_withdrawPayoutId = 0;
|
||||
_withdrawIban = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ثبت برداشت: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSubmittingWithdrawal = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplyFilter()
|
||||
{
|
||||
_isLoading = true;
|
||||
try
|
||||
{
|
||||
int? status = _statusFilter switch
|
||||
{
|
||||
"pending" => 1,
|
||||
"requested" => 2,
|
||||
"withdrawn" => 3,
|
||||
"cancelled" => 4,
|
||||
_ => null
|
||||
};
|
||||
_withdrawals = await WalletService.GetWithdrawalsAsync(status);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveStatusText(int status) => status switch
|
||||
{
|
||||
0 => "ایجاد شده",
|
||||
1 => "در انتظار پرداخت",
|
||||
2 => "درخواست برداشت",
|
||||
3 => "برداشت شده",
|
||||
4 => "لغو شده",
|
||||
_ => status.ToString()
|
||||
};
|
||||
|
||||
private static Color ResolveStatusColor(int status) => status switch
|
||||
{
|
||||
1 => Color.Info,
|
||||
2 => Color.Warning,
|
||||
3 => Color.Success,
|
||||
4 => Color.Error,
|
||||
_ => Color.Default
|
||||
};
|
||||
|
||||
private static string ResolveMethodText(int? method) => method switch
|
||||
{
|
||||
0 => "نقدی (شبا)",
|
||||
1 => "الماس",
|
||||
_ => "-"
|
||||
};
|
||||
}
|
||||
@@ -141,11 +141,22 @@
|
||||
</MudHidden>
|
||||
@if (DeviceDetector.IsMobile())
|
||||
{
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center"
|
||||
Class="mobile-actions-stack px-5">
|
||||
<MudText Typo="Typo.subtitle2"> تخفیف کل: <b>@FormatPrice(CartData.TotalDiscount)</b></MudText>
|
||||
<MudText Typo="Typo.h5"> مبلغ کل: <b>@FormatPrice(CartData.Total)</b></MudText>
|
||||
|
||||
<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(CartData.Total) تومان</MudText>
|
||||
</MudStack>
|
||||
@if (VAT.IsEnabled)
|
||||
{
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">مالیات (@VAT.VatPercentage%):</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">@FormatPrice(VATAmount) تومان</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.h6" Style="font-weight:bold;">مبلغ قابل پرداخت:</MudText>
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary" Style="font-weight:bold;">@FormatPrice(TotalWithVAT) تومان</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" Spacing="2" Class="mobile-actions-stack">
|
||||
@@ -159,12 +170,29 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center"
|
||||
Class="mobile-actions-stack px-5">
|
||||
<MudText Typo="Typo.subtitle2"> تخفیف کل: <b>@FormatPrice(CartData.TotalDiscount)</b></MudText>
|
||||
<MudText Typo="Typo.h5"> مبلغ کل: <b>@FormatPrice(CartData.Total)</b></MudText>
|
||||
<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(CartData.Total) تومان</MudText>
|
||||
</MudStack>
|
||||
@if (VAT.IsEnabled)
|
||||
{
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">مالیات بر ارزش افزوده (@VAT.VatPercentage%):</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">@FormatPrice(VATAmount) تومان</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
<MudDivider Class="my-1" />
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.subtitle1" Style="font-weight:bold;">مبلغ قابل پرداخت:</MudText>
|
||||
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Style="font-weight:bold;">@FormatPrice(TotalWithVAT) تومان</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
||||
<MudStack Row="true" Justify="Justify.FlexEnd" Spacing="2" Class="mt-3">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
|
||||
OnClick="() => Navigation.NavigateTo(RouteConstants.Store.Products)">افزودن محصول
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Success" OnClick="ProceedCheckout"
|
||||
|
||||
@@ -7,14 +7,27 @@ namespace FrontOffice.Main.Pages.Store;
|
||||
public partial class Cart : ComponentBase, IDisposable
|
||||
{
|
||||
[Inject] private CartService CartService { get; set; } = default!;
|
||||
[Inject] private VATService VAT { get; set; } = default!;
|
||||
// Navigation and Snackbar are available via _Imports.razor
|
||||
private CartService CartData => CartService;
|
||||
|
||||
protected override void OnInitialized()
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
|
||||
await CartService.EnsureInitializedAsync();
|
||||
CartService.OnChange += StateHasChanged;
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
// بارگذاری نرخ VAT
|
||||
await VAT.LoadAsync();
|
||||
}
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
private async Task IncrementQty(long productId)
|
||||
{
|
||||
var item = CartData.Items.FirstOrDefault(i => i.ProductId == productId);
|
||||
@@ -44,7 +57,13 @@ public partial class Cart : ComponentBase, IDisposable
|
||||
Navigation.NavigateTo(RouteConstants.Store.CheckoutSummary);
|
||||
}
|
||||
|
||||
private static string FormatPrice(long price) => string.Format("{0:N0} ", price);
|
||||
private string FormatPrice(long price) => $"{price:N0} ";
|
||||
|
||||
private string FormatPriceWithVAT(long price) => $"{VAT.AddVAT(price):N0} ";
|
||||
|
||||
private long TotalWithVAT => VAT.AddVAT(CartData.Total);
|
||||
|
||||
private long VATAmount => VAT.CalculateVAT(CartData.Total);
|
||||
|
||||
private static string GetProductImageUrl(string? imageUrl)
|
||||
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl;
|
||||
|
||||
@@ -108,14 +108,17 @@
|
||||
<MudText Typo="Typo.body2">جمع کالاها:</MudText>
|
||||
<MudText Typo="Typo.body2">@FormatPrice(Cart.Total)</MudText>
|
||||
</MudStack>
|
||||
@if (VAT.IsEnabled)
|
||||
{
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">مالیات بر ارزش افزوده (۹%):</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">مالیات بر ارزش افزوده (@VAT.VatPercentage%):</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">@FormatPrice(CalculateVAT())</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
<MudDivider Class="my-1" />
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.subtitle1" Style="font-weight: bold;">مبلغ قابل پرداخت:</MudText>
|
||||
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Style="font-weight: bold;">@FormatPrice(Cart.Total + CalculateVAT())</MudText>
|
||||
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Style="font-weight: bold;">@FormatPrice(VAT.AddVAT(Cart.Total))</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ public partial class CheckoutSummary : ComponentBase
|
||||
{
|
||||
[Inject] private CartService Cart { get; set; } = default!;
|
||||
[Inject] private OrderService OrderService { get; set; } = default!;
|
||||
[Inject] private VATService VAT { get; set; } = default!;
|
||||
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
||||
[Inject] private UserOrderContract.UserOrderContractClient UserOrderContract { get; set; } = default!;
|
||||
// Snackbar and Navigation are injected via _Imports.razor
|
||||
@@ -25,10 +26,22 @@ public partial class CheckoutSummary : ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
|
||||
await Cart.EnsureInitializedAsync();
|
||||
await LoadAddresses();
|
||||
await LoadWalletBalance();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
// بارگذاری نرخ VAT
|
||||
await VAT.LoadAsync();
|
||||
}
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
private async Task LoadWalletBalance()
|
||||
{
|
||||
var walletResult = await WalletService.GetBalancesAsync();
|
||||
@@ -75,7 +88,7 @@ public partial class CheckoutSummary : ComponentBase
|
||||
{
|
||||
var request = new SubmitShopBuyOrderRequest
|
||||
{
|
||||
TotalAmount = Cart.Total
|
||||
TotalAmount = VAT.AddVAT(Cart.Total)
|
||||
};
|
||||
|
||||
var response = await UserOrderContract.SubmitShopBuyOrderAsync(request);
|
||||
@@ -92,9 +105,9 @@ public partial class CheckoutSummary : ComponentBase
|
||||
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
|
||||
|
||||
/// <summary>
|
||||
/// محاسبه مالیات بر ارزش افزوده (۹%)
|
||||
/// محاسبه مالیات بر ارزش افزوده
|
||||
/// </summary>
|
||||
private long CalculateVAT() => (long)(Cart.Total * 0.09);
|
||||
private long CalculateVAT() => VAT.CalculateVAT(Cart.Total);
|
||||
|
||||
private static string GetProductImageUrl(string? imageUrl)
|
||||
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl;
|
||||
|
||||
@@ -78,8 +78,6 @@ else
|
||||
@* نمایش جزئیات مالی *@
|
||||
@{
|
||||
var subtotal = _order.FactorDetails.Sum(s => s.UnitPrice.Value * s.Count.Value);
|
||||
var vatAmount = (long)(subtotal * 0.09);
|
||||
var totalWithVat = subtotal + vatAmount;
|
||||
}
|
||||
<MudStack Spacing="1" Class="pa-2" Style="background-color: var(--mud-palette-background-grey); border-radius: 8px;">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
@@ -87,15 +85,26 @@ else
|
||||
<MudText Typo="Typo.body2">@FormatPrice(subtotal)</MudText>
|
||||
</MudStack>
|
||||
|
||||
@if (_order.VatInfo is not null)
|
||||
{
|
||||
var vatPercent = (int)(_order.VatInfo.VatRate * 100);
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">مالیات بر ارزش افزوده (۹%):</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">@FormatPrice(vatAmount)</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">مالیات بر ارزش افزوده (@vatPercent%):</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">@FormatPrice(_order.VatInfo.VatAmount)</MudText>
|
||||
</MudStack>
|
||||
<MudDivider Class="my-1" />
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.subtitle1" Style="font-weight: bold;">مبلغ قابل پرداخت:</MudText>
|
||||
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Style="font-weight: bold;">@FormatPrice(totalWithVat)</MudText>
|
||||
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Style="font-weight: bold;">@FormatPrice(_order.VatInfo.TotalAmount)</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.subtitle1" Style="font-weight: bold;">مبلغ قابل پرداخت:</MudText>
|
||||
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Style="font-weight: bold;">@FormatPrice(subtotal)</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace FrontOffice.Main.Pages.Store;
|
||||
public partial class Orders : ComponentBase
|
||||
{
|
||||
[Inject] private OrderService OrderService { get; set; } = default!;
|
||||
[Inject] private VATService VAT { get; set; } = default!;
|
||||
|
||||
private List<GetUserOrderResponse> _orders = new();
|
||||
private bool _loading;
|
||||
@@ -31,6 +32,16 @@ public partial class Orders : ComponentBase
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
// بارگذاری نرخ VAT
|
||||
await VAT.LoadAsync();
|
||||
}
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
|
||||
|
||||
|
||||
|
||||
@@ -77,10 +77,36 @@ else
|
||||
@* </MudStack> *@
|
||||
@* } *@
|
||||
<MudDivider Class="my-2"/>
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.h5" Color="Color.Primary">@FormatPrice(_product.Price)</MudText>
|
||||
@if (VAT.IsEnabled)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">
|
||||
(شامل @VAT.VatPercentage% مالیات بر ارزش افزوده)
|
||||
</MudText>
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
<!-- نمایش وضعیت موجودی -->
|
||||
@if (IsInStock)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Success" Variant="Variant.Filled" Size="Size.Small">
|
||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Size="Size.Small" Class="me-1" />
|
||||
موجود در انبار (@_product.RemainingCount عدد)
|
||||
</MudChip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Cancel" Size="Size.Small" Class="me-1" />
|
||||
ناموجود
|
||||
</MudChip>
|
||||
}
|
||||
|
||||
<!-- Desktop/tablet actions -->
|
||||
<MudHidden Breakpoint="Breakpoint.MdAndUp" Invert="true">
|
||||
@if (IsInStock)
|
||||
{
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudNumericField T="int" @bind-Value="_qty" Min="@MinQty" Max="@MaxQty" Immediate="true"
|
||||
HideSpinButtons="true" Style="max-width:120px"/>
|
||||
@@ -89,6 +115,13 @@ else
|
||||
سبد
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="true">
|
||||
این محصول در حال حاضر موجود نیست.
|
||||
</MudAlert>
|
||||
}
|
||||
</MudHidden>
|
||||
|
||||
<!-- Mobile actions use sticky bar rendered below -->
|
||||
@@ -103,6 +136,8 @@ else
|
||||
<MudPaper Class="mud-width-full mud-elevation-3 pa-3"
|
||||
Style="position:sticky;bottom:0;left:0;z-index:9;border-top-left-radius:1rem;border-top-right-radius:1rem;">
|
||||
|
||||
@if (IsInStock)
|
||||
{
|
||||
<MudGrid Class="align-center" Justify="Justify.SpaceBetween">
|
||||
<MudItem xs="6">
|
||||
<MudStack Spacing="1">
|
||||
@@ -138,7 +173,6 @@ else
|
||||
{
|
||||
<MudItem xs="6" >
|
||||
<MudStack Spacing="1">
|
||||
|
||||
<MudButton Variant="Variant.Filled" Size="Size.Large" Color="Color.Error" Class="mud-width-full"
|
||||
StartIcon="@Icons.Material.Filled.AddShoppingCart" OnClick="AddToCart">
|
||||
افزودن به سبد
|
||||
@@ -147,7 +181,13 @@ else
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Variant="Variant.Filled" Dense="true" Class="mb-0">
|
||||
این محصول ناموجود است
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
</MudPaper>
|
||||
</MudHidden>
|
||||
|
||||
@@ -11,6 +11,7 @@ public partial class ProductDetail : ComponentBase, IDisposable
|
||||
{
|
||||
[Inject] private ProductService ProductService { get; set; } = default!;
|
||||
[Inject] private CartService Cart { get; set; } = default!;
|
||||
[Inject] private VATService VAT { get; set; } = default!;
|
||||
|
||||
[Parameter] public long id { get; set; }
|
||||
|
||||
@@ -18,7 +19,13 @@ public partial class ProductDetail : ComponentBase, IDisposable
|
||||
private bool _loading;
|
||||
private int _qty = 1;
|
||||
private const int MinQty = 1;
|
||||
private const int MaxQty = 20;
|
||||
|
||||
// حداکثر تعداد قابل خرید بر اساس موجودی انبار
|
||||
private int MaxQty => _product?.RemainingCount > 0 ? _product.RemainingCount : 0;
|
||||
|
||||
// آیا محصول موجود است
|
||||
private bool IsInStock => _product is not null && _product.RemainingCount > 0;
|
||||
|
||||
private IReadOnlyList<ProductGalleryImage> _galleryItems = Array.Empty<ProductGalleryImage>();
|
||||
private IReadOnlyList<ProductCategoryPathInfo> _categoryPaths = Array.Empty<ProductCategoryPathInfo>();
|
||||
private ProductGalleryImage? _selectedGalleryImage;
|
||||
@@ -44,8 +51,12 @@ public partial class ProductDetail : ComponentBase, IDisposable
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
|
||||
await Cart.EnsureInitializedAsync();
|
||||
Cart.OnChange += HandleCartChanged;
|
||||
|
||||
|
||||
|
||||
_loading = true;
|
||||
_product = await ProductService.GetByIdAsync(id);
|
||||
_loading = false;
|
||||
@@ -68,7 +79,15 @@ public partial class ProductDetail : ComponentBase, IDisposable
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
if (firstRender)
|
||||
{
|
||||
// بارگذاری نرخ VAT
|
||||
await VAT.LoadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddToCart()
|
||||
{
|
||||
@@ -127,7 +146,9 @@ public partial class ProductDetail : ComponentBase, IDisposable
|
||||
Cart.OnChange -= HandleCartChanged;
|
||||
}
|
||||
|
||||
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
|
||||
private string FormatPrice(long price) => $"{VAT.AddVAT(price):N0} تومان";
|
||||
|
||||
private string FormatPriceWithoutVAT(long price) => $"{price:N0} تومان";
|
||||
|
||||
private static IReadOnlyList<ProductGalleryImage> BuildGalleryItems(Product product)
|
||||
{
|
||||
|
||||
@@ -59,8 +59,20 @@
|
||||
|
||||
|
||||
<MudCardContent Class="d-flex flex-column pa-1 h-100">
|
||||
<div style="height: 60%;background-image: url('@p.ImageUrl');background-size: cover; background-position: center;border-radius: 0.5rem">
|
||||
|
||||
<div style="height: 60%;background-image: url('@p.ImageUrl');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>
|
||||
}
|
||||
</div>
|
||||
<div class="pa-1 flex-grow-1 d-flex flex-column justify-space-between">
|
||||
<MudText Typo="Typo.subtitle1">@p.Title</MudText>
|
||||
@@ -73,7 +85,9 @@
|
||||
@* Href="@($"{RouteConstants.Store.ProductDetail}{p.Id}")">جزئیات *@
|
||||
@* </MudButton> *@
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="() => AddToCart(p)"
|
||||
StartIcon="@Icons.Material.Filled.AddShoppingCart">افزودن
|
||||
StartIcon="@Icons.Material.Filled.AddShoppingCart"
|
||||
Disabled="@(p.RemainingCount <= 0)">
|
||||
@(p.RemainingCount <= 0 ? "ناموجود" : "افزودن")
|
||||
</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
|
||||
@@ -12,6 +12,7 @@ public partial class Products : ComponentBase, IDisposable
|
||||
[Inject] private ProductService ProductService { get; set; } = default!;
|
||||
[Inject] private CategoryService CategoryService { get; set; } = default!;
|
||||
[Inject] private CartService Cart { get; set; } = default!;
|
||||
[Inject] private VATService VAT { get; set; } = default!;
|
||||
|
||||
private string _query = string.Empty;
|
||||
private bool _loading;
|
||||
@@ -21,11 +22,23 @@ public partial class Products : ComponentBase, IDisposable
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
|
||||
await Cart.EnsureInitializedAsync();
|
||||
Cart.OnChange += StateHasChanged;
|
||||
Navigation.LocationChanged += HandleLocationChanged;
|
||||
await Load();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
// بارگذاری نرخ VAT
|
||||
await VAT.LoadAsync();
|
||||
}
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
private async Task Load()
|
||||
{
|
||||
_loading = true;
|
||||
@@ -47,7 +60,7 @@ public partial class Products : ComponentBase, IDisposable
|
||||
await Cart.Add(p, 1);
|
||||
}
|
||||
|
||||
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
|
||||
private string FormatPrice(long price) => $"{VAT.AddVAT(price):N0} تومان";
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
|
||||
<!-- Ensure latest MudBlazor CSS is used (cache-busting) -->
|
||||
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" asp-append-version="true" />
|
||||
<!-- d3-org-chart custom styles -->
|
||||
<link href="css/org-chart.css" rel="stylesheet" asp-append-version="true" />
|
||||
</head>
|
||||
<body>
|
||||
<component type="typeof(App)" render-mode="Server" />
|
||||
@@ -34,6 +36,13 @@
|
||||
|
||||
<!-- Load MudBlazor JS before Blazor to avoid early JS interop calls failing; add cache-busting -->
|
||||
<script src="_content/MudBlazor/MudBlazor.min.js" asp-append-version="true"></script>
|
||||
|
||||
<!-- d3.js and d3-org-chart for network tree visualization -->
|
||||
<script src="js/d3.v7.min.js"></script>
|
||||
<script src="js/d3-flextree.min.js"></script>
|
||||
<script src="js/d3-org-chart3.js"></script>
|
||||
<script src="js/org-chart.js" asp-append-version="true"></script>
|
||||
|
||||
<script src="_framework/blazor.server.js"></script>
|
||||
<script>
|
||||
window.fetchAndDownloadPdf = async function(html, fileName){
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
@using FrontOffice.BFF.ClubMembership.Protobuf.Protos.ClubMembership
|
||||
@using Blazored.LocalStorage
|
||||
@using FrontOffice.Main.Utilities
|
||||
@using MudBlazor
|
||||
@inject ClubMembershipContract.ClubMembershipContractClient ClubMembershipClient
|
||||
@inject ILocalStorageService LocalStorage
|
||||
@inject AuthService AuthService
|
||||
@inject ISnackbar Snackbar
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
<MudDialog DisableSidePadding="true">
|
||||
<DialogContent>
|
||||
<MudContainer Style="max-height: 80vh; overflow-y: auto;" Class="pa-4">
|
||||
@if (_step == ContractStep.ReadContract)
|
||||
{
|
||||
<MudText Typo="Typo.h5" Class="mb-4" Align="Align.Center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Article" Class="ml-2" />
|
||||
قرارداد باشگاه مشتریان
|
||||
</MudText>
|
||||
|
||||
<MudAlert Severity="Severity.Info" Class="mb-4">
|
||||
لطفاً قرارداد باشگاه مشتریان را به دقت مطالعه کنید و در صورت موافقت تایید نمایید.
|
||||
</MudAlert>
|
||||
|
||||
<MudPaper Class="pa-4 mb-4" Style="max-height: 400px; overflow-y: auto;">
|
||||
@((MarkupString)GetClubContractHtml())
|
||||
</MudPaper>
|
||||
|
||||
<MudCheckBox @bind-Value="_acceptedTerms" Label="قوانین و مقررات باشگاه مشتریان را مطالعه کردم و میپذیرم" Color="Color.Primary" />
|
||||
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
FullWidth="true"
|
||||
Class="mt-4"
|
||||
Disabled="@(!_acceptedTerms || _isLoading)"
|
||||
OnClick="RequestOtp">
|
||||
@if (_isLoading)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="ml-2" />
|
||||
}
|
||||
ادامه و دریافت کد تایید
|
||||
</MudButton>
|
||||
}
|
||||
else if (_step == ContractStep.EnterOtp)
|
||||
{
|
||||
<MudText Typo="Typo.h5" Class="mb-4" Align="Align.Center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Sms" Class="ml-2" />
|
||||
تایید قرارداد
|
||||
</MudText>
|
||||
|
||||
<MudAlert Severity="Severity.Success" Class="mb-4">
|
||||
کد تایید به شماره موبایل شما ارسال شد. لطفاً کد را وارد کنید.
|
||||
</MudAlert>
|
||||
|
||||
<MudTextField @bind-Value="_otpCode"
|
||||
Label="کد تایید ۶ رقمی"
|
||||
Variant="Variant.Outlined"
|
||||
MaxLength="6"
|
||||
InputType="InputType.Number"
|
||||
Class="mb-4"
|
||||
Immediate="true" />
|
||||
|
||||
@if (_remainingSeconds > 0)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary" Class="mb-2">
|
||||
زمان باقیمانده: @_remainingSeconds ثانیه
|
||||
</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton Variant="Variant.Text" Color="Color.Primary" OnClick="RequestOtp" Disabled="_isLoading">
|
||||
ارسال مجدد کد
|
||||
</MudButton>
|
||||
}
|
||||
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
FullWidth="true"
|
||||
Class="mt-4"
|
||||
Disabled="@(string.IsNullOrEmpty(_otpCode) || _otpCode.Length != 6 || _isLoading)"
|
||||
OnClick="AcceptContract">
|
||||
@if (_isLoading)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="ml-2" />
|
||||
}
|
||||
تایید و فعالسازی عضویت
|
||||
</MudButton>
|
||||
}
|
||||
else if (_step == ContractStep.Success)
|
||||
{
|
||||
<div class="text-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Style="font-size: 80px" />
|
||||
<MudText Typo="Typo.h5" Color="Color.Success" Class="mt-4">
|
||||
عضویت شما در باشگاه مشتریان فعال شد!
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mt-2">
|
||||
اکنون میتوانید از لینک دعوت خود استفاده کنید و دوستانتان را دعوت کنید.
|
||||
</MudText>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
Class="mt-6"
|
||||
OnClick="CloseAndRefresh">
|
||||
متوجه شدم
|
||||
</MudButton>
|
||||
</div>
|
||||
}
|
||||
</MudContainer>
|
||||
</DialogContent>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] IMudDialogInstance MudDialog { get; set; }
|
||||
|
||||
private enum ContractStep { ReadContract, EnterOtp, Success }
|
||||
private ContractStep _step = ContractStep.ReadContract;
|
||||
|
||||
private bool _acceptedTerms;
|
||||
private string _otpCode = "";
|
||||
private bool _isLoading;
|
||||
private int _remainingSeconds;
|
||||
private readonly Guid _signGuid = Guid.NewGuid();
|
||||
private System.Timers.Timer? _timer;
|
||||
private const string TokenStorageKey = "auth:token";
|
||||
|
||||
// این Modal غیرقابل بسته شدن است - Options در ShowAsync تنظیم میشوند
|
||||
|
||||
private async Task RequestOtp()
|
||||
{
|
||||
_isLoading = true;
|
||||
try
|
||||
{
|
||||
var response = await ClubMembershipClient.RequestClubContractOtpAsync(
|
||||
new RequestClubContractOtpRequest
|
||||
{
|
||||
SignGuid = _signGuid.ToString()
|
||||
});
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
_step = ContractStep.EnterOtp;
|
||||
_remainingSeconds = response.RemainingSeconds > 0 ? response.RemainingSeconds : 120;
|
||||
StartCountdown();
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add(response.Message, Severity.Error);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ارسال کد: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AcceptContract()
|
||||
{
|
||||
_isLoading = true;
|
||||
try
|
||||
{
|
||||
var response = await ClubMembershipClient.AcceptClubMembershipContractAsync(
|
||||
new AcceptClubMembershipContractRequest
|
||||
{
|
||||
OtpCode = _otpCode,
|
||||
SignGuid = _signGuid.ToString(),
|
||||
ContractHtml = GetClubContractHtml()
|
||||
});
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
// ذخیره توکن جدید
|
||||
if (!string.IsNullOrEmpty(response.Token))
|
||||
{
|
||||
await LocalStorage.SetItemAsync(TokenStorageKey, response.Token);
|
||||
await AuthService.InitUserAuthInfo();
|
||||
}
|
||||
|
||||
_step = ContractStep.Success;
|
||||
StopCountdown();
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add(response.Message, Severity.Error);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ثبت قرارداد: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseAndRefresh()
|
||||
{
|
||||
MudDialog.Close(DialogResult.Ok(true));
|
||||
Navigation.NavigateTo(Navigation.Uri, forceLoad: true);
|
||||
}
|
||||
|
||||
private void StartCountdown()
|
||||
{
|
||||
_timer = new System.Timers.Timer(1000);
|
||||
_timer.Elapsed += (s, e) =>
|
||||
{
|
||||
_remainingSeconds--;
|
||||
if (_remainingSeconds <= 0)
|
||||
{
|
||||
StopCountdown();
|
||||
}
|
||||
InvokeAsync(StateHasChanged);
|
||||
};
|
||||
_timer.Start();
|
||||
}
|
||||
|
||||
private void StopCountdown()
|
||||
{
|
||||
_timer?.Stop();
|
||||
_timer?.Dispose();
|
||||
_timer = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
StopCountdown();
|
||||
}
|
||||
|
||||
private string GetClubContractHtml()
|
||||
{
|
||||
return @"
|
||||
<div style='direction: rtl; font-family: Tahoma, sans-serif; line-height: 2; text-align: justify;'>
|
||||
<h2 style='text-align: center; margin-bottom: 20px;'>قرارداد عضویت در باشگاه مشتریان کارابازار</h2>
|
||||
|
||||
<p><strong>ماده ۱ - تعاریف</strong></p>
|
||||
<p>۱-۱. باشگاه مشتریان: سامانهای که امکان معرفی دوستان و دریافت پاداش را فراهم میکند.</p>
|
||||
<p>۱-۲. لینک دعوت: لینک اختصاصی هر کاربر برای معرفی دیگران.</p>
|
||||
<p>۱-۳. پاداش معرفی: مبلغی که به ازای هر معرفی موفق به کاربر تعلق میگیرد.</p>
|
||||
|
||||
<p><strong>ماده ۲ - شرایط عضویت</strong></p>
|
||||
<p>۲-۱. کاربر با پرداخت مبلغ ۵۶ میلیون تومان (پکیج پایه)، امکان عضویت در باشگاه مشتریان را خواهد داشت.</p>
|
||||
<p>۲-۲. پس از امضای این قرارداد، لینک دعوت کاربر فعال خواهد شد.</p>
|
||||
<p>۲-۳. کاربر میتواند حداکثر ۲ نفر را مستقیماً دعوت کند (یک نفر در شاخه چپ و یک نفر در شاخه راست).</p>
|
||||
|
||||
<p><strong>ماده ۳ - تعهدات کاربر</strong></p>
|
||||
<p>۳-۱. کاربر متعهد است از لینک دعوت فقط برای معرفی افراد واقعی استفاده کند.</p>
|
||||
<p>۳-۲. هرگونه سوءاستفاده از سیستم منجر به لغو عضویت خواهد شد.</p>
|
||||
<p>۳-۳. کاربر حق انتقال یا فروش عضویت خود را ندارد.</p>
|
||||
|
||||
<p><strong>ماده ۴ - حقوق کاربر</strong></p>
|
||||
<p>۴-۱. دریافت پاداش معرفی به ازای هر عضو جدید که از طریق شبکه کاربر عضو شود.</p>
|
||||
<p>۴-۲. دسترسی به داشبورد مدیریت شبکه و مشاهده وضعیت زیرمجموعهها.</p>
|
||||
<p>۴-۳. امکان برداشت موجودی کیف پول طبق قوانین سایت.</p>
|
||||
|
||||
<p><strong>ماده ۵ - شرایط فسخ</strong></p>
|
||||
<p>۵-۱. در صورت نقض هر یک از مفاد این قرارداد، کارابازار حق لغو عضویت کاربر را دارد.</p>
|
||||
<p>۵-۲. در صورت فسخ قرارداد، موجودی کیف پول طبق قوانین سایت تسویه خواهد شد.</p>
|
||||
|
||||
<p><strong>ماده ۶ - حل اختلاف</strong></p>
|
||||
<p>۶-۱. در صورت بروز اختلاف، طرفین ابتدا از طریق مذاکره اقدام به حل و فصل خواهند کرد.</p>
|
||||
<p>۶-۲. در صورت عدم توافق، مراجع قضایی صلاحیتدار رسیدگی خواهند کرد.</p>
|
||||
|
||||
<p style='text-align: center; margin-top: 30px;'><strong>شناسه قرارداد: " + _signGuid + @"</strong></p>
|
||||
</div>";
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,11 @@
|
||||
<div class="d-flex align-center gap-2">
|
||||
@if (_isAuthenticated)
|
||||
{
|
||||
@* <MudBadge Content="@_cartCount" Color="Color.Error" Overlap="true" Visible="@(_cartCount > 0)"> *@
|
||||
@* *@
|
||||
@* </MudBadge> *@
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ShoppingCart" Color="@(_cartCount > 0?Color.Success:Color.Inherit)"
|
||||
Href="@(RouteConstants.Store.Cart)" />
|
||||
<MudMenu Icon="@Icons.Material.Filled.Person" Color="Color.Inherit" Size="Size.Medium">
|
||||
<MudMenuItem OnClick="NavigateToProfile" Disabled="@(!AuthService.IsCompleteRegister())">
|
||||
پروفایل
|
||||
|
||||
@@ -6,7 +6,7 @@ using MudBlazor;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
|
||||
namespace FrontOffice.Main.Shared;
|
||||
public partial class MainLayout
|
||||
public partial class MainLayout : IDisposable
|
||||
{
|
||||
private const string TokenStorageKey = "auth:token";
|
||||
|
||||
@@ -15,10 +15,12 @@ public partial class MainLayout
|
||||
private bool _drawerOpen;
|
||||
private bool _isAuthenticated;
|
||||
private string? _email;
|
||||
private int _cartCount;
|
||||
|
||||
[Inject] private ILocalStorageService LocalStorage { get; set; } = default!;
|
||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||
[Inject] private AuthDialogService AuthDialogService { get; set; } = default!;
|
||||
[Inject] private CartService CartService { get; set; } = default!;
|
||||
|
||||
private void ToggleTheme() => _isDark = !_isDark;
|
||||
private void ToggleDrawer() => _drawerOpen = !_drawerOpen;
|
||||
@@ -33,10 +35,25 @@ public partial class MainLayout
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("changeNavBgOnBodyScroll", "top", null, 1);
|
||||
await CheckAuthStatus();
|
||||
|
||||
if (_isAuthenticated)
|
||||
{
|
||||
// لود سبد خرید فقط برای کاربر لاگین شده
|
||||
await CartService.EnsureInitializedAsync();
|
||||
CartService.OnChange += OnCartChanged;
|
||||
_cartCount = CartService.Count;
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCartChanged()
|
||||
{
|
||||
_cartCount = CartService.Count;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task CheckAuthStatus()
|
||||
{
|
||||
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
|
||||
@@ -64,6 +81,12 @@ public partial class MainLayout
|
||||
{
|
||||
await AuthService.LogoutAsync();
|
||||
_isAuthenticated = false;
|
||||
_cartCount = 0;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
CartService.OnChange -= OnCartChanged;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
@using FrontOffice.Main.Utilities
|
||||
@using MudBlazor
|
||||
|
||||
<MudAutocomplete T="WeekDefinitionDto"
|
||||
@bind-Value="_selectedWeek"
|
||||
Label="@Label"
|
||||
Placeholder="@Placeholder"
|
||||
SearchFunc="SearchWeeksAsync"
|
||||
ToStringFunc="@(w => w?.DropdownDisplay ?? string.Empty)"
|
||||
Variant="@Variant"
|
||||
Dense="@Dense"
|
||||
Margin="@Margin"
|
||||
Clearable="@Clearable"
|
||||
ShowProgressIndicator="true"
|
||||
ProgressIndicatorColor="Color.Primary"
|
||||
AdornmentIcon="@Icons.Material.Filled.CalendarMonth"
|
||||
AdornmentColor="Color.Primary"
|
||||
Class="@Class"
|
||||
Style="@Style">
|
||||
<ItemTemplate>
|
||||
<MudStack Spacing="0" Class="py-1">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.body1">
|
||||
@context.DisplayName
|
||||
@if (context.IsCurrentWeek)
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Primary" Class="ms-2">هفته جاری</MudChip>
|
||||
}
|
||||
</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">@context.PersianYear</MudText>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">@context.DateRangeDisplay</MudText>
|
||||
</MudStack>
|
||||
</ItemTemplate>
|
||||
<NoItemsTemplate>
|
||||
<MudText Typo="Typo.body2" Class="pa-3">هفتهای یافت نشد</MudText>
|
||||
</NoItemsTemplate>
|
||||
</MudAutocomplete>
|
||||
@@ -0,0 +1,210 @@
|
||||
using FrontOffice.Main.Utilities;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace FrontOffice.Main.Shared;
|
||||
|
||||
public partial class WeekSelector : ComponentBase
|
||||
{
|
||||
[Inject] private CommissionService CommissionService { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Selected week definition
|
||||
/// </summary>
|
||||
[Parameter] public WeekDefinitionDto? Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Callback when week selection changes
|
||||
/// </summary>
|
||||
[Parameter] public EventCallback<WeekDefinitionDto?> ValueChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Label for the input
|
||||
/// </summary>
|
||||
[Parameter] public string Label { get; set; } = "انتخاب هفته";
|
||||
|
||||
/// <summary>
|
||||
/// Placeholder text
|
||||
/// </summary>
|
||||
[Parameter] public string Placeholder { get; set; } = "هفته را انتخاب کنید...";
|
||||
|
||||
/// <summary>
|
||||
/// Input variant
|
||||
/// </summary>
|
||||
[Parameter] public Variant Variant { get; set; } = Variant.Outlined;
|
||||
|
||||
/// <summary>
|
||||
/// Dense mode
|
||||
/// </summary>
|
||||
[Parameter] public bool Dense { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Input margin
|
||||
/// </summary>
|
||||
[Parameter] public Margin Margin { get; set; } = Margin.Normal;
|
||||
|
||||
/// <summary>
|
||||
/// Allow clearing the selection
|
||||
/// </summary>
|
||||
[Parameter] public bool Clearable { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// CSS class
|
||||
/// </summary>
|
||||
[Parameter] public string? Class { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Inline style
|
||||
/// </summary>
|
||||
[Parameter] public string? Style { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Persian year filter
|
||||
/// </summary>
|
||||
[Parameter] public int? PersianYear { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gregorian year filter
|
||||
/// </summary>
|
||||
[Parameter] public int? GregorianYear { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Only show active weeks
|
||||
/// </summary>
|
||||
[Parameter] public bool? OnlyActive { get; set; } = true;
|
||||
|
||||
private WeekDefinitionDto? _selectedWeek
|
||||
{
|
||||
get => Value;
|
||||
set
|
||||
{
|
||||
if (Value != value)
|
||||
{
|
||||
Value = value;
|
||||
ValueChanged.InvokeAsync(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
private List<WeekDefinitionDto> _cachedWeeks = new();
|
||||
private bool _isLoading = false;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Pre-load weeks
|
||||
await LoadWeeksAsync();
|
||||
}
|
||||
|
||||
private async Task LoadWeeksAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_isLoading = true;
|
||||
_cachedWeeks = await CommissionService.GetWeekDefinitionsAsync(
|
||||
searchText: null,
|
||||
gregorianYear: GregorianYear,
|
||||
persianYear: PersianYear,
|
||||
isActive: OnlyActive,
|
||||
pageSize: 100
|
||||
);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_cachedWeeks = new List<WeekDefinitionDto>();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<WeekDefinitionDto>> SearchWeeksAsync(string searchText, CancellationToken cancellationToken)
|
||||
{
|
||||
// If cached weeks are empty, load them
|
||||
if (!_cachedWeeks.Any())
|
||||
{
|
||||
await LoadWeeksAsync();
|
||||
}
|
||||
|
||||
// Filter locally from cache
|
||||
if (string.IsNullOrWhiteSpace(searchText))
|
||||
return _cachedWeeks;
|
||||
|
||||
return _cachedWeeks.Where(w =>
|
||||
w.DisplayName.Contains(searchText, StringComparison.OrdinalIgnoreCase) ||
|
||||
w.StartDatePersian.Contains(searchText, StringComparison.OrdinalIgnoreCase) ||
|
||||
w.EndDatePersian.Contains(searchText, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private async Task OnWeekChanged(WeekDefinitionDto? week)
|
||||
{
|
||||
_selectedWeek = week;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current week from cached weeks
|
||||
/// </summary>
|
||||
public WeekDefinitionDto? GetCurrentWeek()
|
||||
{
|
||||
return _cachedWeeks.FirstOrDefault(w => w.IsCurrentWeek);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Select current week
|
||||
/// </summary>
|
||||
public async Task SelectCurrentWeekAsync()
|
||||
{
|
||||
var currentWeek = GetCurrentWeek();
|
||||
if (currentWeek != null)
|
||||
{
|
||||
_selectedWeek = currentWeek;
|
||||
StateHasChanged();
|
||||
}
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find week by WeekOrder number (e.g., 46 from ?week=46)
|
||||
/// </summary>
|
||||
public WeekDefinitionDto? FindByWeekOrder(int weekOrder)
|
||||
{
|
||||
return _cachedWeeks.FirstOrDefault(w => w.WeekOrder == weekOrder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find week by Id
|
||||
/// </summary>
|
||||
public WeekDefinitionDto? FindById(long id)
|
||||
{
|
||||
return _cachedWeeks.FirstOrDefault(w => w.Id == id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Select week by WeekOrder
|
||||
/// </summary>
|
||||
public async Task SelectByWeekOrderAsync(int weekOrder)
|
||||
{
|
||||
var week = FindByWeekOrder(weekOrder);
|
||||
if (week != null)
|
||||
{
|
||||
_selectedWeek = week;
|
||||
StateHasChanged();
|
||||
}
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure weeks are loaded (useful for parent components)
|
||||
/// </summary>
|
||||
public async Task EnsureLoadedAsync()
|
||||
{
|
||||
if (!_cachedWeeks.Any())
|
||||
{
|
||||
await LoadWeeksAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all cached weeks
|
||||
/// </summary>
|
||||
public IReadOnlyList<WeekDefinitionDto> GetCachedWeeks() => _cachedWeeks.AsReadOnly();
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Blazored.LocalStorage;
|
||||
using FrontOffice.BFF.User.Protobuf.Protos.User;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
@@ -11,15 +12,25 @@ public class AuthService
|
||||
private readonly NavigationManager _navigation;
|
||||
private readonly ISnackbar _snackbar;
|
||||
private readonly UserAuthInfo _userAuthInfo;
|
||||
private readonly UserContract.UserContractClient _userContract;
|
||||
private readonly ILogger<AuthService> _logger;
|
||||
|
||||
private const string TokenStorageKey = "auth:token";
|
||||
|
||||
public AuthService(ILocalStorageService localStorage, NavigationManager navigation, ISnackbar snackbar, UserAuthInfo userAuthInfo)
|
||||
public AuthService(
|
||||
ILocalStorageService localStorage,
|
||||
NavigationManager navigation,
|
||||
ISnackbar snackbar,
|
||||
UserAuthInfo userAuthInfo,
|
||||
UserContract.UserContractClient userContract,
|
||||
ILogger<AuthService> logger)
|
||||
{
|
||||
_localStorage = localStorage;
|
||||
_navigation = navigation;
|
||||
_snackbar = snackbar;
|
||||
_userAuthInfo = userAuthInfo;
|
||||
_userContract = userContract;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<bool> IsAuthenticatedAsync()
|
||||
@@ -99,6 +110,51 @@ public class AuthService
|
||||
_navigation.NavigateTo(RouteConstants.Main.MainPage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refresh the user's token by calling the BFF RefreshToken API.
|
||||
/// If successful, the new token is stored and user info is updated.
|
||||
/// </summary>
|
||||
/// <returns>True if token was refreshed successfully</returns>
|
||||
public async Task<bool> RefreshTokenAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var currentToken = await GetTokenAsync();
|
||||
if (string.IsNullOrEmpty(currentToken))
|
||||
{
|
||||
_logger.LogWarning("No token found for refresh");
|
||||
return false;
|
||||
}
|
||||
|
||||
var response = await _userContract.RefreshTokenAsync(new RefreshTokenRequest
|
||||
{
|
||||
CurrentToken = currentToken
|
||||
});
|
||||
|
||||
if (response.Success && !string.IsNullOrEmpty(response.Token))
|
||||
{
|
||||
// Store the new token
|
||||
await _localStorage.SetItemAsync(TokenStorageKey, response.Token);
|
||||
|
||||
// Update user auth info with new token claims
|
||||
await InitUserAuthInfo();
|
||||
|
||||
_logger.LogInformation("Token refreshed successfully");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Token refresh not needed: {Message}", response.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to refresh token");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RequireAuthenticationAsync()
|
||||
{
|
||||
var isAuthenticated = await IsAuthenticatedAsync();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using DateTimeConverterCL;
|
||||
using FrontOffice.BFF.ShopingCart.Protobuf.Protos.ShopingCart;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Blazored.LocalStorage;
|
||||
|
||||
namespace FrontOffice.Main.Utilities;
|
||||
|
||||
@@ -16,13 +17,18 @@ public record CartItem(long cartId,long ProductId, string Title, string ImageUrl
|
||||
public class CartService
|
||||
{
|
||||
private readonly ShopingCartContract.ShopingCartContractClient _client;
|
||||
private readonly ILocalStorageService _localStorage;
|
||||
private readonly List<CartItem> _items = new();
|
||||
private const string TokenStorageKey = "auth:token";
|
||||
private bool _isInitialized;
|
||||
|
||||
public event Action? OnChange;
|
||||
|
||||
public CartService(ShopingCartContract.ShopingCartContractClient client)
|
||||
public CartService(ShopingCartContract.ShopingCartContractClient client, ILocalStorageService localStorage)
|
||||
{
|
||||
_client = client;
|
||||
_ = LoadFromServerAsync();
|
||||
_localStorage = localStorage;
|
||||
// لود سبد خرید به صورت lazy انجام میشه - نه در constructor
|
||||
}
|
||||
|
||||
public IReadOnlyList<CartItem> Items => _items.AsReadOnly();
|
||||
@@ -33,6 +39,13 @@ public class CartService
|
||||
public async Task Add(Product product, int quantity = 1)
|
||||
{
|
||||
if (quantity <= 0) return;
|
||||
|
||||
// اطمینان از لود شدن سبد خرید
|
||||
await EnsureInitializedAsync();
|
||||
|
||||
// چک لاگین بودن کاربر
|
||||
if (!await IsAuthenticatedAsync()) return;
|
||||
|
||||
var existing = _items.FirstOrDefault(i => i.ProductId == product.Id);
|
||||
int newQuantity;
|
||||
|
||||
@@ -82,11 +95,14 @@ public class CartService
|
||||
|
||||
public async Task UpdateQuantity(long productId, int quantity)
|
||||
{
|
||||
// چک لاگین بودن کاربر
|
||||
if (!await IsAuthenticatedAsync()) return;
|
||||
|
||||
var existing = _items.FirstOrDefault(i => i.ProductId == productId);
|
||||
if (existing is null) return;
|
||||
if (quantity <= 0)
|
||||
{
|
||||
Remove(existing.cartId);
|
||||
await Remove(existing.cartId);
|
||||
return;
|
||||
}
|
||||
var idx = _items.IndexOf(existing);
|
||||
@@ -109,6 +125,9 @@ public class CartService
|
||||
|
||||
public async Task Remove(long cartId)
|
||||
{
|
||||
// چک لاگین بودن کاربر
|
||||
if (!await IsAuthenticatedAsync()) return;
|
||||
|
||||
_items.RemoveAll(i => i.cartId == cartId);
|
||||
Notify();
|
||||
|
||||
@@ -129,6 +148,9 @@ public class CartService
|
||||
|
||||
public async Task Clear()
|
||||
{
|
||||
// چک لاگین بودن کاربر
|
||||
if (!await IsAuthenticatedAsync()) return;
|
||||
|
||||
var productIds = _items.Select(i => i.ProductId).ToList();
|
||||
_items.Clear();
|
||||
Notify();
|
||||
@@ -152,8 +174,44 @@ public class CartService
|
||||
|
||||
private void Notify() => OnChange?.Invoke();
|
||||
|
||||
/// <summary>
|
||||
/// بررسی اینکه کاربر لاگین کرده یا نه
|
||||
/// </summary>
|
||||
private async Task<bool> IsAuthenticatedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var token = await _localStorage.GetItemAsync<string>(TokenStorageKey);
|
||||
return !string.IsNullOrWhiteSpace(token);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// اطمینان از لود شدن سبد خرید - فقط اگر کاربر لاگین کرده باشد
|
||||
/// </summary>
|
||||
public async Task EnsureInitializedAsync()
|
||||
{
|
||||
if (_isInitialized) return;
|
||||
|
||||
if (await IsAuthenticatedAsync())
|
||||
{
|
||||
await LoadFromServerAsync();
|
||||
}
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
private async Task LoadFromServerAsync()
|
||||
{
|
||||
// ابتدا چک کن که کاربر لاگین کرده
|
||||
if (!await IsAuthenticatedAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _client.GetAllUserCartAsync(new Empty());
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
using FrontOffice.BFF.Configuration.Protobuf.Protos.Configuration;
|
||||
|
||||
namespace FrontOffice.Main.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// سرویس تنظیمات باشگاه مشتریان
|
||||
/// </summary>
|
||||
public class ClubConfigurationService
|
||||
{
|
||||
private readonly ConfigurationContract.ConfigurationContractClient _client;
|
||||
|
||||
public ClubConfigurationService(ConfigurationContract.ConfigurationContractClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت تنظیمات باشگاه مشتریان
|
||||
/// </summary>
|
||||
public async Task<ClubConfigDto> GetClubConfigurationAsync()
|
||||
{
|
||||
var response = await _client.GetClubConfigurationAsync(new Google.Protobuf.WellKnownTypes.Empty());
|
||||
|
||||
return new ClubConfigDto
|
||||
{
|
||||
ActivationFee = response.ActivationFee,
|
||||
MembershipGiftValue = response.MembershipGiftValue
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت لیست فیچرهای باشگاه برای کاربر جاری
|
||||
/// </summary>
|
||||
public async Task<List<ClubFeatureDto>> GetClubFeaturesAsync()
|
||||
{
|
||||
var response = await _client.GetClubFeaturesAsync(new Google.Protobuf.WellKnownTypes.Empty());
|
||||
|
||||
return response.Features.Select(f => new ClubFeatureDto
|
||||
{
|
||||
Id = f.Id,
|
||||
Title = f.Title,
|
||||
Description = f.Description,
|
||||
IsEnabled = f.IsEnabled,
|
||||
DisplayOrder = f.DisplayOrder,
|
||||
GrantedAt = f.GrantedAt?.ToDateTime(),
|
||||
CreatedAt = f.CreatedAt?.ToDateTime(),
|
||||
Notes = f.Notes
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public class ClubConfigDto
|
||||
{
|
||||
public long ActivationFee { get; set; }
|
||||
public long MembershipGiftValue { get; set; }
|
||||
}
|
||||
|
||||
public class ClubFeatureDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public bool IsEnabled { get; set; }
|
||||
public int DisplayOrder { get; set; }
|
||||
public DateTime? GrantedAt { get; set; }
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using DateTimeConverterCL;
|
||||
|
||||
namespace FrontOffice.Main.Utilities;
|
||||
|
||||
/// <summary>
|
||||
@@ -6,8 +8,8 @@ namespace FrontOffice.Main.Utilities;
|
||||
public class CommissionPayoutDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public int WeekNumber { get; set; }
|
||||
public string WeekLabel { get; set; } = string.Empty;
|
||||
public long WeekDefinitionId { get; set; }
|
||||
public string WeekDisplayName { get; set; } = string.Empty;
|
||||
public int BalancesEarned { get; set; }
|
||||
public long TotalAmount { get; set; }
|
||||
public string AmountFormatted { get; set; } = string.Empty;
|
||||
@@ -32,8 +34,8 @@ public class CommissionPayoutsResponseDto
|
||||
/// </summary>
|
||||
public class WeeklyBalanceDto
|
||||
{
|
||||
public int WeekNumber { get; set; }
|
||||
public string WeekLabel { get; set; } = string.Empty;
|
||||
public long WeekDefinitionId { get; set; }
|
||||
public string WeekDisplayName { get; set; } = string.Empty;
|
||||
public long LeftBalance { get; set; }
|
||||
public long RightBalance { get; set; }
|
||||
public long MinBalance { get; set; }
|
||||
@@ -45,12 +47,37 @@ public class WeeklyBalanceDto
|
||||
public DateTime EndDate { get; set; }
|
||||
|
||||
// Formatted properties
|
||||
public string LeftBalanceFormatted => $"{LeftBalance:N0} تومان";
|
||||
public string RightBalanceFormatted => $"{RightBalance:N0} تومان";
|
||||
public string MinBalanceFormatted => $"{MinBalance:N0} تومان";
|
||||
public string CalculatedCommissionFormatted => $"{CalculatedCommission:N0} تومان";
|
||||
public string LeftCarryoverFormatted => $"{LeftCarryover:N0} تومان";
|
||||
public string RightCarryoverFormatted => $"{RightCarryover:N0} تومان";
|
||||
public string StartDatePersian => StartDate.ToString("yyyy/MM/dd");
|
||||
public string EndDatePersian => EndDate.ToString("yyyy/MM/dd");
|
||||
public string LeftBalanceFormatted => $"{LeftBalance:N0} ";
|
||||
public string RightBalanceFormatted => $"{RightBalance:N0} ";
|
||||
public string MinBalanceFormatted => $"{MinBalance:N0} ";
|
||||
public string CalculatedCommissionFormatted => $"{CalculatedCommission:N0} ";
|
||||
public string LeftCarryoverFormatted => $"{LeftCarryover:N0} ";
|
||||
public string RightCarryoverFormatted => $"{RightCarryover:N0} ";
|
||||
public string StartDatePersian => StartDate.MiladiToJalali();
|
||||
public string EndDatePersian => EndDate.MiladiToJalali();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO for Week Definition (for dropdowns)
|
||||
/// </summary>
|
||||
public class WeekDefinitionDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public int WeekOrder { get; set; }
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public DateTime StartDate { get; set; }
|
||||
public DateTime EndDate { get; set; }
|
||||
public int GregorianYear { get; set; }
|
||||
public int PersianYear { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public bool IsCurrentWeek { get; set; }
|
||||
public string StartDatePersian { get; set; } = string.Empty;
|
||||
public string EndDatePersian { get; set; } = string.Empty;
|
||||
|
||||
// For display in dropdown
|
||||
public string DropdownDisplay => IsCurrentWeek
|
||||
? $"{DisplayName} (هفته جاری)"
|
||||
: DisplayName;
|
||||
|
||||
public string DateRangeDisplay => $"{StartDate.MiladiToJalali()} - {EndDate.MiladiToJalali()}";
|
||||
}
|
||||
|
||||
@@ -15,12 +15,67 @@ public class CommissionService
|
||||
_client = client;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get week definitions for dropdown
|
||||
/// Maps to: CommissionCQ.GetWeekDefinitions
|
||||
/// </summary>
|
||||
public async Task<List<WeekDefinitionDto>> GetWeekDefinitionsAsync(
|
||||
string? searchText = null,
|
||||
int? gregorianYear = null,
|
||||
int? persianYear = null,
|
||||
bool? isActive = null,
|
||||
int pageSize = 100)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new GetWeekDefinitionsRequest
|
||||
{
|
||||
PageNumber = 1,
|
||||
PageSize = pageSize
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchText))
|
||||
request.SearchText = searchText;
|
||||
|
||||
if (gregorianYear.HasValue)
|
||||
request.GregorianYear = gregorianYear.Value;
|
||||
|
||||
if (persianYear.HasValue)
|
||||
request.PersianYear = persianYear.Value;
|
||||
|
||||
if (isActive.HasValue)
|
||||
request.IsActive = isActive.Value;
|
||||
|
||||
var response = await _client.GetWeekDefinitionsAsync(request);
|
||||
|
||||
return response.Data.Select(w => new WeekDefinitionDto
|
||||
{
|
||||
Id = w.Id,
|
||||
WeekOrder = w.WeekOrder,
|
||||
DisplayName = w.DisplayName,
|
||||
StartDate = w.StartDate?.ToDateTime() ?? DateTime.MinValue,
|
||||
EndDate = w.EndDate?.ToDateTime() ?? DateTime.MinValue,
|
||||
GregorianYear = w.GregorianYear,
|
||||
PersianYear = w.PersianYear,
|
||||
IsActive = w.IsActive,
|
||||
IsCurrentWeek = w.IsCurrentWeek,
|
||||
StartDatePersian = w.StartDatePersian,
|
||||
EndDatePersian = w.EndDatePersian
|
||||
}).ToList();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback to mock data if backend is unavailable
|
||||
return GenerateMockWeekDefinitions();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get commission payouts with pagination and filters
|
||||
/// Maps to: CommissionCQ.GetMyCommissionPayouts
|
||||
/// </summary>
|
||||
public async Task<CommissionPayoutsResponseDto> GetMyCommissionPayoutsAsync(
|
||||
int? weekNumber,
|
||||
long? weekDefinitionId,
|
||||
string? status,
|
||||
int pageNumber,
|
||||
int pageSize)
|
||||
@@ -33,9 +88,9 @@ public class CommissionService
|
||||
PageSize = pageSize
|
||||
};
|
||||
|
||||
if (weekNumber.HasValue)
|
||||
if (weekDefinitionId.HasValue)
|
||||
{
|
||||
request.WeekNumber = $"2025-W{weekNumber:D2}";
|
||||
request.WeekDefinitionId = weekDefinitionId.Value;
|
||||
}
|
||||
|
||||
// Map status string to int
|
||||
@@ -58,8 +113,8 @@ public class CommissionService
|
||||
Payouts = response.Payouts.Select(p => new CommissionPayoutDto
|
||||
{
|
||||
Id = p.Id,
|
||||
WeekNumber = ExtractWeekNumber(p.WeekNumber),
|
||||
WeekLabel = p.WeekLabel,
|
||||
WeekDefinitionId = p.WeekDefinitionId,
|
||||
WeekDisplayName = p.WeekDisplayName,
|
||||
BalancesEarned = p.BalancesEarned,
|
||||
TotalAmount = p.TotalAmount,
|
||||
AmountFormatted = p.AmountFormatted,
|
||||
@@ -75,7 +130,7 @@ public class CommissionService
|
||||
catch
|
||||
{
|
||||
// Fallback to mock data if backend is unavailable
|
||||
return GenerateMockPayoutsResponse(weekNumber, status, pageNumber, pageSize);
|
||||
return GenerateMockPayoutsResponse(weekDefinitionId, status, pageNumber, pageSize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +138,7 @@ public class CommissionService
|
||||
/// Get weekly balance details for a specific week
|
||||
/// Maps to: CommissionCQ.GetMyWeeklyBalances
|
||||
/// </summary>
|
||||
public async Task<WeeklyBalanceDto> GetMyWeeklyBalanceAsync(int? weekNumber = null)
|
||||
public async Task<WeeklyBalanceDto> GetMyWeeklyBalanceAsync(long? weekDefinitionId = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -94,9 +149,9 @@ public class CommissionService
|
||||
OnlyActive = false
|
||||
};
|
||||
|
||||
if (weekNumber.HasValue)
|
||||
if (weekDefinitionId.HasValue)
|
||||
{
|
||||
request.WeekNumber = $"2025-W{weekNumber:D2}";
|
||||
request.WeekDefinitionId = weekDefinitionId.Value;
|
||||
}
|
||||
|
||||
var response = await _client.GetMyWeeklyBalancesAsync(request);
|
||||
@@ -106,8 +161,8 @@ public class CommissionService
|
||||
var balance = response.Balances[0];
|
||||
return new WeeklyBalanceDto
|
||||
{
|
||||
WeekNumber = ExtractWeekNumber(balance.WeekNumber),
|
||||
WeekLabel = $"هفته {ExtractWeekNumber(balance.WeekNumber)} - سال 1404",
|
||||
WeekDefinitionId = balance.WeekDefinitionId,
|
||||
WeekDisplayName = balance.WeekDisplayName,
|
||||
LeftBalance = balance.LeftLegBalances,
|
||||
RightBalance = balance.RightLegBalances,
|
||||
MinBalance = Math.Min(balance.LeftLegBalances, balance.RightLegBalances),
|
||||
@@ -120,41 +175,28 @@ public class CommissionService
|
||||
};
|
||||
}
|
||||
|
||||
return CreateMockWeeklyBalance(weekNumber ?? 45);
|
||||
return CreateMockWeeklyBalance();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return CreateMockWeeklyBalance(weekNumber ?? 45);
|
||||
return CreateMockWeeklyBalance();
|
||||
}
|
||||
}
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static int ExtractWeekNumber(string weekNumberStr)
|
||||
{
|
||||
// Format: "2025-W48" → 48
|
||||
if (string.IsNullOrEmpty(weekNumberStr)) return 0;
|
||||
|
||||
var parts = weekNumberStr.Split('W');
|
||||
if (parts.Length > 1 && int.TryParse(parts[1], out var week))
|
||||
{
|
||||
return week;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static WeeklyBalanceDto CreateMockWeeklyBalance(int weekNumber)
|
||||
private static WeeklyBalanceDto CreateMockWeeklyBalance()
|
||||
{
|
||||
return new WeeklyBalanceDto
|
||||
{
|
||||
WeekNumber = weekNumber,
|
||||
WeekLabel = $"هفته {weekNumber} - سال 1404",
|
||||
LeftBalance = 15_000_000,
|
||||
RightBalance = 12_000_000,
|
||||
MinBalance = 12_000_000,
|
||||
BalanceCount = 12,
|
||||
CalculatedCommission = 1_200_000,
|
||||
LeftCarryover = 3_000_000,
|
||||
WeekDefinitionId = 0,
|
||||
WeekDisplayName = "دادهای یافت نشد",
|
||||
LeftBalance = 0,
|
||||
RightBalance = 0,
|
||||
MinBalance = 0,
|
||||
BalanceCount = 0,
|
||||
CalculatedCommission = 0,
|
||||
LeftCarryover = 0,
|
||||
RightCarryover = 0,
|
||||
StartDate = DateTime.Now.AddDays(-7),
|
||||
EndDate = DateTime.Now
|
||||
@@ -162,13 +204,13 @@ public class CommissionService
|
||||
}
|
||||
|
||||
private static CommissionPayoutsResponseDto GenerateMockPayoutsResponse(
|
||||
int? weekNumber, string? status, int pageNumber, int pageSize)
|
||||
long? weekDefinitionId, string? status, int pageNumber, int pageSize)
|
||||
{
|
||||
var allPayouts = GenerateMockPayouts(50);
|
||||
|
||||
var filtered = allPayouts.AsEnumerable();
|
||||
if (weekNumber.HasValue)
|
||||
filtered = filtered.Where(p => p.WeekNumber == weekNumber.Value);
|
||||
if (weekDefinitionId.HasValue)
|
||||
filtered = filtered.Where(p => p.WeekDefinitionId == weekDefinitionId.Value);
|
||||
if (!string.IsNullOrEmpty(status))
|
||||
filtered = filtered.Where(p => p.Status == status);
|
||||
|
||||
@@ -201,8 +243,8 @@ public class CommissionService
|
||||
payouts.Add(new CommissionPayoutDto
|
||||
{
|
||||
Id = i + 1,
|
||||
WeekNumber = weekNum,
|
||||
WeekLabel = $"هفته {weekNum} - سال 1404",
|
||||
WeekDefinitionId = i + 1,
|
||||
WeekDisplayName = $"هفته {weekNum} - سال 1404",
|
||||
BalancesEarned = balances,
|
||||
TotalAmount = amount,
|
||||
AmountFormatted = $"{amount:N0} تومان",
|
||||
@@ -215,5 +257,35 @@ public class CommissionService
|
||||
return payouts;
|
||||
}
|
||||
|
||||
private static List<WeekDefinitionDto> GenerateMockWeekDefinitions()
|
||||
{
|
||||
var weeks = new List<WeekDefinitionDto>();
|
||||
var persianOrdinals = new[] { "یکم", "دوم", "سوم", "چهارم", "پنجم", "ششم", "هفتم", "هشتم", "نهم", "دهم" };
|
||||
|
||||
// Generate 10 weeks starting from week 46
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var weekOrder = i + 1;
|
||||
var startDate = new DateTime(2025, 11, 8).AddDays(i * 7);
|
||||
|
||||
weeks.Add(new WeekDefinitionDto
|
||||
{
|
||||
Id = i + 1,
|
||||
WeekOrder = weekOrder,
|
||||
DisplayName = weekOrder <= 10 ? $"هفته {persianOrdinals[weekOrder - 1]}" : $"هفته {weekOrder}",
|
||||
StartDate = startDate,
|
||||
EndDate = startDate.AddDays(6),
|
||||
GregorianYear = 2025,
|
||||
PersianYear = 1404,
|
||||
IsActive = true,
|
||||
IsCurrentWeek = weekOrder == 6, // Assume week 6 is current
|
||||
StartDatePersian = $"1404/{8 + i}/17",
|
||||
EndDatePersian = $"1404/{8 + i}/23"
|
||||
});
|
||||
}
|
||||
|
||||
return weeks;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -9,10 +9,32 @@ public class NetworkNodeDto
|
||||
public string FullName { get; set; } = string.Empty;
|
||||
public string Mobile { get; set; } = string.Empty;
|
||||
public string? Avatar { get; set; }
|
||||
public string Position { get; set; } = string.Empty; // "Left" or "Right"
|
||||
public string Position { get; set; } = string.Empty; // "Root", "Left" or "Right"
|
||||
public NetworkNodeDto? LeftChild { get; set; }
|
||||
public NetworkNodeDto? RightChild { get; set; }
|
||||
public int Level { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public DateTime? JoinedAt { get; set; }
|
||||
public bool IsClubActive { get; set; }
|
||||
public string? ActivationWeekNumber { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO for flat node structure (for d3-org-chart)
|
||||
/// </summary>
|
||||
public class FlatNetworkNodeDto
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string ParentId { get; set; } = string.Empty;
|
||||
public string FullName { get; set; } = string.Empty;
|
||||
public string Mobile { get; set; } = string.Empty;
|
||||
public string? Avatar { get; set; }
|
||||
public string Position { get; set; } = string.Empty;
|
||||
public int Level { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public bool IsClubActive { get; set; }
|
||||
public string? ActivationWeekNumber { get; set; }
|
||||
public DateTime? JoinedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -23,6 +45,48 @@ public class NetworkTreeDto
|
||||
public NetworkNodeDto? RootNode { get; set; }
|
||||
public int TotalMembers { get; set; }
|
||||
public int CurrentDepth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Convert hierarchical tree to flat array for d3-org-chart
|
||||
/// </summary>
|
||||
public List<FlatNetworkNodeDto> ToFlatArray()
|
||||
{
|
||||
var result = new List<FlatNetworkNodeDto>();
|
||||
if (RootNode == null) return result;
|
||||
|
||||
TraverseAndFlatten(RootNode, "", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void TraverseAndFlatten(NetworkNodeDto node, string parentId, List<FlatNetworkNodeDto> result)
|
||||
{
|
||||
var flatNode = new FlatNetworkNodeDto
|
||||
{
|
||||
Id = node.UserId.ToString(),
|
||||
ParentId = parentId,
|
||||
FullName = node.FullName,
|
||||
Mobile = node.Mobile,
|
||||
Avatar = node.Avatar,
|
||||
Position = node.Position,
|
||||
Level = node.Level,
|
||||
IsActive = node.IsActive,
|
||||
IsClubActive = node.IsClubActive,
|
||||
ActivationWeekNumber = node.ActivationWeekNumber,
|
||||
JoinedAt = node.JoinedAt
|
||||
};
|
||||
|
||||
result.Add(flatNode);
|
||||
|
||||
if (node.LeftChild != null)
|
||||
{
|
||||
TraverseAndFlatten(node.LeftChild, flatNode.Id, result);
|
||||
}
|
||||
|
||||
if (node.RightChild != null)
|
||||
{
|
||||
TraverseAndFlatten(node.RightChild, flatNode.Id, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -41,6 +41,39 @@ public class NetworkMembershipService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get subordinate's network tree (with security check on backend)
|
||||
/// Maps to: NetworkMembershipCQ.GetSubordinateTree
|
||||
/// </summary>
|
||||
public async Task<NetworkTreeDto> GetSubordinateTreeAsync(long targetUserId, int maxDepth = 3)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new GetSubordinateTreeRequest
|
||||
{
|
||||
TargetUserId = targetUserId,
|
||||
MaxDepth = maxDepth
|
||||
};
|
||||
var response = await _client.GetSubordinateTreeAsync(request);
|
||||
|
||||
return new NetworkTreeDto
|
||||
{
|
||||
RootNode = MapNodeFromProto(response.RootNode),
|
||||
TotalMembers = response.TotalMembers,
|
||||
CurrentDepth = response.CurrentDepth
|
||||
};
|
||||
}
|
||||
catch (Grpc.Core.RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.PermissionDenied)
|
||||
{
|
||||
throw new UnauthorizedAccessException("شما اجازه مشاهده درخت این کاربر را ندارید");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback to current user's tree
|
||||
return await GetMyNetworkTreeAsync(maxDepth);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current user's network statistics
|
||||
/// Maps to: NetworkMembershipCQ.GetMyNetworkStatistics
|
||||
@@ -102,6 +135,10 @@ public class NetworkMembershipService
|
||||
Avatar = node.Avatar,
|
||||
Position = node.Position,
|
||||
Level = node.Level,
|
||||
IsActive = node.HasChildren, // Temporary: use HasChildren as active indicator until proto is updated
|
||||
IsClubActive = false, // Will be populated when proto is updated
|
||||
ActivationWeekNumber = null, // Will be populated when proto is updated
|
||||
JoinedAt = null, // Will be populated when proto is updated
|
||||
LeftChild = MapNodeFromProto(node.LeftChild),
|
||||
RightChild = MapNodeFromProto(node.RightChild)
|
||||
};
|
||||
@@ -111,8 +148,8 @@ public class NetworkMembershipService
|
||||
{
|
||||
return new NetworkTreeDto
|
||||
{
|
||||
CurrentDepth = 2,
|
||||
TotalMembers = 5,
|
||||
CurrentDepth = 3,
|
||||
TotalMembers = 7,
|
||||
RootNode = new NetworkNodeDto
|
||||
{
|
||||
UserId = 1,
|
||||
@@ -120,13 +157,38 @@ public class NetworkMembershipService
|
||||
Mobile = "09121234567",
|
||||
Position = "Root",
|
||||
Level = 0,
|
||||
IsActive = true,
|
||||
IsClubActive = true,
|
||||
LeftChild = new NetworkNodeDto
|
||||
{
|
||||
UserId = 2,
|
||||
FullName = "علی محمدی",
|
||||
Mobile = "09121234568",
|
||||
Position = "Left",
|
||||
Level = 1
|
||||
Level = 1,
|
||||
IsActive = true,
|
||||
IsClubActive = true,
|
||||
JoinedAt = DateTime.Now.AddDays(-30),
|
||||
LeftChild = new NetworkNodeDto
|
||||
{
|
||||
UserId = 4,
|
||||
FullName = "رضا کریمی",
|
||||
Mobile = "09121234570",
|
||||
Position = "Left",
|
||||
Level = 2,
|
||||
IsActive = true,
|
||||
JoinedAt = DateTime.Now.AddDays(-15)
|
||||
},
|
||||
RightChild = new NetworkNodeDto
|
||||
{
|
||||
UserId = 5,
|
||||
FullName = "زهرا احمدی",
|
||||
Mobile = "09121234571",
|
||||
Position = "Right",
|
||||
Level = 2,
|
||||
IsActive = false,
|
||||
JoinedAt = DateTime.Now.AddDays(-10)
|
||||
}
|
||||
},
|
||||
RightChild = new NetworkNodeDto
|
||||
{
|
||||
@@ -134,7 +196,20 @@ public class NetworkMembershipService
|
||||
FullName = "فاطمه حسینی",
|
||||
Mobile = "09121234569",
|
||||
Position = "Right",
|
||||
Level = 1
|
||||
Level = 1,
|
||||
IsActive = true,
|
||||
JoinedAt = DateTime.Now.AddDays(-25),
|
||||
LeftChild = new NetworkNodeDto
|
||||
{
|
||||
UserId = 6,
|
||||
FullName = "محمد نوری",
|
||||
Mobile = "09121234572",
|
||||
Position = "Left",
|
||||
Level = 2,
|
||||
IsActive = true,
|
||||
IsClubActive = true,
|
||||
JoinedAt = DateTime.Now.AddDays(-5)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -79,4 +79,25 @@ public class OrderService
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت نرخ مالیات بر ارزش افزوده
|
||||
/// </summary>
|
||||
public async Task<GetVATRateResponse> GetVATRateAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _userOrderContractClient.GetVATRateAsync(new Google.Protobuf.WellKnownTypes.Empty());
|
||||
}
|
||||
catch
|
||||
{
|
||||
// در صورت خطا، مقادیر پیشفرض
|
||||
return new GetVATRateResponse
|
||||
{
|
||||
VatRate = 0.10,
|
||||
VatPercentage = 10,
|
||||
IsEnabled = true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ public static class RouteConstants
|
||||
public const string ChangePassword = "/profile/change-password";
|
||||
public const string Tree = "/profile/tree";
|
||||
public const string Wallet = "/profile/wallet";
|
||||
public const string WithdrawalRequests = "/profile/withdrawal-requests";
|
||||
}
|
||||
|
||||
public static class Club
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Blazored.LocalStorage;
|
||||
|
||||
namespace FrontOffice.Main.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing SignalR connection and handling token-related notifications from BFF.
|
||||
/// This service connects to the BFF SignalR Hub and notifies the application when token refresh is needed.
|
||||
/// </summary>
|
||||
public class TokenNotificationService : IAsyncDisposable
|
||||
{
|
||||
private readonly ILocalStorageService _localStorage;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<TokenNotificationService> _logger;
|
||||
private HubConnection? _hubConnection;
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when a token revoked notification is received from server
|
||||
/// </summary>
|
||||
public event Func<TokenRevokedEventArgs, Task>? OnTokenRevoked;
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when a force refresh token notification is received from server
|
||||
/// </summary>
|
||||
public event Func<ForceRefreshEventArgs, Task>? OnForceRefreshToken;
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when a broadcast message is received from server
|
||||
/// </summary>
|
||||
public event Func<BroadcastMessageEventArgs, Task>? OnBroadcastMessage;
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when connection state changes
|
||||
/// </summary>
|
||||
public event Action<HubConnectionState>? OnConnectionStateChanged;
|
||||
|
||||
public TokenNotificationService(
|
||||
ILocalStorageService localStorage,
|
||||
IConfiguration configuration,
|
||||
ILogger<TokenNotificationService> logger)
|
||||
{
|
||||
_localStorage = localStorage;
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current connection state
|
||||
/// </summary>
|
||||
public HubConnectionState ConnectionState => _hubConnection?.State ?? HubConnectionState.Disconnected;
|
||||
|
||||
/// <summary>
|
||||
/// Connect to the SignalR Hub with the user's authentication token
|
||||
/// </summary>
|
||||
public async Task ConnectAsync()
|
||||
{
|
||||
if (_hubConnection?.State == HubConnectionState.Connected)
|
||||
{
|
||||
_logger.LogWarning("Already connected to SignalR Hub");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var token = await _localStorage.GetItemAsync<string>("authToken");
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
_logger.LogWarning("No auth token found, cannot connect to SignalR Hub");
|
||||
return;
|
||||
}
|
||||
|
||||
var gwUrl = _configuration["GwUrl"]?.TrimEnd('/') ?? "https://localhost:5002";
|
||||
var hubPath = _configuration["SignalR:HubPath"] ?? "/hubs/token-relay";
|
||||
var hubUrl = $"{gwUrl}{hubPath}";
|
||||
|
||||
_logger.LogInformation("Connecting to SignalR Hub at {HubUrl}", hubUrl);
|
||||
|
||||
_hubConnection = new HubConnectionBuilder()
|
||||
.WithUrl(hubUrl, options =>
|
||||
{
|
||||
options.AccessTokenProvider = () => Task.FromResult<string?>(token);
|
||||
})
|
||||
.WithAutomaticReconnect(new[]
|
||||
{
|
||||
TimeSpan.FromSeconds(0),
|
||||
TimeSpan.FromSeconds(2),
|
||||
TimeSpan.FromSeconds(5),
|
||||
TimeSpan.FromSeconds(10),
|
||||
TimeSpan.FromSeconds(30)
|
||||
})
|
||||
.Build();
|
||||
|
||||
RegisterEventHandlers();
|
||||
|
||||
await _hubConnection.StartAsync();
|
||||
|
||||
_logger.LogInformation("Successfully connected to SignalR Hub");
|
||||
OnConnectionStateChanged?.Invoke(HubConnectionState.Connected);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to connect to SignalR Hub");
|
||||
OnConnectionStateChanged?.Invoke(HubConnectionState.Disconnected);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnect from the SignalR Hub
|
||||
/// </summary>
|
||||
public async Task DisconnectAsync()
|
||||
{
|
||||
if (_hubConnection != null)
|
||||
{
|
||||
await _hubConnection.StopAsync();
|
||||
_logger.LogInformation("Disconnected from SignalR Hub");
|
||||
OnConnectionStateChanged?.Invoke(HubConnectionState.Disconnected);
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterEventHandlers()
|
||||
{
|
||||
if (_hubConnection == null) return;
|
||||
|
||||
// Handle TokenRevoked event
|
||||
_hubConnection.On<TokenRevokedEventArgs>("TokenRevoked", async args =>
|
||||
{
|
||||
_logger.LogInformation("Received TokenRevoked notification. UserId: {UserId}, Reason: {Reason}",
|
||||
args.UserId, args.Reason);
|
||||
|
||||
if (OnTokenRevoked != null)
|
||||
{
|
||||
await OnTokenRevoked.Invoke(args);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle ForceRefreshToken event
|
||||
_hubConnection.On<ForceRefreshEventArgs>("ForceRefreshToken", async args =>
|
||||
{
|
||||
_logger.LogInformation("Received ForceRefreshToken notification. UserId: {UserId}", args.UserId);
|
||||
|
||||
if (OnForceRefreshToken != null)
|
||||
{
|
||||
await OnForceRefreshToken.Invoke(args);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle BroadcastMessage event
|
||||
_hubConnection.On<BroadcastMessageEventArgs>("BroadcastMessage", async args =>
|
||||
{
|
||||
_logger.LogInformation("Received BroadcastMessage: {Message}", args.Message);
|
||||
|
||||
if (OnBroadcastMessage != null)
|
||||
{
|
||||
await OnBroadcastMessage.Invoke(args);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle connection state changes
|
||||
_hubConnection.Reconnecting += error =>
|
||||
{
|
||||
_logger.LogWarning(error, "SignalR connection lost. Attempting to reconnect...");
|
||||
OnConnectionStateChanged?.Invoke(HubConnectionState.Reconnecting);
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
_hubConnection.Reconnected += connectionId =>
|
||||
{
|
||||
_logger.LogInformation("SignalR reconnected. ConnectionId: {ConnectionId}", connectionId);
|
||||
OnConnectionStateChanged?.Invoke(HubConnectionState.Connected);
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
_hubConnection.Closed += async error =>
|
||||
{
|
||||
_logger.LogWarning(error, "SignalR connection closed");
|
||||
OnConnectionStateChanged?.Invoke(HubConnectionState.Disconnected);
|
||||
await Task.CompletedTask;
|
||||
};
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_hubConnection != null)
|
||||
{
|
||||
await _hubConnection.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event arguments for token revoked notification
|
||||
/// </summary>
|
||||
public class TokenRevokedEventArgs
|
||||
{
|
||||
public long UserId { get; set; }
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
public DateTime Timestamp { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event arguments for force refresh token notification
|
||||
/// </summary>
|
||||
public class ForceRefreshEventArgs
|
||||
{
|
||||
public long UserId { get; set; }
|
||||
public DateTime Timestamp { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event arguments for broadcast message notification
|
||||
/// </summary>
|
||||
public class BroadcastMessageEventArgs
|
||||
{
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public DateTime Timestamp { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using Blazored.LocalStorage;
|
||||
using FrontOffice.BFF.UserOrder.Protobuf.Protos.UserOrder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace FrontOffice.Main.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// سرویس مدیریت مالیات بر ارزش افزوده
|
||||
/// نرخ VAT یک بار در روز از سرور گرفته و در LocalStorage ذخیره میشود
|
||||
/// </summary>
|
||||
public class VATService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILocalStorageService _localStorageService;
|
||||
|
||||
private const string VAT_RATE_KEY = "vat_rate";
|
||||
private const string VAT_PERCENTAGE_KEY = "vat_percentage";
|
||||
private const string VAT_DATE_KEY = "vat_date";
|
||||
|
||||
// مقادیر پیشفرض
|
||||
private double _vatRate = 0.0999;
|
||||
private int _vatPercentage = 99;
|
||||
private bool _isEnabled = true;
|
||||
private bool _isLoaded = false;
|
||||
|
||||
public VATService(IServiceProvider serviceProvider, ILocalStorageService localStorageService)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_localStorageService = localStorageService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// نرخ مالیات (مثلاً 0.09)
|
||||
/// </summary>
|
||||
public double VatRate => _vatRate;
|
||||
|
||||
/// <summary>
|
||||
/// درصد مالیات (مثلاً 9)
|
||||
/// </summary>
|
||||
public int VatPercentage => _vatPercentage;
|
||||
|
||||
/// <summary>
|
||||
/// آیا مالیات فعال است
|
||||
/// </summary>
|
||||
public bool IsEnabled => _isEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// بارگذاری نرخ VAT - اگر امروز گرفته شده از cache، وگرنه از سرور
|
||||
/// </summary>
|
||||
public async Task LoadAsync()
|
||||
{
|
||||
if (_isLoaded) return;
|
||||
|
||||
try
|
||||
{
|
||||
// using var scope = _serviceProvider.CreateScope();
|
||||
// var localStorage = scope.ServiceProvider.GetRequiredService<ILocalStorageService>();
|
||||
//
|
||||
// چک کن آیا امروز قبلاً گرفته شده
|
||||
var savedDate = await _localStorageService.GetItemAsStringAsync(VAT_DATE_KEY);
|
||||
var today = DateTime.Today.ToString("yyyy-MM-dd");
|
||||
|
||||
if (savedDate == today)
|
||||
{
|
||||
// از cache بخوان
|
||||
var savedRate = await _localStorageService.GetItemAsync<double>(VAT_RATE_KEY);
|
||||
var savedPercentage = await _localStorageService.GetItemAsync<int>(VAT_PERCENTAGE_KEY);
|
||||
if (savedRate > 0 && savedPercentage > 0)
|
||||
{
|
||||
_vatRate = savedRate;
|
||||
_vatPercentage = savedPercentage;
|
||||
_isLoaded = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// از سرور بگیر
|
||||
await RefreshFromServerAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// در صورت خطا از مقادیر پیشفرض استفاده شود
|
||||
_isLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// بروزرسانی از سرور و ذخیره در cache
|
||||
/// </summary>
|
||||
public async Task RefreshFromServerAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// ایجاد scope برای دریافت client و localStorage
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var client = scope.ServiceProvider.GetRequiredService<UserOrderContract.UserOrderContractClient>();
|
||||
// var localStorage = scope.ServiceProvider.GetRequiredService<ILocalStorageService>();
|
||||
|
||||
var response = await client.GetVATRateAsync(new Google.Protobuf.WellKnownTypes.Empty());
|
||||
|
||||
_vatRate = response.VatRate;
|
||||
_vatPercentage = response.VatPercentage;
|
||||
_isEnabled = response.IsEnabled;
|
||||
|
||||
// ذخیره در LocalStorage
|
||||
await _localStorageService.SetItemAsync(VAT_RATE_KEY, _vatRate);
|
||||
await _localStorageService.SetItemAsync(VAT_PERCENTAGE_KEY, _vatPercentage);
|
||||
await _localStorageService.SetItemAsStringAsync(VAT_DATE_KEY, DateTime.Today.ToString("yyyy-MM-dd"));
|
||||
|
||||
_isLoaded = true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// مقادیر پیشفرض
|
||||
_isLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// محاسبه مبلغ مالیات
|
||||
/// </summary>
|
||||
public long CalculateVAT(long amount) => _isEnabled ? (long)(amount * _vatRate) : 0;
|
||||
|
||||
/// <summary>
|
||||
/// محاسبه قیمت با احتساب مالیات
|
||||
/// </summary>
|
||||
public long AddVAT(long amount) => _isEnabled ? amount + CalculateVAT(amount) : amount;
|
||||
|
||||
/// <summary>
|
||||
/// فرمت قیمت با نمایش مالیات
|
||||
/// </summary>
|
||||
public string FormatPriceWithVAT(long price)
|
||||
{
|
||||
var priceWithVat = AddVAT(price);
|
||||
return $"{priceWithVat:N0} تومان";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// فرمت قیمت با جزئیات مالیات
|
||||
/// </summary>
|
||||
public (long BasePrice, long VatAmount, long TotalPrice) GetPriceBreakdown(long basePrice)
|
||||
{
|
||||
var vatAmount = CalculateVAT(basePrice);
|
||||
return (basePrice, vatAmount, basePrice + vatAmount);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ namespace FrontOffice.Main.Utilities;
|
||||
|
||||
public record WalletBalances(long CreditBalance, long DiscountBalance, long NetworkBalance);
|
||||
public record WalletTransaction(string Date, long Amount, string Channel, string Description);
|
||||
public record WalletWithdrawal(long Id, string WeekNumber, long Amount, int Status, int? Method, string? Iban, string Created);
|
||||
public record WalletWithdrawal(long Id, long WeekDefinitionId, string WeekDisplayName, long Amount, int Status, int? Method, string? Iban, string Created);
|
||||
public enum WithdrawalMethodClient
|
||||
{
|
||||
Cash = 0,
|
||||
@@ -134,7 +134,8 @@ public class WalletService
|
||||
return response.Models
|
||||
.Select(m => new WalletWithdrawal(
|
||||
m.Id,
|
||||
m.WeekNumber,
|
||||
m.WeekDefinitionId,
|
||||
m.WeekDisplayName,
|
||||
m.TotalAmount,
|
||||
m.Status,
|
||||
m.WithdrawalMethod,
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
@using MudBlazor
|
||||
@using FrontOffice.Main.Utilities
|
||||
@using DateTimeConverterCL
|
||||
@using CityProto = FrontOffice.BFF.City.Protobuf
|
||||
|
||||
@inject NavigationManager Navigation
|
||||
@inject IDialogService DialogService
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
"Key": "kmcQ3XTmH4mrdh8VHziuscyf8LLYjG//Kyni81nH/0E=",
|
||||
"IV": "1wyF3Tt142MOkCpIyCxh/g=="
|
||||
},
|
||||
"SignalR": {
|
||||
"HubPath": "/hubs/token-relay"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
/**
|
||||
* d3-org-chart custom styles for FourSat
|
||||
* Binary Network Tree visualization
|
||||
*/
|
||||
|
||||
/* Container */
|
||||
.org-chart-wrapper {
|
||||
width: 100%;
|
||||
min-height: 500px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.org-chart-container {
|
||||
width: 100%;
|
||||
height: 550px;
|
||||
}
|
||||
|
||||
/* Loading & Error States */
|
||||
.chart-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 400px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.no-data-message,
|
||||
.error-message {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #6c757d;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
/* Node Card - Compact */
|
||||
.org-node-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
border: 1px solid #e0e0e0;
|
||||
font-family: 'Vazirmatn', 'IRANSans', sans-serif;
|
||||
direction: rtl;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.org-node-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
border-color: #0380C0;
|
||||
}
|
||||
|
||||
.org-node-card.active {
|
||||
border-left: 3px solid #28a745;
|
||||
}
|
||||
|
||||
.org-node-card.inactive {
|
||||
opacity: 0.6;
|
||||
border-left: 3px solid #dc3545;
|
||||
}
|
||||
|
||||
/* Compact Header */
|
||||
.node-header-compact {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #fff 100%);
|
||||
}
|
||||
|
||||
.node-header-compact.root {
|
||||
background: linear-gradient(135deg, #e8f5e9 0%, #fff 100%);
|
||||
}
|
||||
|
||||
.node-header-compact.root .node-avatar-sm {
|
||||
background: linear-gradient(135deg, #28a745 0%, #1e7e34 100%);
|
||||
}
|
||||
|
||||
/* Avatar Small */
|
||||
.node-avatar-sm {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
min-width: 26px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #0380C0 0%, #026a9e 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.node-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.node-name-sm {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #212529;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.node-level-sm {
|
||||
font-size: 9px;
|
||||
color: #6c757d;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
/* Legacy Avatar (keep for compatibility) */
|
||||
.node-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Position Badge */
|
||||
.position-badge {
|
||||
font-size: 11px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.position-badge.position-left {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.position-badge.position-right {
|
||||
background: #fce4ec;
|
||||
color: #c2185b;
|
||||
}
|
||||
|
||||
.position-badge.position-root {
|
||||
background: #e8f5e9;
|
||||
color: #388e3c;
|
||||
}
|
||||
|
||||
/* Node Body */
|
||||
.node-body {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.node-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #212529;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.node-mobile {
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
direction: ltr;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.node-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.level-badge {
|
||||
font-size: 10px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
background: #f8f9fa;
|
||||
color: #495057;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.club-badge {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Expand/Collapse Button */
|
||||
.org-expand-btn {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #0380C0;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 6px rgba(3, 128, 192, 0.4);
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
.org-expand-btn:hover {
|
||||
background: #026a9e;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Toolbar */
|
||||
.org-chart-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 10px 12px;
|
||||
background: white;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.toolbar-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #dee2e6;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.toolbar-btn:hover {
|
||||
background: #f8f9fa;
|
||||
border-color: #adb5bd;
|
||||
}
|
||||
|
||||
.toolbar-btn.primary {
|
||||
background: #0380C0;
|
||||
color: white;
|
||||
border-color: #0380C0;
|
||||
}
|
||||
|
||||
.toolbar-btn.primary:hover {
|
||||
background: #026a9e;
|
||||
}
|
||||
|
||||
/* Stats Bar */
|
||||
.org-chart-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
padding: 12px 16px;
|
||||
background: #f8f9fa;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-weight: 600;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.stat-value.left {
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.stat-value.right {
|
||||
color: #c2185b;
|
||||
}
|
||||
|
||||
/* Depth Controls */
|
||||
.depth-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.depth-control label {
|
||||
font-size: 13px;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.depth-control select {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.org-chart-wrapper {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.org-chart-container {
|
||||
height: 450px;
|
||||
}
|
||||
|
||||
.org-chart-toolbar {
|
||||
padding: 10px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.toolbar-group {
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.org-chart-stats {
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.org-chart-container {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.org-chart-toolbar {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
padding: 5px 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.toolbar-btn span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.org-chart-toolbar,
|
||||
.org-chart-stats {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.org-chart-container {
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* d3-org-chart integration for Blazor
|
||||
* Network Binary Tree visualization for FourSat
|
||||
*/
|
||||
|
||||
window.OrgChart = {
|
||||
chart: null,
|
||||
dotNetHelper: null,
|
||||
|
||||
/**
|
||||
* Initialize the organization chart
|
||||
* @param {string} containerId - The ID of the container element
|
||||
* @param {object} data - The tree data in flat array format
|
||||
* @param {object} dotNetHelper - Blazor .NET helper for callbacks
|
||||
*/
|
||||
init: function (containerId, data, dotNetHelper) {
|
||||
this.dotNetHelper = dotNetHelper;
|
||||
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) {
|
||||
console.error('OrgChart: Container not found:', containerId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear previous chart
|
||||
container.innerHTML = '';
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
container.innerHTML = '<div class="no-data-message">دادهای برای نمایش وجود ندارد</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.chart = new d3.OrgChart()
|
||||
.container('#' + containerId)
|
||||
.data(data)
|
||||
.nodeWidth((d) => 130)
|
||||
.nodeHeight((d) => 56)
|
||||
.childrenMargin((d) => 50)
|
||||
.compactMarginBetween((d) => 15)
|
||||
.compactMarginPair((d) => 15)
|
||||
.neighbourMargin((a, b) => 15)
|
||||
.siblingsMargin((d) => 15)
|
||||
.buttonContent(({ node, state }) => {
|
||||
const hasChildren = node.data._directSubordinates > 0;
|
||||
const isExpanded = node.children;
|
||||
return hasChildren ? `<div class="org-expand-btn">
|
||||
<span>${isExpanded ? '−' : '+'}</span>
|
||||
</div>` : '';
|
||||
})
|
||||
.linkUpdate(function (d, i, arr) {
|
||||
d3.select(this)
|
||||
.attr('stroke', (d) => d.data._highlighted || d.data._upToTheRootHighlighted ? '#0380C0' : '#ccc')
|
||||
.attr('stroke-width', (d) => d.data._highlighted || d.data._upToTheRootHighlighted ? 3 : 2);
|
||||
})
|
||||
.nodeContent(function (d, i, arr, state) {
|
||||
const data = d.data;
|
||||
const isRoot = !data.parentId || data.parentId === '';
|
||||
const positionClass = data.position === 'Left' ? 'position-left' :
|
||||
data.position === 'Right' ? 'position-right' : 'position-root';
|
||||
const activeClass = data.isActive ? 'active' : 'inactive';
|
||||
|
||||
// Avatar - first letter of name
|
||||
const firstChar = data.fullName ? data.fullName.charAt(0) : '?';
|
||||
|
||||
return `
|
||||
<div class="org-node-card ${positionClass} ${activeClass}" data-user-id="${data.id}">
|
||||
<div class="node-header-compact ${isRoot ? 'root' : ''}">
|
||||
<div class="node-avatar-sm">${firstChar}</div>
|
||||
<div class="node-info">
|
||||
<div class="node-name-sm">${data.fullName || 'بدون نام'}</div>
|
||||
<div class="node-level-sm">L${data.level || 0}${!isRoot ? ' • ' + (data.position === 'Left' ? 'چپ' : 'راست') : ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.onNodeClick((d) => {
|
||||
if (this.dotNetHelper) {
|
||||
// Convert string id to number for C# long
|
||||
const userId = parseInt(d.data.id, 10);
|
||||
this.dotNetHelper.invokeMethodAsync('OnNodeClicked', userId);
|
||||
}
|
||||
})
|
||||
.render();
|
||||
|
||||
// Initial centering and zoom
|
||||
this.chart.fit();
|
||||
|
||||
} catch (error) {
|
||||
console.error('OrgChart: Error initializing chart:', error);
|
||||
container.innerHTML = '<div class="error-message">خطا در بارگذاری نمودار</div>';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the chart with new data
|
||||
* @param {object} data - The new tree data
|
||||
*/
|
||||
update: function (data) {
|
||||
if (this.chart) {
|
||||
this.chart.data(data).render();
|
||||
this.chart.fit();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Expand all nodes
|
||||
*/
|
||||
expandAll: function () {
|
||||
if (this.chart) {
|
||||
this.chart.expandAll().render();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Collapse all nodes
|
||||
*/
|
||||
collapseAll: function () {
|
||||
if (this.chart) {
|
||||
this.chart.collapseAll().render();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Center the chart
|
||||
*/
|
||||
center: function () {
|
||||
if (this.chart) {
|
||||
this.chart.fit();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fit entire tree to screen (zoom out completely)
|
||||
*/
|
||||
fitToScreen: function () {
|
||||
if (this.chart) {
|
||||
this.chart.fit();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Zoom to specific node
|
||||
* @param {string} nodeId - The ID of the node to zoom to
|
||||
*/
|
||||
zoomToNode: function (nodeId) {
|
||||
if (this.chart) {
|
||||
this.chart.setCentered(nodeId).render();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Highlight path to specific node
|
||||
* @param {string} nodeId - The ID of the node
|
||||
*/
|
||||
highlightNode: function (nodeId) {
|
||||
if (this.chart) {
|
||||
this.chart.setHighlighted(nodeId).render();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear highlighting
|
||||
*/
|
||||
clearHighlight: function () {
|
||||
if (this.chart) {
|
||||
this.chart.clearHighlighting().render();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Export chart as PNG
|
||||
*/
|
||||
exportPng: function () {
|
||||
if (this.chart) {
|
||||
this.chart.exportImg({ full: true });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Export chart as SVG
|
||||
*/
|
||||
exportSvg: function () {
|
||||
if (this.chart) {
|
||||
this.chart.exportSvg();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Dispose the chart
|
||||
*/
|
||||
dispose: function () {
|
||||
if (this.chart) {
|
||||
// d3-org-chart doesn't have built-in dispose, so we just clear the reference
|
||||
this.chart = null;
|
||||
this.dotNetHelper = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert hierarchical tree data to flat array format for d3-org-chart
|
||||
* @param {object} rootNode - The root node with nested children
|
||||
* @returns {array} - Flat array of nodes
|
||||
*/
|
||||
convertToFlatArray: function (rootNode) {
|
||||
if (!rootNode) return [];
|
||||
|
||||
const result = [];
|
||||
|
||||
function traverse(node, parentId) {
|
||||
const flatNode = {
|
||||
id: node.userId?.toString() || node.id?.toString(),
|
||||
parentId: parentId,
|
||||
fullName: node.fullName || '',
|
||||
mobile: node.mobile || '',
|
||||
avatar: node.avatar,
|
||||
position: node.position || 'Root',
|
||||
level: node.level || 0,
|
||||
isActive: node.isActive !== false,
|
||||
isClubActive: node.isClubActive || false,
|
||||
activationWeekNumber: node.activationWeekNumber,
|
||||
joinedAt: node.joinedAt
|
||||
};
|
||||
|
||||
result.push(flatNode);
|
||||
|
||||
// Process left child
|
||||
if (node.leftChild) {
|
||||
traverse(node.leftChild, flatNode.id);
|
||||
}
|
||||
|
||||
// Process right child
|
||||
if (node.rightChild) {
|
||||
traverse(node.rightChild, flatNode.id);
|
||||
}
|
||||
}
|
||||
|
||||
traverse(rootNode, '');
|
||||
return result;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user