0bbc11e82c
- Checkout.razor.cs: Rewrite ProcessPayment() to use CustomerPurchasePackageAsync instead of disabled dead code (old TransactionsContract + UserOrderContract flow removed) - Callback URL now points to /profile/payment-callback (reuses existing verify page with CustomerVerifyPackagePurchase) - Remove unused DI: UserOrderContract, TransactionContract - Remove unused usings: Transactions, UserOrder, WellKnownTypes - Profile/Index.razor.cs: Rename basePackage → selectedPackage, tempCallbackUrl → callbackUrl (cosmetic) - Build: 0 errors
503 lines
19 KiB
C#
503 lines
19 KiB
C#
using FluentValidation;
|
||
|
||
using FrontOffice.Main.Pages.Profile.Components;
|
||
using FrontOffice.Main.Utilities;
|
||
using Mapster;
|
||
using Microsoft.AspNetCore.Components;
|
||
using Microsoft.JSInterop;
|
||
using MudBlazor;
|
||
using System.ComponentModel.DataAnnotations;
|
||
using CMSMicroservice.Protobuf.Protos.Package;
|
||
using CMSMicroservice.Protobuf.Protos.User;
|
||
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
||
using CMSMicroservice.Protobuf.Validator.User;
|
||
using CMSMicroservice.Protobuf.Validator.UserAddress;
|
||
using Severity = MudBlazor.Severity;
|
||
|
||
namespace FrontOffice.Main.Pages.Profile;
|
||
|
||
public partial class Index
|
||
{
|
||
[Inject] private UserContract.UserContractClient UserContract { get; set; } = default!;
|
||
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
||
[Inject] private PackageContract.PackageContractClient PackageContract { get; set; } = default!;
|
||
[Inject] private AuthService AuthService { get; set; } = default!;
|
||
[Inject] private BlogPostService BlogPostService { get; set; } = default!;
|
||
|
||
private GetUserForCustomerResponse _userProfile = new();
|
||
private BlogPostCardDto? _latestPost;
|
||
private UpdateCustomerProfileRequest _updateUserRequest = new();
|
||
|
||
private MudForm? _personalForm;
|
||
|
||
private bool _isPersonalSaving;
|
||
private bool _isSettingsSaving;
|
||
|
||
private string _copyMessage = string.Empty;
|
||
|
||
private DateTime? _date;
|
||
|
||
// Referral Link Control
|
||
private bool _hasPurchasedPackage;
|
||
private bool _isClubMemberActive;
|
||
private bool CanShowReferralLink => _hasPurchasedPackage && _isClubMemberActive;
|
||
|
||
// Purchase Options Bottom Sheet
|
||
private bool _showPurchaseBottomSheet;
|
||
private bool _isProcessingPayment;
|
||
private int _purchaseCycleCount; // تعداد دورهای خرید — اگر > 0 فقط IPG مجاز
|
||
|
||
// Dashboard Tiles
|
||
private readonly List<DashTile> _dashTiles = new()
|
||
{
|
||
new(RouteConstants.Profile.Personal, Icons.Material.Filled.Person, "اطلاعات شخصی", "نمایش و ویرایش", "background:rgba(99,102,241,.12); color:#6366f1;"),
|
||
new(RouteConstants.Profile.Addresses, Icons.Material.Filled.LocationOn, "آدرسها", "مدیریت آدرسها", "background:rgba(99,102,241,.12); color:#6366f1;"),
|
||
new(RouteConstants.Profile.Tree, Icons.Material.Filled.AccountTree, "شجرهنامه", "ساختار تیم", "background:rgba(99,102,241,.12); color:#6366f1;"),
|
||
new(RouteConstants.Gateway.StoreChooser, Icons.Material.Filled.Store, "فروشگاهها", "ورود به فروشگاه", "background:rgba(16,185,129,.12); color:#10b981;"),
|
||
new(RouteConstants.Profile.Wallet, Icons.Material.Filled.AccountBalanceWallet, "کیف پول", "مدیریت و تاریخچه", "background:rgba(16,185,129,.12); color:#10b981;"),
|
||
new(RouteConstants.Profile.MagicWallet, Icons.Material.Filled.AutoAwesome, "کیفپول جادویی", "شارژ ×2.5", "background:rgba(168,85,247,.12); color:#a855f7;"),
|
||
new(RouteConstants.Profile.WithdrawalRequests, Icons.Material.Filled.RequestPage, "درخواست برداشت", "ثبت و پیگیری", "background:rgba(245,158,11,.12); color:#f59e0b;"),
|
||
new(RouteConstants.Club.Membership, Icons.Material.Filled.CardMembership, "باشگاه مشتریان", "عضویت و مزایا", "background:rgba(168,85,247,.12); color:#a855f7;"),
|
||
new(RouteConstants.Network.Statistics, Icons.Material.Filled.Groups, "آمار باشگاه", "جزئیات باشگاه", "background:rgba(59,130,246,.12); color:#3b82f6;"),
|
||
new(RouteConstants.Commission.Dashboard, Icons.Material.Filled.Payments, "پاداش", "درآمد و تاریخچه", "background:rgba(16,185,129,.12); color:#10b981;"),
|
||
};
|
||
|
||
private record DashTile(string Href, string Icon, string Title, string Subtitle, string AvatarStyle);
|
||
|
||
// Address management
|
||
private List<CustomerAddressModel> _addresses = new();
|
||
private bool _isLoadingAddresses;
|
||
private CreateNewUserAddressRequest _newAddressRequest = new();
|
||
private UpdateUserAddressRequest _editAddressRequest = new();
|
||
private MudForm? _addressForm;
|
||
private readonly CreateNewUserAddressRequestValidator _addressValidator = new();
|
||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||
{
|
||
await base.OnAfterRenderAsync(firstRender);
|
||
if (firstRender)
|
||
{
|
||
// Refresh token to get latest user claims
|
||
await AuthService.RefreshTokenAsync();
|
||
|
||
await LoadUserAuthInfo();
|
||
await LoadUserProfile();
|
||
await LoadAddresses();
|
||
await LoadWallet();
|
||
await LoadLatestPost();
|
||
|
||
// نمایش Modal قرارداد باشگاه اگر پکیج خریده ولی باشگاه فعال نشده
|
||
await CheckAndShowClubContractModal();
|
||
}
|
||
}
|
||
|
||
private async Task LoadUserAuthInfo()
|
||
{
|
||
try
|
||
{
|
||
var userInfo = await AuthService.GetUserAuthInfo();
|
||
_hasPurchasedPackage = userInfo.HasPurchasedPackage;
|
||
_isClubMemberActive = userInfo.IsClubMemberActive;
|
||
}
|
||
catch
|
||
{
|
||
_hasPurchasedPackage = false;
|
||
_isClubMemberActive = false;
|
||
}
|
||
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 = "-";
|
||
private async Task LoadWallet()
|
||
{
|
||
var b = await WalletService.GetBalancesAsync();
|
||
_walletCredit = FormatPrice(b.CreditBalance);
|
||
var discount = FormatPrice(b.DiscountBalance);
|
||
_walletNetwork = FormatPrice(b.NetworkBalance);
|
||
_walletDiscount = discount;
|
||
|
||
// بارگذاری تعداد دورهای خرید برای کنترل روشهای پرداخت
|
||
var magicStatus = await WalletService.GetMagicWalletStatusAsync();
|
||
_purchaseCycleCount = magicStatus.PurchaseCycleCount;
|
||
|
||
StateHasChanged();
|
||
}
|
||
|
||
private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price);
|
||
|
||
private async Task LoadLatestPost()
|
||
{
|
||
try
|
||
{
|
||
var posts = await BlogPostService.GetFeaturedPostsAsync(1);
|
||
if (!posts.Any())
|
||
{
|
||
var result = await BlogPostService.GetPublishedPostsAsync(page: 1, pageSize: 1);
|
||
posts = result.Posts;
|
||
}
|
||
_latestPost = posts.FirstOrDefault();
|
||
}
|
||
catch
|
||
{
|
||
_latestPost = null;
|
||
}
|
||
StateHasChanged();
|
||
}
|
||
|
||
private async Task LoadUserProfile()
|
||
{
|
||
try
|
||
{
|
||
_userProfile = await UserContract.GetUserForCustomerAsync(new());
|
||
_updateUserRequest = new UpdateCustomerProfileRequest
|
||
{
|
||
FirstName = _userProfile.FirstName,
|
||
LastName = _userProfile.LastName,
|
||
Email = _userProfile.Email,
|
||
NationalCode = _userProfile.NationalCode,
|
||
};
|
||
if (_userProfile.BirthDate != null)
|
||
{
|
||
_date = _userProfile.BirthDate.ToDateTime();
|
||
_updateUserRequest.BirthDate = _userProfile.BirthDate;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// Handle the case when user is not authenticated or API fails
|
||
_userProfile = new GetUserForCustomerResponse();
|
||
}
|
||
StateHasChanged();
|
||
}
|
||
|
||
private async Task SavePersonalInfo()
|
||
{
|
||
if (_personalForm is null) return;
|
||
|
||
await _personalForm.Validate();
|
||
if (!_personalForm.IsValid) return;
|
||
|
||
_isPersonalSaving = true;
|
||
|
||
try
|
||
{
|
||
if (!string.IsNullOrWhiteSpace(_updateUserRequest.NationalCode))
|
||
_updateUserRequest.NationalCode = _updateUserRequest.NationalCode.PersianToEnglish();
|
||
|
||
if (_date != null)
|
||
_updateUserRequest.BirthDate = _date.Value.DateTimeToTimestamp();
|
||
|
||
await UserContract.UpdateCustomerProfileAsync(request: _updateUserRequest);
|
||
await LoadUserProfile();
|
||
Snackbar.Add("اطلاعات شخصی با موفقیت ذخیره شد.", Severity.Success);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Snackbar.Add($"خطا در ذخیره اطلاعات: {ex.Message}", Severity.Error);
|
||
}
|
||
finally
|
||
{
|
||
_isPersonalSaving = false;
|
||
await InvokeAsync(StateHasChanged);
|
||
}
|
||
}
|
||
|
||
|
||
private async Task SaveSettings()
|
||
{
|
||
_isSettingsSaving = true;
|
||
|
||
try
|
||
{
|
||
await SavePersonalInfo();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Snackbar.Add($"خطا در ذخیره تنظیمات: {ex.Message}", Severity.Error);
|
||
}
|
||
finally
|
||
{
|
||
_isSettingsSaving = false;
|
||
await InvokeAsync(StateHasChanged);
|
||
}
|
||
}
|
||
|
||
private async Task CopyReferralCode()
|
||
{
|
||
try
|
||
{
|
||
await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", $"{Navigation.BaseUri}?ref={_userProfile.ReferralCode}");
|
||
_copyMessage = "کد دعوت کپی شد!";
|
||
Snackbar.Add("کد دعوت در کلیپبورد کپی شد.", Severity.Success);
|
||
|
||
// Clear message after 3 seconds
|
||
await Task.Delay(3000);
|
||
_copyMessage = string.Empty;
|
||
await InvokeAsync(StateHasChanged);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Snackbar.Add("خطا در کپی کردن کد دعوت.", Severity.Error);
|
||
}
|
||
}
|
||
|
||
private async Task ShareReferralCode()
|
||
{
|
||
var shareText = $"کد دعوت من در کارا بازار سلامت: {_userProfile.ReferralCode}\nبرای عضویت از این لینک استفاده کنید:";
|
||
|
||
try
|
||
{
|
||
// Try to use Web Share API if available
|
||
await JSRuntime.InvokeVoidAsync("navigator.share",
|
||
new
|
||
{
|
||
title = "کد دعوت کارا بازار سلامت",
|
||
text = shareText,
|
||
url = $"{Navigation.BaseUri}?ref={_userProfile.ReferralCode}"
|
||
});
|
||
}
|
||
catch
|
||
{
|
||
// Fallback: copy to clipboard
|
||
await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", $"{Navigation.BaseUri}?ref={_userProfile.ReferralCode}");
|
||
Snackbar.Add("لینک دعوت در کلیپبورد کپی شد.", Severity.Success);
|
||
}
|
||
}
|
||
|
||
// Address management methods
|
||
private async Task LoadAddresses()
|
||
{
|
||
_isLoadingAddresses = true;
|
||
try
|
||
{
|
||
var response = await UserAddressContract.GetCustomerAddressesAsync(request: new());
|
||
if (response?.Models?.Any() == true)
|
||
{
|
||
_addresses = response.Models.ToList();
|
||
}
|
||
else
|
||
{
|
||
_addresses = new List<CustomerAddressModel>();
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Snackbar.Add($"خطا در بارگذاری آدرسها: {ex.Message}", Severity.Error);
|
||
_addresses = new List<CustomerAddressModel>();
|
||
}
|
||
finally
|
||
{
|
||
_isLoadingAddresses = false;
|
||
await InvokeAsync(StateHasChanged);
|
||
}
|
||
}
|
||
|
||
private async Task OpenAddAddressDialog()
|
||
{
|
||
var dialog = await DialogService.ShowAsync<AddAddressDialog>("افزودن آدرس جدید");
|
||
var result = await dialog.Result;
|
||
|
||
if (!result.Canceled)
|
||
{
|
||
await LoadAddresses();
|
||
}
|
||
}
|
||
|
||
private async Task OpenEditAddressDialog(CustomerAddressModel address)
|
||
{
|
||
var dialog = await DialogService.ShowAsync<EditAddressDialog>("ویرایش آدرس", new DialogParameters<EditAddressDialog>
|
||
{
|
||
{ x => x.Model, address }
|
||
});
|
||
var result = await dialog.Result;
|
||
|
||
if (!result.Canceled)
|
||
{
|
||
await LoadAddresses();
|
||
}
|
||
}
|
||
|
||
private async Task SetAsDefaultAddress(long addressId)
|
||
{
|
||
try
|
||
{
|
||
await UserAddressContract.SetAddressAsDefaultAsync(request: new()
|
||
{
|
||
Id = addressId
|
||
});
|
||
Snackbar.Add("آدرس پیشفرض با موفقیت تغییر کرد.", Severity.Success);
|
||
await LoadAddresses();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Snackbar.Add($"خطا در تغییر آدرس پیشفرض: {ex.Message}", Severity.Error);
|
||
}
|
||
}
|
||
|
||
private async Task DeleteAddress(long addressId)
|
||
{
|
||
var result = await DialogService.ShowMessageBox(
|
||
"تأیید حذف",
|
||
"آیا از حذف این آدرس اطمینان دارید؟",
|
||
yesText: "حذف",
|
||
cancelText: "لغو");
|
||
|
||
if (result == true)
|
||
{
|
||
try
|
||
{
|
||
await UserAddressContract.DeleteUserAddressAsync(request: new()
|
||
{
|
||
Id = addressId
|
||
});
|
||
Snackbar.Add("آدرس با موفقیت حذف شد.", Severity.Success);
|
||
await LoadAddresses();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Snackbar.Add($"خطا در حذف آدرس: {ex.Message}", Severity.Error);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void OpenPurchaseOptions()
|
||
{
|
||
_showPurchaseBottomSheet = true;
|
||
}
|
||
|
||
private void ClosePurchaseOptions()
|
||
{
|
||
_showPurchaseBottomSheet = false;
|
||
}
|
||
|
||
private async Task DirectPayment()
|
||
{
|
||
if (_isProcessingPayment) return;
|
||
|
||
_isProcessingPayment = true;
|
||
_showPurchaseBottomSheet = false;
|
||
|
||
try
|
||
{
|
||
// Get available packages and pick the first one
|
||
var packages = await PackageContract.GetCustomerPackagesAsync(new GetCustomerPackagesRequest());
|
||
var selectedPackage = packages?.Models?.FirstOrDefault();
|
||
if (selectedPackage == null)
|
||
{
|
||
Snackbar.Add("پکیجی برای خرید یافت نشد", Severity.Error);
|
||
return;
|
||
}
|
||
|
||
// Call CMS to initiate purchase (UserId from JWT, PackageId explicit)
|
||
var callbackUrl = $"{Navigation.BaseUri}profile/payment-callback";
|
||
|
||
var response = await PackageContract.CustomerPurchasePackageAsync(new CustomerPurchasePackageRequest
|
||
{
|
||
PackageId = selectedPackage.Id,
|
||
CallbackUrl = callbackUrl,
|
||
PurchaseMethod = PurchaseMethodEnum.PurchaseMethodGateway
|
||
});
|
||
|
||
if (!response.Success)
|
||
{
|
||
Snackbar.Add(response.Message ?? "خطا در آغاز فرآیند پرداخت", Severity.Error);
|
||
return;
|
||
}
|
||
|
||
// Redirect to payment gateway
|
||
// ZarinPal appends ?Authority=...&Status=... to callback URL
|
||
// PaymentCallback uses Authority+Status to verify via CustomerVerifyPackagePurchase
|
||
if (!string.IsNullOrEmpty(response.PaymentGatewayUrl))
|
||
{
|
||
Navigation.NavigateTo(response.PaymentGatewayUrl, forceLoad: true);
|
||
}
|
||
else
|
||
{
|
||
Snackbar.Add("خطا در دریافت آدرس درگاه پرداخت", Severity.Error);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Snackbar.Add($"خطا در فرآیند پرداخت: {ex.Message}", Severity.Error);
|
||
}
|
||
finally
|
||
{
|
||
_isProcessingPayment = false;
|
||
await InvokeAsync(StateHasChanged);
|
||
}
|
||
}
|
||
|
||
private async Task DayaLoanPayment()
|
||
{
|
||
// Validate user info before redirecting
|
||
if (string.IsNullOrWhiteSpace(_updateUserRequest.FirstName))
|
||
{
|
||
_showPurchaseBottomSheet = false;
|
||
Snackbar.Add($"لطفا اطلاعات شخصی خود را تکمیل کنید. (نام وارد نشده)", Severity.Error);
|
||
return;
|
||
}
|
||
if (string.IsNullOrWhiteSpace(_updateUserRequest.LastName))
|
||
{
|
||
_showPurchaseBottomSheet = false;
|
||
Snackbar.Add($"لطفا اطلاعات شخصی خود را تکمیل کنید. (نام خانوادگی وارد نشده)", Severity.Error);
|
||
return;
|
||
}
|
||
if (string.IsNullOrWhiteSpace(_updateUserRequest.NationalCode))
|
||
{
|
||
_showPurchaseBottomSheet = false;
|
||
Snackbar.Add($"لطفا اطلاعات شخصی خود را تکمیل کنید. (کدملی وارد نشده)", Severity.Error);
|
||
return;
|
||
}
|
||
if (_updateUserRequest.BirthDate == null)
|
||
{
|
||
_showPurchaseBottomSheet = false;
|
||
Snackbar.Add($"لطفا اطلاعات شخصی خود را تکمیل کنید. (تاریخ تولد وارد نشده)", Severity.Error);
|
||
return;
|
||
}
|
||
if (!_addresses.Any())
|
||
{
|
||
_showPurchaseBottomSheet = false;
|
||
Snackbar.Add($"آدرس محل سکونت شما الزامی است!", Severity.Error);
|
||
return;
|
||
}
|
||
|
||
_showPurchaseBottomSheet = false;
|
||
var url = "https://dayadiamond.ir/profile/creditpurchase/?merchantcode=56146364";
|
||
await JSRuntime.InvokeVoidAsync("open", url, "_blank");
|
||
}
|
||
}
|