Compare commits

..

2 Commits

Author SHA1 Message Date
masoodafar-web c815f8d358 refactor(project): update Protobuf package management and add conditional ProjectReference
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m22s
- Removed the direct PackageReference for Foursat.CMSMicroservice.Protobuf and added a conditional ProjectReference to the CMSMicroservice.Protobuf project for local development.
- This change allows for direct communication with the CMS when the project is available locally, while still maintaining the NuGet package reference for CI/Docker environments.

These updates improve the flexibility of the project setup for different development scenarios.
2026-09-20 23:35:11 +03:30
masoodafar-web 27a3ea76c5 feat(profile): add invitees section and inviter mobile functionality
- Introduced a new section in the Profile hub for viewing invitees, enhancing user engagement and tracking of referrals.
- Added buttons for accessing the invitees page from the Profile index, improving navigation.
- Enhanced the AuthDialog and PhoneVerifyForm components to support inviter mobile input, allowing users to specify who referred them.
- Updated RouteConstants to include a new route for invitees, ensuring proper navigation within the application.

These changes improve the user experience by facilitating better referral tracking and enhancing the profile management features.
2026-09-20 23:33:18 +03:30
9 changed files with 247 additions and 10 deletions
+12 -4
View File
@@ -10,8 +10,7 @@
<ItemGroup>
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
<!-- Local protobuf until 0.0.212 is published to Nexus -->
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.212" />
<PackageReference Include="MudBlazor" Version="8.14.0" />
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="Mapster" Version="7.4.0" />
@@ -23,8 +22,17 @@
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.0" />
</ItemGroup>
<ItemGroup>
<!-- <ProjectReference Include="..\..\..\CMS\src\CMSMicroservice.Protobuf\CMSMicroservice.Protobuf.csproj" /> -->
<!-- ============================================================ -->
<!-- Proto References: Direct reference to CMS Protobuf project -->
<!-- This enables direct communication with CMS (no BFF needed) -->
<!-- ============================================================ -->
<!-- CMS Protobuf: NuGet in CI/Docker; ProjectReference for local monorepo dev -->
<ItemGroup Condition="Exists('../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj')">
<ProjectReference Include="../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj" />
</ItemGroup>
<ItemGroup Condition="!Exists('../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj')">
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.213" />
</ItemGroup>
<!-- Local Proto Project Reference for development -->
@@ -46,6 +46,21 @@
</MudPaper>
</MudLink>
<MudLink Href="@RouteConstants.Profile.Invitees" Underline="Underline.None">
<MudPaper Elevation="0" Class="pa-4 rounded-xl profile-hub-item">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
<MudIcon Icon="@Icons.Material.Outlined.GroupAdd" Color="Color.Info" />
<MudStack Spacing="0">
<MudText Typo="Typo.subtitle2">دعوت‌شدگان من</MudText>
<MudText Typo="Typo.caption" Class="mud-text-secondary">کسانی که با موبایل شما ثبت‌نام کرده‌اند</MudText>
</MudStack>
</MudStack>
<MudIcon Icon="@Icons.Material.Filled.ChevronLeft" Class="mud-text-secondary" />
</MudStack>
</MudPaper>
</MudLink>
<MudLink Href="@RouteConstants.Profile.Settings" Underline="Underline.None">
<MudPaper Elevation="0" Class="pa-4 rounded-xl profile-hub-item">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
@@ -109,6 +109,13 @@
{
<MudText Typo="Typo.caption" Color="Color.Success" Class="mt-2">@(_copyMessage)</MudText>
}
<MudButton Variant="Variant.Text"
Color="Color.Primary"
Class="mt-3"
Href="@RouteConstants.Profile.Invitees"
StartIcon="@Icons.Material.Filled.GroupAdd">
مشاهده دعوت‌شدگان من
</MudButton>
</MudPaper>
}
else
@@ -121,6 +128,13 @@
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-1" Style="max-width:560px; margin:0 auto;">
برای فعال‌سازی لینک دعوت، ابتدا یکی از <strong>پکیج‌ها</strong> را تهیه کنید و سپس در <strong>باشگاه مشتریان</strong> عضو شوید.
</MudText>
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
Class="mt-3"
Href="@RouteConstants.Profile.Invitees"
StartIcon="@Icons.Material.Filled.GroupAdd">
دعوت‌شدگان من
</MudButton>
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-4" Style="max-width:560px; margin:0 auto;">
از مزایای ویژه عضویت بهره‌مند شده و با فعالیت در زمینه توسعه فروشگاه‌ها پاداش دریافت کنید.
</MudText>
@@ -0,0 +1,159 @@
@page "/profile/invitees"
@attribute [Authorize]
@attribute [Route(RouteConstants.Profile.Invitees)]
@using CMSMicroservice.Protobuf.Protos.User
@using CMSMicroservice.Protobuf.Protos.City
@using DateTimeConverterCL
@inject UserContract.UserContractClient UserClient
@inject ISnackbar Snackbar
<PageTitle>دعوت‌شدگان من | کارا بازار سلامت</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-6">
<PageHeader Title="دعوت‌شدگان من" BackHref="@RouteConstants.Profile.Index" />
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-4">
افرادی که با شماره موبایل شما به‌عنوان دعوت‌کننده ثبت‌نام کرده‌اند (متفاوت از زیرمجموعه شبکه).
</MudText>
@if (_loading)
{
<MudStack AlignItems="AlignItems.Center" Class="py-8">
<MudProgressCircular Indeterminate="true" Color="Color.Primary" />
</MudStack>
}
else
{
<MudGrid Spacing="3" Class="mb-4">
<MudItem xs="12" sm="4">
<MudPaper Class="pa-4 rounded-xl" Elevation="1">
<MudText Typo="Typo.caption" Class="mud-text-secondary">کل دعوت‌ها</MudText>
<MudText Typo="Typo.h5">@_stats.TotalInvitees</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="4">
<MudPaper Class="pa-4 rounded-xl" Elevation="1">
<MudText Typo="Typo.caption" Class="mud-text-secondary">فعال در باشگاه</MudText>
<MudText Typo="Typo.h5" Color="Color.Success">@_stats.ClubActiveCount</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="4">
<MudPaper Class="pa-4 rounded-xl" Elevation="1">
<MudText Typo="Typo.caption" Class="mud-text-secondary">غیرفعال باشگاه</MudText>
<MudText Typo="Typo.h5" Color="Color.Warning">@_stats.ClubInactiveCount</MudText>
</MudPaper>
</MudItem>
</MudGrid>
<MudPaper Class="pa-3 rounded-xl mb-3" Elevation="0">
<MudSelect T="string" Label="فیلتر وضعیت باشگاه" Value="_clubFilter" ValueChanged="OnFilterChanged"
Variant="Variant.Outlined" Dense="true" Margin="Margin.Dense">
<MudSelectItem Value="@("ALL")">همه</MudSelectItem>
<MudSelectItem Value="@("ACTIVE")">فعال باشگاه</MudSelectItem>
<MudSelectItem Value="@("INACTIVE")">غیرفعال باشگاه</MudSelectItem>
</MudSelect>
</MudPaper>
@if (!_invitees.Any())
{
<MudAlert Severity="Severity.Info" Dense="true">هنوز کسی با کد دعوت شما ثبت‌نام نکرده است.</MudAlert>
}
else
{
<MudTable Items="_invitees" Hover="true" Dense="true" Breakpoint="Breakpoint.Sm" Class="rounded-xl">
<HeaderContent>
<MudTh>نام</MudTh>
<MudTh>تاریخ ثبت‌نام</MudTh>
<MudTh>باشگاه</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="نام">
@(string.IsNullOrWhiteSpace($"{context.FirstName} {context.LastName}".Trim())
? "—"
: $"{context.FirstName} {context.LastName}".Trim())
</MudTd>
<MudTd DataLabel="تاریخ ثبت‌نام">
@FormatDate(context.RegisteredAt)
</MudTd>
<MudTd DataLabel="باشگاه">
@if (context.IsClubActive)
{
<MudChip T="string" Size="Size.Small" Color="Color.Success" Variant="Variant.Filled">فعال</MudChip>
}
else
{
<MudChip T="string" Size="Size.Small" Color="Color.Default" Variant="Variant.Outlined">غیرفعال</MudChip>
}
</MudTd>
</RowTemplate>
</MudTable>
@if (_totalPages > 1)
{
<MudStack AlignItems="AlignItems.Center" Class="mt-4">
<MudPagination Count="_totalPages" Selected="_page" SelectedChanged="OnPageChanged" Color="Color.Primary" />
</MudStack>
}
}
}
</MudContainer>
@code {
private bool _loading = true;
private string _clubFilter = "ALL";
private int _page = 1;
private const int PageSize = 10;
private int _totalPages;
private InviteeStats _stats = new();
private List<InviteeModel> _invitees = new();
protected override async Task OnInitializedAsync()
{
await LoadAsync();
}
private async Task OnFilterChanged(string value)
{
_clubFilter = value;
_page = 1;
await LoadAsync();
}
private async Task OnPageChanged(int page)
{
_page = page;
await LoadAsync();
}
private async Task LoadAsync()
{
_loading = true;
try
{
var response = await UserClient.GetMyInviteesAsync(new GetMyInviteesRequest
{
PaginationState = new PaginationState { PageNumber = _page, PageSize = PageSize },
ClubStatusFilter = _clubFilter
});
_stats = response.Stats ?? new InviteeStats();
_invitees = response.Invitees.ToList();
_totalPages = (int)(response.MetaData?.TotalPage ?? 0);
if (_totalPages < 1 && _invitees.Count > 0) _totalPages = 1;
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری دعوت‌شدگان: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
private static string FormatDate(Google.Protobuf.WellKnownTypes.Timestamp? ts)
{
if (ts == null) return "—";
return ts.ToDateTime().ToLocalTime().MiladiToJalali();
}
}
+6 -2
View File
@@ -24,7 +24,9 @@
OnChangePhone="ChangePhoneAsync"
OnResendOtp="ResendOtpAsync"
ReferralCode="@_referralCode"
ReferralCodeChanged="OnReferralCodeChanged" />
ReferralCodeChanged="OnReferralCodeChanged"
InviterMobile="@_inviterMobile"
InviterMobileChanged="OnInviterMobileChanged" />
</div>
}
else
@@ -55,7 +57,9 @@ else
OnChangePhone="ChangePhoneAsync"
OnResendOtp="ResendOtpAsync"
ReferralCode="@_referralCode"
ReferralCodeChanged="OnReferralCodeChanged" />
ReferralCodeChanged="OnReferralCodeChanged"
InviterMobile="@_inviterMobile"
InviterMobileChanged="OnInviterMobileChanged" />
<MudStack Class="mt-4" Spacing="2">
@if (_currentStep == AuthStep.Phone)
@@ -44,6 +44,8 @@ public partial class AuthDialog : IDisposable
// Referral code field
private string? _referralCode;
// Inviter mobile (دعوت‌کننده)
private string? _inviterMobile;
[Inject] private ILocalStorageService LocalStorage { get; set; } = default!;
[Inject] private UserContract.UserContractClient UserClient { get; set; } = default!;
@@ -71,6 +73,7 @@ public partial class AuthDialog : IDisposable
}
_referralCode = await LocalStorage.GetItemAsync<string>("referral:code");
_inviterMobile = await LocalStorage.GetItemAsync<string>("inviter:mobile");
// await LocalStorage.RemoveItemAsync(TokenStorageKey);
}
@@ -83,6 +86,15 @@ public partial class AuthDialog : IDisposable
await LocalStorage.RemoveItemAsync("referral:code");
}
private async Task OnInviterMobileChanged(string? value)
{
_inviterMobile = value;
if (!string.IsNullOrWhiteSpace(value))
await LocalStorage.SetItemAsync("inviter:mobile", value);
else
await LocalStorage.RemoveItemAsync("inviter:mobile");
}
private void GenerateCaptcha()
{
_captchaCode = Guid.NewGuid().ToString("N")[..6].ToUpperInvariant();
@@ -204,6 +216,15 @@ public partial class AuthDialog : IDisposable
_verifyRequest.ParentReferralCode = effectiveReferralCode;
}
var storedInviterMobile = await LocalStorage.GetItemAsync<string>("inviter:mobile");
var effectiveInviterMobile = !string.IsNullOrWhiteSpace(_inviterMobile)
? _inviterMobile
: storedInviterMobile;
if (!string.IsNullOrWhiteSpace(effectiveInviterMobile))
{
_verifyRequest.InviterMobile = effectiveInviterMobile;
}
var validationResult = true; // _verifyRequestValidator.Validate(_verifyRequest);
if (!validationResult)
{
@@ -236,6 +257,7 @@ public partial class AuthDialog : IDisposable
await LocalStorage.RemoveItemAsync(PhoneStorageKey);
await LocalStorage.RemoveItemAsync(RedirectStorageKey);
await LocalStorage.RemoveItemAsync("referral:code");
await LocalStorage.RemoveItemAsync("inviter:mobile");
_attemptsLeft = MaxVerificationAttempts;
_verifyRequest.Code = string.Empty;
@@ -38,11 +38,21 @@
<MudTextField Value="ReferralCode"
ValueChanged="@((string? v) => ReferralCodeChanged.InvokeAsync(v))"
Label="کد معرف (اختیاری)"
Label="کد معرف شبکه (اختیاری)"
Variant="Variant.Outlined"
Immediate="true"
Placeholder="کد معرف را وارد کنید"
Class="mb-3" />
Placeholder="کد معرف ۱۰ رقمی"
Class="mb-3"
HelperText="جایگاه شما در شبکه — با لینک معرف پر می‌شود" />
<MudTextField Value="InviterMobile"
ValueChanged="@((string? v) => InviterMobileChanged.InvokeAsync(v))"
Label="شماره موبایل دعوت‌کننده (اختیاری)"
Variant="Variant.Outlined"
Immediate="true"
Placeholder="مثلاً 0912xxxxxxx"
Class="mb-3"
HelperText="کسی که شما را با سیستم آشنا کرده — متفاوت از معرف شبکه" />
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
{
@@ -33,6 +33,10 @@ public partial class PhoneVerifyForm
[Parameter] public string? ReferralCode { get; set; }
[Parameter] public EventCallback<string?> ReferralCodeChanged { get; set; }
// ── Inviter (دعوت‌کننده) ──
[Parameter] public string? InviterMobile { get; set; }
[Parameter] public EventCallback<string?> InviterMobileChanged { get; set; }
// ── Verify actions ──
[Parameter] public EventCallback OnChangePhone { get; set; }
[Parameter] public EventCallback OnResendOtp { get; set; }
@@ -21,6 +21,7 @@ public static class RouteConstants
public const string Settings = "/profile/settings";
public const string ChangePassword = "/profile/change-password";
public const string Tree = "/profile/tree";
public const string Invitees = "/profile/invitees";
public const string Wallet = "/profile/wallet";
public const string MagicWallet = "/profile/magic-wallet";
public const string ChargeDiscountWallet = "/profile/charge-discount-wallet";