Merge branch 'main' into kub-stage

# Conflicts:
#	.gitea/workflows/kub-deploy.yml
This commit is contained in:
masoodafar-web
2025-12-07 00:25:09 +03:30
19 changed files with 1108 additions and 21 deletions

25
src/.dockerignore Normal file
View File

@@ -0,0 +1,25 @@
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/.idea
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

View File

@@ -51,6 +51,8 @@ public static class ConfigureServices
services.AddScoped<CategoryService>(); services.AddScoped<CategoryService>();
services.AddScoped<OrderService>(); services.AddScoped<OrderService>();
services.AddScoped<WalletService>(); services.AddScoped<WalletService>();
// Package service
services.AddScoped<PackageService>();
// New services for Club, Network, Commission // New services for Club, Network, Commission
services.AddScoped<ClubMembershipService>(); services.AddScoped<ClubMembershipService>();
services.AddScoped<NetworkMembershipService>(); services.AddScoped<NetworkMembershipService>();

View File

@@ -0,0 +1,24 @@
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 8080
EXPOSE 8081
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["FrontOffice.Main/NuGet.config", "NuGet.config"]
COPY ["FrontOffice.Main/FrontOffice.Main.csproj", "FrontOffice.Main/"]
RUN dotnet restore "FrontOffice.Main/FrontOffice.Main.csproj" --configfile NuGet.config
COPY . .
WORKDIR "/src/FrontOffice.Main"
RUN dotnet build "./FrontOffice.Main.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./FrontOffice.Main.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "FrontOffice.Main.dll"]

View File

@@ -5,18 +5,24 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>6dab807c-c6d8-4711-bf64-11c69e8d39f4</UserSecretsId> <UserSecretsId>6dab807c-c6d8-4711-bf64-11c69e8d39f4</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" /> <PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
<PackageReference Include="Foursat.FrontOffice.BFF.Package.Protobuf" Version="0.0.112" /> <PackageReference Include="Foursat.FrontOffice.BFF.ClubMembership.Protobuf" Version="0.0.3" />
<PackageReference Include="Foursat.FrontOffice.BFF.Products.Protobuf" Version="0.0.15" /> <PackageReference Include="Foursat.FrontOffice.BFF.Commission.Protobuf" Version="0.0.2" />
<PackageReference Include="Foursat.FrontOffice.BFF.Transaction.Protobuf" Version="0.0.111" /> <PackageReference Include="Foursat.FrontOffice.BFF.DiscountShop.Protobuf" Version="0.0.2" />
<PackageReference Include="Foursat.FrontOffice.BFF.Category.Protobuf" Version="0.0.12" /> <PackageReference Include="Foursat.FrontOffice.BFF.NetworkMembership.Protobuf" Version="0.0.2" />
<PackageReference Include="Foursat.FrontOffice.BFF.User.Protobuf" Version="0.0.116" /> <PackageReference Include="Foursat.FrontOffice.BFF.Package.Protobuf" Version="0.0.113" />
<PackageReference Include="Foursat.FrontOffice.BFF.UserAddress.Protobuf" Version="0.0.114" /> <PackageReference Include="Foursat.FrontOffice.BFF.Products.Protobuf" Version="0.0.17" />
<PackageReference Include="Foursat.FrontOffice.BFF.UserOrder.Protobuf" Version="0.0.114" /> <PackageReference Include="Foursat.FrontOffice.BFF.Transaction.Protobuf" Version="0.0.112" />
<PackageReference Include="Foursat.FrontOffice.BFF.ShopingCart.Protobuf" Version="0.0.15" /> <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 --> <!-- UserWallet moved to ProjectReference for latest proto -->
<PackageReference Include="MudBlazor" Version="8.14.0" /> <PackageReference Include="MudBlazor" Version="8.14.0" />
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" /> <PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
@@ -29,17 +35,24 @@
</ItemGroup> </ItemGroup>
<!-- New Proto Projects (local references until NuGet publish) --> <!-- New Proto Projects (local references until NuGet publish) -->
<ItemGroup> <!-- <ItemGroup>-->
<ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.ClubMembership.Protobuf\FrontOffice.BFF.ClubMembership.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.Commission.Protobuf\FrontOffice.BFF.Commission.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.NetworkMembership.Protobuf\FrontOffice.BFF.NetworkMembership.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.DiscountShop.Protobuf\FrontOffice.BFF.DiscountShop.Protobuf.csproj" /> <!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.DiscountShop.Protobuf\FrontOffice.BFF.DiscountShop.Protobuf.csproj" />-->
<ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.UserWallet.Protobuf\FrontOffice.BFF.UserWallet.Protobuf.csproj" /> <!-- <ProjectReference Include="..\..\..\FrontOffice.BFF\src\Protobufs\FrontOffice.BFF.UserWallet.Protobuf\FrontOffice.BFF.UserWallet.Protobuf.csproj" />-->
</ItemGroup> <!-- </ItemGroup>-->
<!-- -->
<ItemGroup> <ItemGroup>
<Folder Include="Components" /> <Folder Include="Components" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
</ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
<add key="FourSat" value="https://git.afrino.co/api/packages/FourSat/nuget/index.json" />
<add key="Afrino" value="https://git.afrino.co/api/packages/Afrino/nuget/index.json" />
</packageSources>
<packageSourceCredentials>
<FourSat>
<add key="Username" value="masoud" />
<add key="ClearTextPassword" value="87zH26nbqT" />
</FourSat>
<Afrino>
<add key="Username" value="systemuser" />
<add key="ClearTextPassword" value="sZSA7PTiv3pUSQZ" />
</Afrino>
</packageSourceCredentials>
</configuration>

View File

@@ -0,0 +1,201 @@
@attribute [Route(RouteConstants.Package.MyPackages)]
<PageTitle>پکیج‌های من</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-8">
<!-- Breadcrumb -->
<MudBreadcrumbs Items="_breadcrumbItems" Class="mb-4" />
<!-- Header -->
<MudStack Class="mb-6">
<MudText Typo="Typo.h4">پکیج‌های من</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
وضعیت پکیج‌های خریداری شده و عضویت باشگاه مشتریان
</MudText>
</MudStack>
@if (_isLoading)
{
<MudStack AlignItems="AlignItems.Center" Class="py-16">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large" />
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">در حال بارگذاری...</MudText>
</MudStack>
}
else if (_userStatus == null || !_userStatus.HasPurchasedPackage)
{
<!-- No Package Purchased -->
<MudPaper Elevation="2" Class="pa-8 text-center">
<MudStack AlignItems="AlignItems.Center" Spacing="4">
<MudIcon Icon="@Icons.Material.Filled.CardGiftcard" Size="Size.Large" Color="Color.Primary" Style="font-size: 80px;" />
<MudText Typo="Typo.h5">شما هنوز پکیجی خریداری نکرده‌اید</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary" Style="max-width: 500px;">
با خرید پکیج طلایی، به باشگاه مشتریان بپیوندید و از مزایای ویژه مانند کمیسیون هفتگی و شبکه‌سازی بهره‌مند شوید.
</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
StartIcon="@Icons.Material.Filled.ShoppingCart"
OnClick="@(() => Navigation.NavigateTo(RouteConstants.Package.List))">
مشاهده پکیج‌ها
</MudButton>
</MudStack>
</MudPaper>
}
else
{
<MudGrid Spacing="4">
<!-- Package Status Card -->
<MudItem xs="12" md="6">
<MudPaper Elevation="2" Class="pa-6 h-100">
<MudStack Spacing="3">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Inventory2" Color="Color.Primary" />
<MudText Typo="Typo.h6">وضعیت پکیج</MudText>
</MudStack>
<MudDivider />
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">نوع پکیج:</MudText>
<MudText Typo="Typo.body2">پکیج طلایی</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">روش خرید:</MudText>
<MudChip T="string" Size="Size.Small" Color="@GetPurchaseMethodColor(_userStatus.PurchaseMethod)">
@GetPurchaseMethodText(_userStatus.PurchaseMethod)
</MudChip>
</MudStack>
@if (_userStatus.PurchaseDate.HasValue)
{
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">تاریخ خرید:</MudText>
<MudText Typo="Typo.body2">@_userStatus.PurchaseDate.Value.ToLocalTime().ToString("yyyy/MM/dd")</MudText>
</MudStack>
}
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">موجودی کیف پول:</MudText>
<MudText Typo="Typo.body2" Color="Color.Success">@FormatPrice(_userStatus.WalletBalance)</MudText>
</MudStack>
</MudStack>
</MudStack>
</MudPaper>
</MudItem>
<!-- Club Membership Card -->
<MudItem xs="12" md="6">
<MudPaper Elevation="2" Class="pa-6 h-100">
<MudStack Spacing="3">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.CardMembership" Color="Color.Secondary" />
<MudText Typo="Typo.h6">عضویت باشگاه</MudText>
</MudStack>
<MudDivider />
@if (_userStatus.IsClubMemberActive)
{
<MudAlert Severity="Severity.Success" Dense="true" Icon="@Icons.Material.Filled.CheckCircle">
عضویت باشگاه مشتریان شما فعال است
</MudAlert>
<MudStack Spacing="2">
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
FullWidth="true"
StartIcon="@Icons.Material.Filled.AccountTree"
OnClick="@(() => Navigation.NavigateTo(RouteConstants.Network.Statistics))">
مشاهده شبکه
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Success"
FullWidth="true"
StartIcon="@Icons.Material.Filled.Payments"
OnClick="@(() => Navigation.NavigateTo(RouteConstants.Commission.Dashboard))">
کمیسیون‌های من
</MudButton>
</MudStack>
}
else if (_userStatus.CanActivateClubMembership)
{
<MudAlert Severity="Severity.Info" Dense="true" Icon="@Icons.Material.Filled.Info">
شما می‌توانید عضویت باشگاه را فعال کنید
</MudAlert>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
FullWidth="true"
Size="Size.Large"
StartIcon="@Icons.Material.Filled.CardMembership"
OnClick="@(() => Navigation.NavigateTo(RouteConstants.Club.Membership))">
فعالسازی باشگاه مشتریان
</MudButton>
}
else
{
<MudAlert Severity="Severity.Warning" Dense="true" Icon="@Icons.Material.Filled.Warning">
موجودی کیف پول شما برای فعالسازی کافی نیست
</MudAlert>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
برای فعالسازی باشگاه مشتریان، موجودی کیف پول شما باید حداقل ۵۶ میلیون تومان باشد.
</MudText>
}
</MudStack>
</MudPaper>
</MudItem>
<!-- Benefits Card -->
<MudItem xs="12">
<MudPaper Elevation="2" Class="pa-6">
<MudStack Spacing="3">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Stars" Color="Color.Warning" />
<MudText Typo="Typo.h6">مزایای پکیج طلایی</MudText>
</MudStack>
<MudDivider />
<MudGrid Spacing="3">
<MudItem xs="12" sm="6" md="3">
<MudPaper Outlined="true" Class="pa-4 text-center">
<MudIcon Icon="@Icons.Material.Filled.Group" Color="Color.Primary" Size="Size.Large" />
<MudText Typo="Typo.subtitle2" Class="mt-2">شبکه‌سازی نامحدود</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">دعوت دوستان و ساخت تیم</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudPaper Outlined="true" Class="pa-4 text-center">
<MudIcon Icon="@Icons.Material.Filled.Payments" Color="Color.Success" Size="Size.Large" />
<MudText Typo="Typo.subtitle2" Class="mt-2">کمیسیون هفتگی</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">دریافت سهم از فروش شبکه</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudPaper Outlined="true" Class="pa-4 text-center">
<MudIcon Icon="@Icons.Material.Filled.ShoppingBag" Color="Color.Secondary" Size="Size.Large" />
<MudText Typo="Typo.subtitle2" Class="mt-2">فروشگاه تخفیفی</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">خرید با تخفیف ویژه</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudPaper Outlined="true" Class="pa-4 text-center">
<MudIcon Icon="@Icons.Material.Filled.Support" Color="Color.Info" Size="Size.Large" />
<MudText Typo="Typo.subtitle2" Class="mt-2">پشتیبانی ۲۴ ساعته</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">پشتیبانی اولویت‌دار</MudText>
</MudPaper>
</MudItem>
</MudGrid>
</MudStack>
</MudPaper>
</MudItem>
</MudGrid>
}
</MudContainer>

View File

@@ -0,0 +1,70 @@
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace FrontOffice.Main.Pages.Package;
public partial class MyPackages : ComponentBase
{
[Inject] private PackageService PackageService { get; set; } = default!;
private UserPackageStatusDto? _userStatus;
private bool _isLoading = true;
private List<BreadcrumbItem> _breadcrumbItems = new()
{
new BreadcrumbItem("صفحه اصلی", RouteConstants.Main.MainPage),
new BreadcrumbItem("پروفایل", RouteConstants.Profile.Index),
new BreadcrumbItem("پکیج‌های من", null, disabled: true)
};
protected override async Task OnInitializedAsync()
{
await LoadDataAsync();
}
private async Task LoadDataAsync()
{
_isLoading = true;
try
{
_userStatus = await PackageService.GetUserPackageStatusAsync();
}
catch (Exception ex)
{
#if DEBUG
Console.WriteLine($"Error loading user package status: {ex.Message}");
#endif
}
finally
{
_isLoading = false;
}
}
private static string GetPurchaseMethodText(string? method)
{
return method switch
{
"DayaLoan" => "وام دایا",
"DirectPurchase" => "پرداخت مستقیم",
_ => "نامشخص"
};
}
private static Color GetPurchaseMethodColor(string? method)
{
return method switch
{
"DayaLoan" => Color.Info,
"DirectPurchase" => Color.Success,
_ => Color.Default
};
}
private static string FormatPrice(long price)
{
return string.Format("{0:N0} تومان", price);
}
}

View File

@@ -0,0 +1,139 @@
@attribute [Route(RouteConstants.Package.List)]
<PageTitle>پکیج‌ها</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="py-8">
<!-- Header -->
<MudStack Class="mb-6">
<MudText Typo="Typo.h4">پکیج‌های سرمایه‌گذاری</MudText>
<MudText Typo="Typo.body1" Class="mud-text-secondary">
با خرید پکیج طلایی، به باشگاه مشتریان بپیوندید و از مزایای ویژه بهره‌مند شوید.
</MudText>
</MudStack>
@if (_isLoading)
{
<MudStack AlignItems="AlignItems.Center" Class="py-16">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large" />
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">در حال بارگذاری پکیج‌ها...</MudText>
</MudStack>
}
else if (!_packages.Any())
{
<MudStack AlignItems="AlignItems.Center" Class="py-16">
<MudIcon Icon="@Icons.Material.Filled.Inventory2" Size="Size.Large" Color="Color.Default" />
<MudText Typo="Typo.h6" Class="mt-2">هیچ پکیجی یافت نشد</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">در حال حاضر پکیجی برای نمایش وجود ندارد.</MudText>
</MudStack>
}
else
{
<!-- User Status Card (if logged in) -->
@if (_userStatus != null && _userStatus.HasPurchasedPackage)
{
<MudAlert Severity="Severity.Success" Class="mb-6" Icon="@Icons.Material.Filled.CheckCircle">
<MudText>
شما قبلاً پکیج طلایی را خریداری کرده‌اید.
@if (_userStatus.IsClubMemberActive)
{
<span>عضویت باشگاه شما فعال است.</span>
}
else
{
<MudLink Href="@RouteConstants.Club.Membership" Class="mx-2">فعالسازی باشگاه</MudLink>
}
</MudText>
</MudAlert>
}
<!-- Packages Grid -->
<MudGrid Spacing="4">
@foreach (var package in _packages)
{
<MudItem xs="12" sm="6" md="4">
<MudCard Class="h-100 package-card" Elevation="3">
<!-- Package Image -->
<MudCardMedia Image="@package.ImageUrl"
Title="@package.Title"
Height="200" />
<MudCardContent>
<MudStack Spacing="2">
<MudText Typo="Typo.h5">@package.Title</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary" Style="min-height: 60px;">
@TruncateDescription(package.Description)
</MudText>
<!-- Price -->
<MudStack Row="true" AlignItems="AlignItems.Center" Class="mt-2">
<MudIcon Icon="@Icons.Material.Filled.Sell" Color="Color.Success" Size="Size.Small" />
<MudText Typo="Typo.h6" Color="Color.Success">@package.FormattedPrice</MudText>
</MudStack>
<!-- Features Preview -->
<MudStack Spacing="1" Class="mt-2">
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.caption">عضویت در باشگاه مشتریان</MudText>
</MudStack>
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.caption">دریافت کمیسیون هفتگی</MudText>
</MudStack>
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.caption">شبکه‌سازی نامحدود</MudText>
</MudStack>
</MudStack>
</MudStack>
</MudCardContent>
<MudCardActions Class="pa-4">
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
FullWidth="true"
OnClick="@(() => ViewPackageDetails(package.Id))">
مشاهده جزئیات
</MudButton>
</MudCardActions>
</MudCard>
</MudItem>
}
</MudGrid>
<!-- Info Section -->
<MudPaper Elevation="0" Class="pa-6 mt-8 rounded-xl" Style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<MudGrid Spacing="4">
<MudItem xs="12" md="8">
<MudStack Spacing="2">
<MudText Typo="Typo.h5" Style="color: white;">چرا پکیج طلایی؟</MudText>
<MudText Typo="Typo.body1" Style="color: rgba(255,255,255,0.9);">
با خرید پکیج طلایی، علاوه بر دسترسی به محصولات ویژه، می‌توانید در شبکه فروش شرکت کنید
و از کمیسیون‌های هفتگی بهره‌مند شوید. هر چه شبکه شما گسترده‌تر، درآمد شما بیشتر!
</MudText>
</MudStack>
</MudItem>
<MudItem xs="12" md="4" Class="d-flex align-center justify-center">
<MudButton Variant="Variant.Filled"
Color="Color.Default"
Size="Size.Large"
StartIcon="@Icons.Material.Filled.HelpOutline"
OnClick="@(() => Navigation.NavigateTo(RouteConstants.FAQ.Index))">
سوالات متداول
</MudButton>
</MudItem>
</MudGrid>
</MudPaper>
}
</MudContainer>
<style>
.package-card {
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.package-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15) !important;
}
</style>

View File

@@ -0,0 +1,54 @@
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
namespace FrontOffice.Main.Pages.Package;
public partial class Packages : ComponentBase
{
[Inject] private PackageService PackageService { get; set; } = default!;
private List<PackageDto> _packages = new();
private UserPackageStatusDto? _userStatus;
private bool _isLoading = true;
protected override async Task OnInitializedAsync()
{
await LoadDataAsync();
}
private async Task LoadDataAsync()
{
_isLoading = true;
try
{
// Load packages
_packages = await PackageService.GetAllPackagesAsync();
// Load user status (if authenticated)
_userStatus = await PackageService.GetUserPackageStatusAsync();
}
catch (Exception ex)
{
#if DEBUG
Console.WriteLine($"Error loading packages: {ex.Message}");
#endif
}
finally
{
_isLoading = false;
}
}
private void ViewPackageDetails(long packageId)
{
Navigation.NavigateTo(RouteConstants.Package.Detail + packageId);
}
private static string TruncateDescription(string description, int maxLength = 100)
{
if (string.IsNullOrEmpty(description)) return string.Empty;
if (description.Length <= maxLength) return description;
return description[..maxLength] + "...";
}
}

View File

@@ -0,0 +1,70 @@
@attribute [Route(RouteConstants.Profile.ChangePassword)]
<PageTitle>تغییر رمز عبور</PageTitle>
<MudContainer MaxWidth="MaxWidth.Small" 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.Settings">بازگشت</MudButton>
</MudStack>
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudStack Spacing="3">
@if (_success)
{
<MudAlert Severity="Severity.Success" Variant="Variant.Filled">
رمز عبور با موفقیت تغییر کرد
</MudAlert>
}
else
{
<MudTextField T="string" @bind-Value="_currentPassword"
Label="رمز عبور فعلی"
InputType="InputType.Password"
Required="true"
RequiredError="رمز عبور فعلی الزامی است"
Variant="Variant.Outlined" />
<MudTextField T="string" @bind-Value="_newPassword"
Label="رمز عبور جدید"
InputType="InputType.Password"
Required="true"
RequiredError="رمز عبور جدید الزامی است"
HelperText="حداقل 8 کاراکتر شامل حروف و اعداد"
Variant="Variant.Outlined" />
<MudTextField T="string" @bind-Value="_confirmPassword"
Label="تکرار رمز عبور جدید"
InputType="InputType.Password"
Required="true"
RequiredError="تکرار رمز عبور الزامی است"
Variant="Variant.Outlined" />
@if (!string.IsNullOrEmpty(_error))
{
<MudAlert Severity="Severity.Error" Variant="Variant.Outlined">@_error</MudAlert>
}
<MudStack Row="true" Justify="Justify.FlexEnd" Class="mt-2">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="ChangePasswordAsync"
Disabled="_saving"
StartIcon="@Icons.Material.Filled.Lock">
@if (_saving)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="me-2" />
<span>در حال ذخیره...</span>
}
else
{
<span>تغییر رمز عبور</span>
}
</MudButton>
</MudStack>
}
</MudStack>
</MudPaper>
</MudStack>
</MudContainer>

View File

@@ -0,0 +1,77 @@
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace FrontOffice.Main.Pages.Profile;
public partial class ChangePassword : ComponentBase
{
private string _currentPassword = string.Empty;
private string _newPassword = string.Empty;
private string _confirmPassword = string.Empty;
private string _error = string.Empty;
private bool _saving;
private bool _success;
private async Task ChangePasswordAsync()
{
_error = string.Empty;
// Validation
if (string.IsNullOrWhiteSpace(_currentPassword))
{
_error = "رمز عبور فعلی را وارد کنید";
return;
}
if (string.IsNullOrWhiteSpace(_newPassword))
{
_error = "رمز عبور جدید را وارد کنید";
return;
}
if (_newPassword.Length < 8)
{
_error = "رمز عبور جدید باید حداقل 8 کاراکتر باشد";
return;
}
if (_newPassword != _confirmPassword)
{
_error = "رمز عبور جدید و تکرار آن یکسان نیستند";
return;
}
if (_currentPassword == _newPassword)
{
_error = "رمز عبور جدید باید متفاوت از رمز فعلی باشد";
return;
}
_saving = true;
try
{
// TODO: Call UserContract.ChangePasswordAsync when available
// var request = new ChangePasswordRequest
// {
// CurrentPassword = _currentPassword,
// NewPassword = _newPassword
// };
// await UserContract.ChangePasswordAsync(request);
// Simulate API call
await Task.Delay(500);
_success = true;
Snackbar.Add("رمز عبور با موفقیت تغییر کرد", Severity.Success);
}
catch (Exception ex)
{
_error = ex.Message;
Snackbar.Add($"خطا در تغییر رمز عبور: {ex.Message}", Severity.Error);
}
finally
{
_saving = false;
}
}
}

View File

@@ -101,10 +101,24 @@
} }
</MudList> </MudList>
<MudDivider Class="my-2" /> <MudDivider Class="my-2" />
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle2">مبلغ قابل پرداخت</MudText> @* نمایش جزئیات مالی با مالیات *@
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatPrice(Cart.Total)</MudText> <MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">جمع کالاها:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(Cart.Total)</MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2" Class="mud-text-secondary">مالیات بر ارزش افزوده (۹%):</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>
</MudStack>
</MudStack> </MudStack>
<MudButton Disabled="@(!CanPlaceOrder)" Class="mt-3 w-100-mobile" Variant="Variant.Filled" Color="Color.Primary" OnClick="PlaceOrder" StartIcon="@Icons.Material.Filled.CheckCircle">ثبت سفارش</MudButton> <MudButton Disabled="@(!CanPlaceOrder)" Class="mt-3 w-100-mobile" Variant="Variant.Filled" Color="Color.Primary" OnClick="PlaceOrder" StartIcon="@Icons.Material.Filled.CheckCircle">ثبت سفارش</MudButton>
} }
</MudPaper> </MudPaper>

View File

@@ -91,6 +91,11 @@ public partial class CheckoutSummary : ComponentBase
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price); private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
/// <summary>
/// محاسبه مالیات بر ارزش افزوده (۹%)
/// </summary>
private long CalculateVAT() => (long)(Cart.Total * 0.09);
private static string GetProductImageUrl(string? imageUrl) private static string GetProductImageUrl(string? imageUrl)
=> string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl; => string.IsNullOrWhiteSpace(imageUrl) ? "/images/product-placeholder.svg" : imageUrl;
} }

View File

@@ -74,7 +74,29 @@ else
</MudStack> </MudStack>
</MudHidden> </MudHidden>
<MudDivider Class="my-2" /> <MudDivider Class="my-2" />
<MudText Typo="Typo.h6" Align="Align.End">مبلغ کل: @FormatPrice(_order.FactorDetails.Sum(s=>s.UnitPrice.Value*s.Count.Value))</MudText>
@* نمایش جزئیات مالی *@
@{
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">
<MudText Typo="Typo.body2">جمع کالاها:</MudText>
<MudText Typo="Typo.body2">@FormatPrice(subtotal)</MudText>
</MudStack>
<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>
</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>
</MudStack> </MudStack>
</MudPaper> </MudPaper>
</MudContainer> </MudContainer>

View File

@@ -0,0 +1,102 @@
@using FrontOffice.BFF.UserOrder.Protobuf.Protos.UserOrder
@attribute [Route(RouteConstants.Store.OrderTracking + "{id:long}")]
<PageTitle>پیگیری سفارش</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
@if (_loading)
{
<MudStack AlignItems="AlignItems.Center">
<MudProgressCircular Indeterminate="true" Color="Color.Primary" />
<MudText Class="mt-2 mud-text-secondary">در حال بارگذاری...</MudText>
</MudStack>
}
else if (_order is null)
{
<MudAlert Severity="Severity.Warning">سفارش یافت نشد.</MudAlert>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack"
Href="@RouteConstants.Store.Orders">
بازگشت به سفارش‌ها
</MudButton>
}
else
{
<MudStack Spacing="3">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">پیگیری سفارش #@_order.Id</MudText>
<MudButton Variant="Variant.Text" StartIcon="@Icons.Material.Filled.ArrowBack"
Href="@(RouteConstants.Store.OrderDetail + id)">
جزئیات سفارش
</MudButton>
</MudStack>
@* Timeline سفارش *@
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-4">وضعیت سفارش</MudText>
<MudTimeline TimelinePosition="TimelinePosition.Start">
@foreach (var step in _trackingSteps)
{
<MudTimelineItem Color="@(step.IsCompleted ? Color.Success : (step.IsCurrent ? Color.Primary : Color.Default))"
Variant="@(step.IsCompleted || step.IsCurrent ? Variant.Filled : Variant.Outlined)"
Size="Size.Medium">
<ItemContent>
<MudStack Spacing="0">
<MudText Typo="Typo.subtitle1"
Color="@(step.IsCompleted ? Color.Success : (step.IsCurrent ? Color.Primary : Color.Default))">
@step.Title
</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">@step.Description</MudText>
@if (!string.IsNullOrEmpty(step.Date))
{
<MudText Typo="Typo.caption" Class="mud-text-secondary mt-1">
<MudIcon Icon="@Icons.Material.Filled.Schedule" Size="Size.Small" Class="me-1" />
@step.Date
</MudText>
}
</MudStack>
</ItemContent>
</MudTimelineItem>
}
</MudTimeline>
</MudPaper>
@* اطلاعات ارسال *@
<MudPaper Elevation="2" Class="pa-4 rounded-lg">
<MudText Typo="Typo.h6" Class="mb-3">اطلاعات ارسال</MudText>
<MudGrid>
<MudItem xs="12" sm="6">
<MudStack Spacing="1">
<MudText Typo="Typo.caption" Class="mud-text-secondary">آدرس تحویل</MudText>
<MudText>@(_order.UserAddressText ?? "---")</MudText>
</MudStack>
</MudItem>
<MudItem xs="12" sm="6">
<MudStack Spacing="1">
<MudText Typo="Typo.caption" Class="mud-text-secondary">روش پرداخت</MudText>
<MudText>@GetPaymentMethodText(_order.PaymentMethod)</MudText>
</MudStack>
</MudItem>
</MudGrid>
</MudPaper>
@* پشتیبانی *@
<MudPaper Elevation="1" Class="pa-4 rounded-lg" Style="background-color: var(--mud-palette-background-grey);">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
<MudIcon Icon="@Icons.Material.Filled.HeadsetMic" Size="Size.Large" Color="Color.Primary" />
<MudStack Spacing="0">
<MudText Typo="Typo.subtitle1">سوالی دارید؟</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
با پشتیبانی ما تماس بگیرید: ۰۲۱-۱۲۳۴۵۶۷۸
</MudText>
</MudStack>
<MudSpacer />
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Href="@RouteConstants.Contact.Index">
تماس با ما
</MudButton>
</MudStack>
</MudPaper>
</MudStack>
}
</MudContainer>

View File

@@ -0,0 +1,125 @@
using FrontOffice.BFF.UserOrder.Protobuf.Protos.UserOrder;
using FrontOffice.Main.Utilities;
using Microsoft.AspNetCore.Components;
namespace FrontOffice.Main.Pages.Store;
public partial class OrderTracking : ComponentBase
{
[Inject] private OrderService OrderService { get; set; } = default!;
[Parameter] public long id { get; set; }
private GetUserOrderResponse? _order;
private bool _loading = true;
private List<TrackingStep> _trackingSteps = new();
protected override async Task OnInitializedAsync()
{
await LoadOrderAsync();
}
private async Task LoadOrderAsync()
{
_loading = true;
StateHasChanged();
try
{
var response = await OrderService.GetOrderAsync(id);
if (response is not null)
{
_order = response;
BuildTrackingSteps();
}
}
finally
{
_loading = false;
StateHasChanged();
}
}
private void BuildTrackingSteps()
{
if (_order is null) return;
// Build tracking steps based on PaymentStatus
var isPaid = _order.PaymentStatus == PaymentStatus.Success;
_trackingSteps = new List<TrackingStep>
{
new()
{
Title = "ثبت سفارش",
Description = "سفارش شما با موفقیت ثبت شد",
IsCompleted = true,
Date = FormatDate(_order.PaymentDate)
},
new()
{
Title = "در انتظار پرداخت",
Description = "منتظر تایید پرداخت هستیم",
IsCompleted = isPaid,
IsCurrent = !isPaid,
Date = isPaid ? FormatDate(_order.PaymentDate) : ""
},
new()
{
Title = "تایید پرداخت",
Description = "پرداخت شما تایید شد",
IsCompleted = isPaid,
IsCurrent = false,
Date = isPaid ? FormatDate(_order.PaymentDate) : ""
},
new()
{
Title = "در حال پردازش",
Description = "سفارش شما در حال آماده‌سازی است",
IsCompleted = false,
IsCurrent = isPaid,
Date = ""
},
new()
{
Title = "ارسال شده",
Description = "سفارش شما به پست تحویل داده شد",
IsCompleted = false,
IsCurrent = false,
Date = ""
},
new()
{
Title = "تحویل داده شده",
Description = "سفارش به دست شما رسید",
IsCompleted = false,
IsCurrent = false,
Date = ""
}
};
}
private static string FormatDate(Google.Protobuf.WellKnownTypes.Timestamp? timestamp)
{
if (timestamp is null) return "";
var date = timestamp.ToDateTime();
var persianCalendar = new System.Globalization.PersianCalendar();
return $"{persianCalendar.GetYear(date)}/{persianCalendar.GetMonth(date):00}/{persianCalendar.GetDayOfMonth(date):00}";
}
private static string GetPaymentMethodText(PaymentMethod method) => method switch
{
PaymentMethod.Ipg => "پرداخت آنلاین",
PaymentMethod.Wallet => "کیف پول",
_ => "نامشخص"
};
private class TrackingStep
{
public string Title { get; set; } = "";
public string Description { get; set; } = "";
public bool IsCompleted { get; set; }
public bool IsCurrent { get; set; }
public string Date { get; set; } = "";
}
}

View File

@@ -0,0 +1,121 @@
using FrontOffice.BFF.Package.Protobuf.Protos.Package;
namespace FrontOffice.Main.Utilities;
/// <summary>
/// Package data transfer object for UI display
/// </summary>
public record PackageDto(
long Id,
string Title,
string Description,
string ImageUrl,
long Price)
{
public string FormattedPrice => string.Format("{0:N0} تومان", Price);
}
/// <summary>
/// User's package purchase status
/// </summary>
public record UserPackageStatusDto(
bool HasPurchasedPackage,
string? PurchaseMethod, // DayaLoan, DirectPurchase, or null
bool IsClubMemberActive,
long WalletBalance,
bool CanActivateClubMembership,
DateTime? PurchaseDate);
/// <summary>
/// Service for managing packages in FrontOffice
/// </summary>
public class PackageService
{
private readonly PackageContract.PackageContractClient _client;
public PackageService(PackageContract.PackageContractClient client)
{
_client = client;
}
/// <summary>
/// Get all available packages
/// </summary>
public async Task<List<PackageDto>> GetAllPackagesAsync(CancellationToken ct = default)
{
try
{
var request = new GetAllPackageByFilterRequest
{
PaginationState = new PaginationState
{
PageNumber = 1,
PageSize = 100
}
};
var response = await _client.GetAllPackageByFilterAsync(request, cancellationToken: ct);
return response.Models
.Select(m => new PackageDto(
m.Id,
m.Title,
m.Description,
UrlUtility.DownloadUrl + m.ImagePath,
m.Price))
.ToList();
}
catch (Exception ex)
{
#if DEBUG
Console.WriteLine($"Error fetching packages: {ex.Message}");
#endif
return new List<PackageDto>();
}
}
/// <summary>
/// Get a single package by ID
/// </summary>
public async Task<PackageDto?> GetPackageByIdAsync(long id, CancellationToken ct = default)
{
try
{
var response = await _client.GetPackageAsync(
new GetPackageRequest { Id = id },
cancellationToken: ct);
if (response == null) return null;
return new PackageDto(
response.Id,
response.Title,
response.Description,
UrlUtility.DownloadUrl + response.ImagePath,
response.Price);
}
catch (Exception ex)
{
#if DEBUG
Console.WriteLine($"Error fetching package {id}: {ex.Message}");
#endif
return null;
}
}
/// <summary>
/// Get user's package purchase status (Mock for now - needs BFF implementation)
/// </summary>
public Task<UserPackageStatusDto> GetUserPackageStatusAsync(CancellationToken ct = default)
{
// TODO: Connect to GetUserPackageStatus RPC when available in FrontOffice.BFF
// For now, return a mock status
return Task.FromResult(new UserPackageStatusDto(
HasPurchasedPackage: false,
PurchaseMethod: null,
IsClubMemberActive: false,
WalletBalance: 0,
CanActivateClubMembership: false,
PurchaseDate: null));
}
}

View File

@@ -18,6 +18,7 @@ public static class RouteConstants
public const string Personal = "/profile/personal"; public const string Personal = "/profile/personal";
public const string Addresses = "/profile/addresses"; public const string Addresses = "/profile/addresses";
public const string Settings = "/profile/settings"; public const string Settings = "/profile/settings";
public const string ChangePassword = "/profile/change-password";
public const string Tree = "/profile/tree"; public const string Tree = "/profile/tree";
public const string Wallet = "/profile/wallet"; public const string Wallet = "/profile/wallet";
} }
@@ -43,7 +44,10 @@ public static class RouteConstants
public static class Package public static class Package
{ {
public const string List = "/packages";
public const string Detail = "/package/"; public const string Detail = "/package/";
public const string MyPackages = "/my-packages";
public const string Purchase = "/purchase-package/";
} }
public static class About public static class About
@@ -74,6 +78,7 @@ public static class RouteConstants
public const string CheckoutSummary = "/checkout-summary"; public const string CheckoutSummary = "/checkout-summary";
public const string Orders = "/orders"; public const string Orders = "/orders";
public const string OrderDetail = "/order/"; // usage: /order/{id} public const string OrderDetail = "/order/"; // usage: /order/{id}
public const string OrderTracking = "/order-tracking/"; // usage: /order-tracking/{id}
public const string Categories = "/categories"; public const string Categories = "/categories";
} }
} }

View File

@@ -1,5 +1,5 @@
{ {
"GwUrl": "https://fogw.kbs1.ir", "GwUrl": "http://backoffice-bff-svc",
// "GwUrl": "https://localhost:34781", // "GwUrl": "https://localhost:34781",
"DownloadUrl": "https://dl.afrino.co", "DownloadUrl": "https://dl.afrino.co",
"EncryptionSettings": { "EncryptionSettings": {