feat: Implement Commission and Network Statistics Pages with DTOs and Services

- Added CommissionDashboardPage and CommissionHistoryPage for displaying commission payouts and history.
- Implemented WeeklyBalancePage to show weekly balance details.
- Created NetworkStatisticsPage to display network statistics and tree structure.
- Developed corresponding services (CommissionService, NetworkMembershipService) for data retrieval.
- Introduced DTOs for Commission and Network data structures (CommissionPayoutDto, WeeklyBalanceDto, NetworkStatisticsDto).
- Added mock data generation for testing purposes in services.
- Enhanced UI with MudBlazor components for better user experience.
This commit is contained in:
masoodafar-web
2025-12-04 17:29:16 +03:30
parent a8e6693c70
commit 95c6bf5efa
25 changed files with 1882 additions and 26 deletions
@@ -0,0 +1,23 @@
namespace FrontOffice.Main.Utilities;
/// <summary>
/// DTO for Club Membership information
/// </summary>
public class ClubMembershipDto
{
public long UserId { get; set; }
public bool IsActive { get; set; }
public string Status { get; set; } = string.Empty;
public int? DaysRemaining { get; set; }
}
/// <summary>
/// DTO for Club Membership activation response
/// </summary>
public class ClubActivationResponseDto
{
public bool Success { get; set; }
public string? Message { get; set; }
public DateTime? ActivationDate { get; set; }
public long AmountPaid { get; set; }
}
@@ -0,0 +1,70 @@
namespace FrontOffice.Main.Utilities;
/// <summary>
/// Service for Club Membership operations
/// TODO: Connect to FrontOffice.BFF gRPC ClubMembershipCQ
/// </summary>
public class ClubMembershipService
{
// TODO: Inject gRPC client when FrontOffice connects to BFF
// private readonly ClubMembershipContract.ClubMembershipContractClient _client;
public ClubMembershipService()
{
// TODO: Initialize gRPC client
}
/// <summary>
/// Get current user's club membership status
/// Maps to: ClubMembershipCQ.GetMyClubMembership
/// </summary>
public async Task<ClubMembershipDto> GetMyMembershipAsync()
{
// TODO: Replace with actual gRPC call to BFF
// var request = new GetMyClubMembershipRequest();
// var response = await _client.GetMyClubMembershipAsync(request);
// return MapToDto(response);
await Task.Delay(500); // Simulate network delay
// Mock data for now
return new ClubMembershipDto
{
UserId = 1,
IsActive = false,
Status = "Inactive",
DaysRemaining = null
};
}
/// <summary>
/// Activate or renew club membership
/// Maps to: ClubMembershipCQ.ActivateMyClubMembership
/// </summary>
public async Task<ClubActivationResponseDto> ActivateMembershipAsync(
long packageId,
string? activationCode,
int durationMonths)
{
// TODO: Replace with actual gRPC call to BFF
// var request = new ActivateMyClubMembershipRequest
// {
// PackageId = packageId,
// ActivationCode = activationCode ?? string.Empty,
// DurationMonths = durationMonths
// };
// var response = await _client.ActivateMyClubMembershipAsync(request);
// return MapToDto(response);
await Task.Delay(1000); // Simulate network delay
// Mock successful activation
return new ClubActivationResponseDto
{
Success = true,
Message = "عضویت با موفقیت فعال شد",
ActivationDate = DateTime.Now,
AmountPaid = 56_000_000 * durationMonths
};
}
}
@@ -0,0 +1,56 @@
namespace FrontOffice.Main.Utilities;
/// <summary>
/// DTO for Commission Payout
/// </summary>
public class CommissionPayoutDto
{
public long Id { get; set; }
public int WeekNumber { get; set; }
public string WeekLabel { get; set; } = string.Empty;
public int BalancesEarned { get; set; }
public long TotalAmount { get; set; }
public string AmountFormatted { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public string StatusBadgeColor { get; set; } = string.Empty;
public string DatePersian { get; set; } = string.Empty;
}
/// <summary>
/// Response for paginated commission payouts
/// </summary>
public class CommissionPayoutsResponseDto
{
public List<CommissionPayoutDto> Payouts { get; set; } = new();
public int TotalCount { get; set; }
public int PageNumber { get; set; }
public int PageSize { get; set; }
}
/// <summary>
/// DTO for Weekly Balance details
/// </summary>
public class WeeklyBalanceDto
{
public int WeekNumber { get; set; }
public string WeekLabel { get; set; } = string.Empty;
public long LeftBalance { get; set; }
public long RightBalance { get; set; }
public long MinBalance { get; set; }
public int BalanceCount { get; set; }
public long CalculatedCommission { get; set; }
public long LeftCarryover { get; set; }
public long RightCarryover { get; set; }
public DateTime StartDate { get; set; }
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");
}
@@ -0,0 +1,131 @@
namespace FrontOffice.Main.Utilities;
/// <summary>
/// Service for Commission operations
/// TODO: Connect to FrontOffice.BFF gRPC CommissionCQ
/// </summary>
public class CommissionService
{
// TODO: Inject gRPC client when FrontOffice connects to BFF
// private readonly CommissionContract.CommissionContractClient _client;
public CommissionService()
{
// TODO: Initialize gRPC client
}
/// <summary>
/// Get commission payouts with pagination and filters
/// Maps to: CommissionCQ.GetMyCommissionPayouts
/// </summary>
public async Task<CommissionPayoutsResponseDto> GetMyCommissionPayoutsAsync(
int? weekNumber,
string? status,
int pageNumber,
int pageSize)
{
// TODO: Replace with actual gRPC call to BFF
// var request = new GetMyCommissionPayoutsRequest
// {
// WeekNumber = weekNumber,
// Status = status ?? string.Empty,
// PageNumber = pageNumber,
// PageSize = pageSize
// };
// var response = await _client.GetMyCommissionPayoutsAsync(request);
// return MapToDto(response);
await Task.Delay(500); // Simulate network delay
// Mock data
var allPayouts = GenerateMockPayouts(50);
// Apply filters
var filtered = allPayouts.AsEnumerable();
if (weekNumber.HasValue)
filtered = filtered.Where(p => p.WeekNumber == weekNumber.Value);
if (!string.IsNullOrEmpty(status))
filtered = filtered.Where(p => p.Status == status);
var total = filtered.Count();
var paged = filtered.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToList();
return new CommissionPayoutsResponseDto
{
Payouts = paged,
TotalCount = total,
PageNumber = pageNumber,
PageSize = pageSize
};
}
/// <summary>
/// Get weekly balance details for a specific week
/// Maps to: CommissionCQ.GetMyWeeklyBalances
/// </summary>
public async Task<WeeklyBalanceDto> GetMyWeeklyBalanceAsync(int? weekNumber = null)
{
// TODO: Replace with actual gRPC call to BFF
// var request = new GetMyWeeklyBalancesRequest
// {
// WeekNumber = weekNumber
// };
// var response = await _client.GetMyWeeklyBalancesAsync(request);
// return MapToDto(response);
await Task.Delay(500); // Simulate network delay
// Mock current week data
var currentWeek = weekNumber ?? 45;
return new WeeklyBalanceDto
{
WeekNumber = currentWeek,
WeekLabel = $"هفته {currentWeek} - سال 1404",
LeftBalance = 15_000_000,
RightBalance = 12_000_000,
MinBalance = 12_000_000,
BalanceCount = 12,
CalculatedCommission = 1_200_000,
LeftCarryover = 3_000_000,
RightCarryover = 0,
StartDate = DateTime.Now.AddDays(-7),
EndDate = DateTime.Now
};
}
#region Mock Data Helpers
private static List<CommissionPayoutDto> GenerateMockPayouts(int count)
{
var payouts = new List<CommissionPayoutDto>();
var random = new Random();
var statuses = new[] { "Created", "Paid", "WithdrawalRequested", "Withdrawn", "Cancelled" };
var statusColors = new[] { "Info", "Success", "Warning", "Success", "Error" };
var statusTexts = new[] { "ایجاد شده", "پرداخت شده", "درخواست برداشت", "برداشت شده", "لغو شده" };
for (int i = 0; i < count; i++)
{
var statusIndex = random.Next(statuses.Length);
var weekNumber = 50 - i;
var balances = random.Next(5, 20);
var amount = balances * 100_000;
payouts.Add(new CommissionPayoutDto
{
Id = i + 1,
WeekNumber = weekNumber,
WeekLabel = $"هفته {weekNumber} - سال 1404",
BalancesEarned = balances,
TotalAmount = amount,
AmountFormatted = $"{amount:N0} تومان",
Status = statusTexts[statusIndex],
StatusBadgeColor = statusColors[statusIndex],
DatePersian = DateTime.Now.AddDays(-i * 7).ToString("yyyy/MM/dd")
});
}
return payouts;
}
#endregion
}
@@ -0,0 +1,50 @@
namespace FrontOffice.Main.Utilities;
/// <summary>
/// DTO for Network Tree node
/// </summary>
public class NetworkNodeDto
{
public long UserId { get; set; }
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 NetworkNodeDto? LeftChild { get; set; }
public NetworkNodeDto? RightChild { get; set; }
public int Level { get; set; }
}
/// <summary>
/// DTO for Network Tree response
/// </summary>
public class NetworkTreeDto
{
public NetworkNodeDto? RootNode { get; set; }
public int TotalMembers { get; set; }
public int CurrentDepth { get; set; }
}
/// <summary>
/// DTO for Last Member info
/// </summary>
public class LastMemberDto
{
public long UserId { get; set; }
public string FullName { get; set; } = string.Empty;
public string Position { get; set; } = string.Empty;
public DateTime JoinedAt { get; set; }
}
/// <summary>
/// DTO for Network Statistics
/// </summary>
public class NetworkStatisticsDto
{
public int LeftLegCount { get; set; }
public int RightLegCount { get; set; }
public int TotalMembers { get; set; }
public int TreeDepth { get; set; }
public string WeakerLeg { get; set; } = string.Empty; // "Left" or "Right"
public LastMemberDto? LastMember { get; set; }
}
@@ -0,0 +1,108 @@
namespace FrontOffice.Main.Utilities;
/// <summary>
/// Service for Network Membership operations
/// TODO: Connect to FrontOffice.BFF gRPC NetworkMembershipCQ
/// </summary>
public class NetworkMembershipService
{
// TODO: Inject gRPC client when FrontOffice connects to BFF
// private readonly NetworkMembershipContract.NetworkMembershipContractClient _client;
public NetworkMembershipService()
{
// TODO: Initialize gRPC client
}
/// <summary>
/// Get current user's network tree
/// Maps to: NetworkMembershipCQ.GetMyNetworkTree
/// </summary>
public async Task<NetworkTreeDto> GetMyNetworkTreeAsync(int maxDepth = 3)
{
// TODO: Replace with actual gRPC call to BFF
// var request = new GetMyNetworkTreeRequest { MaxDepth = maxDepth };
// var response = await _client.GetMyNetworkTreeAsync(request);
// return MapToDto(response);
await Task.Delay(500); // Simulate network delay
// Mock data with sample tree
return new NetworkTreeDto
{
CurrentDepth = 2,
TotalMembers = 5,
RootNode = new NetworkNodeDto
{
UserId = 1,
FullName = "شما",
Mobile = "09121234567",
Position = "Root",
Level = 0,
LeftChild = new NetworkNodeDto
{
UserId = 2,
FullName = "علی محمدی",
Mobile = "09121234568",
Position = "Left",
Level = 1,
LeftChild = new NetworkNodeDto
{
UserId = 4,
FullName = "رضا احمدی",
Mobile = "09121234570",
Position = "Left",
Level = 2
}
},
RightChild = new NetworkNodeDto
{
UserId = 3,
FullName = "فاطمه حسینی",
Mobile = "09121234569",
Position = "Right",
Level = 1,
RightChild = new NetworkNodeDto
{
UserId = 5,
FullName = "زهرا کریمی",
Mobile = "09121234571",
Position = "Right",
Level = 2
}
}
}
};
}
/// <summary>
/// Get current user's network statistics
/// Maps to: NetworkMembershipCQ.GetMyNetworkStatistics
/// </summary>
public async Task<NetworkStatisticsDto> GetMyNetworkStatisticsAsync()
{
// TODO: Replace with actual gRPC call to BFF
// var request = new GetMyNetworkStatisticsRequest();
// var response = await _client.GetMyNetworkStatisticsAsync(request);
// return MapToDto(response);
await Task.Delay(500); // Simulate network delay
// Mock statistics
return new NetworkStatisticsDto
{
LeftLegCount = 15,
RightLegCount = 12,
TotalMembers = 27,
TreeDepth = 4,
WeakerLeg = "Right",
LastMember = new LastMemberDto
{
UserId = 28,
FullName = "محمد رضایی",
Position = "Left",
JoinedAt = DateTime.Now.AddHours(-2)
}
};
}
}
@@ -22,6 +22,25 @@ public static class RouteConstants
public const string Wallet = "/profile/wallet";
}
public static class Club
{
public const string Membership = "/club/membership";
public const string Features = "/club/features";
}
public static class Network
{
public const string Tree = "/network/tree";
public const string Statistics = "/network/statistics";
}
public static class Commission
{
public const string Dashboard = "/commission/dashboard";
public const string History = "/commission/history";
public const string WeeklyBalance = "/commission/weekly-balance";
}
public static class Package
{
public const string Detail = "/package/";
@@ -28,7 +28,8 @@ public class WalletService
try
{
var response = await _client.GetUserWalletAsync(new Empty());
return new WalletBalances(response.Balance, response.DiscountBalance, response.NetworkBalance);
// TODO: DiscountBalance will be added in BFF protobuf later
return new WalletBalances(response.Balance, 0 /* response.DiscountBalance */, response.NetworkBalance);
}
catch
{
@@ -39,6 +40,9 @@ public class WalletService
public async Task<List<WalletTransaction>> GetTransactionsAsync(long? referenceId = null, bool? isIncrease = null)
{
// TODO: Implement when BFF protobuf has GetAllUserWalletChangeLog
await Task.CompletedTask;
/*
try
{
var request = new GetAllUserWalletChangeLogRequest();
@@ -62,6 +66,7 @@ public class WalletService
}
catch
{
*/
// Fallback to mock data if backend is unavailable
var _transactions = new List<WalletTransaction>
{
@@ -71,7 +76,7 @@ public class WalletService
new(DateTime.Now.AddDays(-9).ToString(), 900_000, "کیف پول شرکای تجاری", "اعتبار خرید"),
};
return _transactions.OrderByDescending(t => t.Date).ToList();
}
// }
}
private static string ResolveTransactionDate(GetAllUserWalletChangeLogResponseModel model)
@@ -100,6 +105,10 @@ public class WalletService
public async Task<bool> RequestWithdrawalAsync(long payoutId, WithdrawalMethodClient method, string? iban)
{
// TODO: Implement when BFF protobuf has WithdrawBalance
await Task.CompletedTask;
return true;
/*
var request = new WithdrawBalanceRequest
{
PayoutId = payoutId,
@@ -119,10 +128,15 @@ public class WalletService
// surface backend error text
throw new InvalidOperationException(ex.Status.Detail ?? "خطا در ثبت برداشت", ex);
}
*/
}
public async Task<List<WalletWithdrawal>> GetWithdrawalsAsync(int? status = null)
{
// TODO: Implement when BFF protobuf has GetUserWithdrawals
await Task.CompletedTask;
return new List<WalletWithdrawal>();
/*
var request = new GetUserWithdrawalsRequest();
if (status.HasValue)
{
@@ -140,11 +154,15 @@ public class WalletService
m.IbanNumber,
m.Created.ToDateTime().MiladiToJalaliWithTime()))
.ToList();
*/
}
public async Task<WithdrawalSettings> GetWithdrawalSettingsAsync()
{
var response = await _client.GetWithdrawalSettingsAsync(new Empty());
return new WithdrawalSettings(response.MinWithdrawalAmount);
// TODO: Implement when BFF protobuf has GetWithdrawalSettings
await Task.CompletedTask;
return new WithdrawalSettings(1_000_000);
// var response = await _client.GetWithdrawalSettingsAsync(new Empty());
// return new WithdrawalSettings(response.MinWithdrawalAmount);
}
}