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.
This commit is contained in:
masoodafar-web
2026-09-20 23:33:18 +03:30
parent 0d0ca252a6
commit 27a3ea76c5
9 changed files with 235 additions and 6 deletions
+1 -1
View File
@@ -11,7 +11,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" /> <PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
<!-- Local protobuf until 0.0.212 is published to Nexus --> <!-- Local protobuf until 0.0.212 is published to Nexus -->
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.212" /> <PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.213" />
<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" />
<PackageReference Include="Mapster" Version="7.4.0" /> <PackageReference Include="Mapster" Version="7.4.0" />
@@ -46,6 +46,21 @@
</MudPaper> </MudPaper>
</MudLink> </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"> <MudLink Href="@RouteConstants.Profile.Settings" Underline="Underline.None">
<MudPaper Elevation="0" Class="pa-4 rounded-xl profile-hub-item"> <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" Justify="Justify.SpaceBetween">
@@ -109,6 +109,13 @@
{ {
<MudText Typo="Typo.caption" Color="Color.Success" Class="mt-2">@(_copyMessage)</MudText> <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> </MudPaper>
} }
else else
@@ -121,6 +128,13 @@
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-1" Style="max-width:560px; margin:0 auto;"> <MudText Typo="Typo.body2" Class="mud-text-secondary mb-1" Style="max-width:560px; margin:0 auto;">
برای فعال‌سازی لینک دعوت، ابتدا یکی از <strong>پکیج‌ها</strong> را تهیه کنید و سپس در <strong>باشگاه مشتریان</strong> عضو شوید. برای فعال‌سازی لینک دعوت، ابتدا یکی از <strong>پکیج‌ها</strong> را تهیه کنید و سپس در <strong>باشگاه مشتریان</strong> عضو شوید.
</MudText> </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 Typo="Typo.body2" Class="mud-text-secondary mb-4" Style="max-width:560px; margin:0 auto;">
از مزایای ویژه عضویت بهره‌مند شده و با فعالیت در زمینه توسعه فروشگاه‌ها پاداش دریافت کنید. از مزایای ویژه عضویت بهره‌مند شده و با فعالیت در زمینه توسعه فروشگاه‌ها پاداش دریافت کنید.
</MudText> </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" OnChangePhone="ChangePhoneAsync"
OnResendOtp="ResendOtpAsync" OnResendOtp="ResendOtpAsync"
ReferralCode="@_referralCode" ReferralCode="@_referralCode"
ReferralCodeChanged="OnReferralCodeChanged" /> ReferralCodeChanged="OnReferralCodeChanged"
InviterMobile="@_inviterMobile"
InviterMobileChanged="OnInviterMobileChanged" />
</div> </div>
} }
else else
@@ -55,7 +57,9 @@ else
OnChangePhone="ChangePhoneAsync" OnChangePhone="ChangePhoneAsync"
OnResendOtp="ResendOtpAsync" OnResendOtp="ResendOtpAsync"
ReferralCode="@_referralCode" ReferralCode="@_referralCode"
ReferralCodeChanged="OnReferralCodeChanged" /> ReferralCodeChanged="OnReferralCodeChanged"
InviterMobile="@_inviterMobile"
InviterMobileChanged="OnInviterMobileChanged" />
<MudStack Class="mt-4" Spacing="2"> <MudStack Class="mt-4" Spacing="2">
@if (_currentStep == AuthStep.Phone) @if (_currentStep == AuthStep.Phone)
@@ -44,6 +44,8 @@ public partial class AuthDialog : IDisposable
// Referral code field // Referral code field
private string? _referralCode; private string? _referralCode;
// Inviter mobile (دعوت‌کننده)
private string? _inviterMobile;
[Inject] private ILocalStorageService LocalStorage { get; set; } = default!; [Inject] private ILocalStorageService LocalStorage { get; set; } = default!;
[Inject] private UserContract.UserContractClient UserClient { 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"); _referralCode = await LocalStorage.GetItemAsync<string>("referral:code");
_inviterMobile = await LocalStorage.GetItemAsync<string>("inviter:mobile");
// await LocalStorage.RemoveItemAsync(TokenStorageKey); // await LocalStorage.RemoveItemAsync(TokenStorageKey);
} }
@@ -83,6 +86,15 @@ public partial class AuthDialog : IDisposable
await LocalStorage.RemoveItemAsync("referral:code"); 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() private void GenerateCaptcha()
{ {
_captchaCode = Guid.NewGuid().ToString("N")[..6].ToUpperInvariant(); _captchaCode = Guid.NewGuid().ToString("N")[..6].ToUpperInvariant();
@@ -204,6 +216,15 @@ public partial class AuthDialog : IDisposable
_verifyRequest.ParentReferralCode = effectiveReferralCode; _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); var validationResult = true; // _verifyRequestValidator.Validate(_verifyRequest);
if (!validationResult) if (!validationResult)
{ {
@@ -236,6 +257,7 @@ public partial class AuthDialog : IDisposable
await LocalStorage.RemoveItemAsync(PhoneStorageKey); await LocalStorage.RemoveItemAsync(PhoneStorageKey);
await LocalStorage.RemoveItemAsync(RedirectStorageKey); await LocalStorage.RemoveItemAsync(RedirectStorageKey);
await LocalStorage.RemoveItemAsync("referral:code"); await LocalStorage.RemoveItemAsync("referral:code");
await LocalStorage.RemoveItemAsync("inviter:mobile");
_attemptsLeft = MaxVerificationAttempts; _attemptsLeft = MaxVerificationAttempts;
_verifyRequest.Code = string.Empty; _verifyRequest.Code = string.Empty;
@@ -38,11 +38,21 @@
<MudTextField Value="ReferralCode" <MudTextField Value="ReferralCode"
ValueChanged="@((string? v) => ReferralCodeChanged.InvokeAsync(v))" ValueChanged="@((string? v) => ReferralCodeChanged.InvokeAsync(v))"
Label="کد معرف (اختیاری)" Label="کد معرف شبکه (اختیاری)"
Variant="Variant.Outlined" Variant="Variant.Outlined"
Immediate="true" Immediate="true"
Placeholder="کد معرف را وارد کنید" Placeholder="کد معرف ۱۰ رقمی"
Class="mb-3" /> 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)) @if (!string.IsNullOrWhiteSpace(ErrorMessage))
{ {
@@ -33,6 +33,10 @@ public partial class PhoneVerifyForm
[Parameter] public string? ReferralCode { get; set; } [Parameter] public string? ReferralCode { get; set; }
[Parameter] public EventCallback<string?> ReferralCodeChanged { 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 ── // ── Verify actions ──
[Parameter] public EventCallback OnChangePhone { get; set; } [Parameter] public EventCallback OnChangePhone { get; set; }
[Parameter] public EventCallback OnResendOtp { 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 Settings = "/profile/settings";
public const string ChangePassword = "/profile/change-password"; public const string ChangePassword = "/profile/change-password";
public const string Tree = "/profile/tree"; public const string Tree = "/profile/tree";
public const string Invitees = "/profile/invitees";
public const string Wallet = "/profile/wallet"; public const string Wallet = "/profile/wallet";
public const string MagicWallet = "/profile/magic-wallet"; public const string MagicWallet = "/profile/magic-wallet";
public const string ChargeDiscountWallet = "/profile/charge-discount-wallet"; public const string ChargeDiscountWallet = "/profile/charge-discount-wallet";