This commit is contained in:
masoodafar-web
2025-12-02 03:32:04 +03:30
parent aaa15d8839
commit 3bc317b19e
16 changed files with 2973 additions and 557 deletions

View File

@@ -37,9 +37,14 @@
<PackageReference Include="Foursat.BackOffice.BFF.UserOrder.Protobuf" Version="0.0.114" />
<PackageReference Include="Foursat.BackOffice.BFF.UserRole.Protobuf" Version="0.0.111" />
<PackageReference Include="Foursat.BackOffice.BFF.Commission.Protobuf" Version="0.0.2" />
<PackageReference Include="Foursat.BackOffice.BFF.ClubMembership.Protobuf" Version="0.0.1" />
<PackageReference Include="Foursat.BackOffice.BFF.NetworkMembership.Protobuf" Version="0.0.1" />
<!--<PackageReference Include="Foursat.BackOffice.BFF.Commission.Protobuf" Version="0.0.2" />-->
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Commission.Protobuf/BackOffice.BFF.Commission.Protobuf.csproj" />
<!--<PackageReference Include="Foursat.BackOffice.BFF.ClubMembership.Protobuf" Version="0.0.1" />-->
<!--<PackageReference Include="Foursat.BackOffice.BFF.NetworkMembership.Protobuf" Version="0.0.1" />-->
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.ClubMembership.Protobuf/BackOffice.BFF.ClubMembership.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.NetworkMembership.Protobuf/BackOffice.BFF.NetworkMembership.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Configuration.Protobuf/BackOffice.BFF.Configuration.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Health.Protobuf/BackOffice.BFF.Health.Protobuf.csproj" />
<PackageReference Include="HtmlAgilityPack" Version="1.12.1" />
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
<PackageReference Include="Grpc.Core" Version="2.46.6" />

View File

@@ -1,5 +1,4 @@

using BackOffice.BFF.Otp.Protobuf.Protos.Otp;
using BackOffice.BFF.Otp.Protobuf.Protos.Otp;
using BackOffice.BFF.Package.Protobuf.Protos.Package;
using BackOffice.BFF.Role.Protobuf.Protos.Role;
using BackOffice.BFF.Products.Protobuf.Protos.Products;
@@ -11,6 +10,8 @@ using BackOffice.BFF.Category.Protobuf.Protos.Category;
using BackOffice.BFF.Commission.Protobuf;
using BackOffice.BFF.NetworkMembership.Protobuf;
using BackOffice.BFF.ClubMembership.Protobuf;
using BackOffice.BFF.Configuration.Protobuf;
using BackOffice.BFF.Health.Protobuf;
using BackOffice.Common.Utilities;
using Blazored.LocalStorage;
using Grpc.Core;
@@ -80,10 +81,12 @@ public static class ConfigureServices
services.AddTransient(sp => new UserRoleContract.UserRoleContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new CategoryContract.CategoryContractClient(sp.GetRequiredService<CallInvoker>()));
// CMS Services (Commission, Network, Club)
// CMS Services (Commission, Network, Club, Configuration, Health)
services.AddTransient(sp => new CommissionContract.CommissionContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new NetworkMembershipContract.NetworkMembershipContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new ClubMembershipContract.ClubMembershipContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new ConfigurationContract.ConfigurationContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new HealthContract.HealthContractClient(sp.GetRequiredService<CallInvoker>()));
return services;
}

View File

@@ -201,9 +201,50 @@
_loading = true;
try
{
// TODO: Implement GetClubStatisticsQuery in CMS and BFF
// For now, generate mock data
GenerateMockStatistics();
var response = await ClubClient.GetClubStatisticsAsync(new GetClubStatisticsRequest());
// Basic stats
_totalMembers = response.TotalMembers;
_activeMembers = response.ActiveMembers;
_inactiveMembers = response.InactiveMembers;
_activePercentage = (int)response.ActivePercentage;
_inactivePercentage = 100 - _activePercentage;
_averageDuration = response.AverageMembershipDurationDays;
// Status distribution
_statusData = new double[] { _activeMembers, _inactiveMembers };
_statusLabels = new[] { "فعال", "غیرفعال" };
// Membership trend
_trendLabels = response.MonthlyTrend.Select(t => t.Month).ToArray();
_membershipTrend = new List<ChartSeries>
{
new ChartSeries
{
Name = "عضویت‌های جدید",
Data = response.MonthlyTrend.Select(t => (double)t.Activations).ToArray()
},
new ChartSeries
{
Name = "لغو عضویت",
Data = response.MonthlyTrend.Select(t => (double)t.Expirations).ToArray()
}
};
// Package distribution from response
_packageLabels = response.PackageDistribution.Select(p => p.PackageName).ToArray();
_packageSeries = new List<ChartSeries>
{
new ChartSeries
{
Name = "تعداد اعضا",
Data = response.PackageDistribution.Select(p => (double)p.MemberCount).ToArray()
}
};
// Recent memberships - would need a separate API call to get individual records
// For now, keep empty or remove this section
_recentMemberships = new List<RecentMembershipModel>();
}
catch (Exception ex)
{
@@ -215,65 +256,7 @@
}
}
private void GenerateMockStatistics()
{
var random = new Random();
// Basic stats
_totalMembers = random.Next(200, 1000);
_activeMembers = random.Next(100, _totalMembers);
_inactiveMembers = _totalMembers - _activeMembers;
_activePercentage = (int)((_activeMembers / (double)_totalMembers) * 100);
_inactivePercentage = 100 - _activePercentage;
_averageDuration = random.Next(30, 180) + random.NextDouble();
// Status distribution
_statusData = new double[] { _activeMembers, _inactiveMembers };
_statusLabels = new[] { "فعال", "غیرفعال" };
// Membership trend
_trendLabels = new[] { "مهر", "آبان", "آذر", "دی", "بهمن", "اسفند" };
_membershipTrend = new List<ChartSeries>
{
new ChartSeries
{
Name = "عضویت‌های جدید",
Data = Enumerable.Range(0, 6).Select(_ => (double)random.Next(10, 50)).ToArray()
},
new ChartSeries
{
Name = "لغو عضویت",
Data = Enumerable.Range(0, 6).Select(_ => (double)random.Next(5, 20)).ToArray()
}
};
// Package distribution
_packageLabels = new[] { "پکیج برنزی", "پکیج نقره‌ای", "پکیج طلایی", "پکیج پلاتینیوم" };
_packageSeries = new List<ChartSeries>
{
new ChartSeries
{
Name = "تعداد اعضا",
Data = new double[]
{
random.Next(50, 150),
random.Next(100, 250),
random.Next(50, 150),
random.Next(20, 80)
}
}
};
// Recent memberships
_recentMemberships = Enumerable.Range(1, 10).Select(i => new RecentMembershipModel
{
ActivatedAt = DateTime.Now.AddDays(-random.Next(1, 30)),
UserId = random.Next(1000, 9999),
UserName = $"کاربر {i}",
PackageName = _packageLabels[random.Next(0, _packageLabels.Length)],
IsActive = random.Next(0, 10) < 8 // 80% active
}).OrderByDescending(m => m.ActivatedAt).ToList();
}
private class RecentMembershipModel
{

View File

@@ -237,27 +237,7 @@
_averageValue = _reports.Any() ? (long)_reports.Average(r => r.ValuePerBalance) : 0;
}
// Mock data generator - remove when BFF endpoint is ready
private List<WeeklyPoolReportModel> GenerateMockData()
{
var reports = new List<WeeklyPoolReportModel>();
var random = new Random();
for (int week = 40; week <= 48; week++)
{
reports.Add(new WeeklyPoolReportModel
{
WeekNumber = $"2025-W{week:D2}",
TotalPoolAmount = random.Next(50000000, 200000000),
TotalBalances = random.Next(100, 500),
ValuePerBalance = random.Next(100000, 500000),
IsCalculated = week < 48,
CalculatedAt = week < 48 ? Timestamp.FromDateTime(DateTime.UtcNow.AddDays(-(48 - week) * 7)) : null
});
}
return reports.OrderByDescending(r => r.WeekNumber).ToList();
}
// Model for weekly pool reports
private class WeeklyPoolReportModel

View File

@@ -0,0 +1,304 @@
@page "/dashboard/overview"
@attribute [Authorize]
@using BackOffice.BFF.Commission.Protobuf
@using BackOffice.BFF.ClubMembership.Protobuf
@using BackOffice.BFF.NetworkMembership.Protobuf
@using Google.Protobuf.WellKnownTypes
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
<MudText Typo="Typo.h4" GutterBottom="true">داشبورد سیستم</MudText>
<MudText Typo="Typo.body1" Color="Color.Secondary" Class="mb-4">
مرور کلی عملکرد سیستم شبکه، باشگاه و کمیسیون
</MudText>
@if (_loading)
{
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
}
else
{
<!-- Commission Stats -->
<MudCard Class="mb-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Class="mr-2" />
کمیسیون هفته جاری
</MudText>
</CardHeaderContent>
<CardHeaderActions>
<MudButton Variant="Variant.Text"
Color="Color.Primary"
Href="/commission/dashboard">
جزئیات بیشتر
</MudButton>
</CardHeaderActions>
</MudCardHeader>
<MudCardContent>
<MudGrid>
<MudItem xs="12" md="3">
<MudCard Outlined="true">
<MudCardContent>
<MudStack Spacing="1">
<MudText Typo="Typo.body2" Color="Color.Secondary">هفته جاری</MudText>
<MudText Typo="Typo.h5">@_currentWeek</MudText>
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" md="3">
<MudCard Outlined="true">
<MudCardContent>
<MudStack Spacing="1">
<MudText Typo="Typo.body2" Color="Color.Secondary">مبلغ کل استخر</MudText>
<MudText Typo="Typo.h5" Color="Color.Primary">
@(_currentPool?.TotalPoolAmount.ToString("N0") ?? "0") ریال
</MudText>
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" md="3">
<MudCard Outlined="true">
<MudCardContent>
<MudStack Spacing="1">
<MudText Typo="Typo.body2" Color="Color.Secondary">مجموع موجودی‌ها</MudText>
<MudText Typo="Typo.h5">@(_currentPool?.TotalBalances.ToString("N0") ?? "0")</MudText>
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" md="3">
<MudCard Outlined="true">
<MudCardContent>
<MudStack Spacing="1">
<MudText Typo="Typo.body2" Color="Color.Secondary">وضعیت</MudText>
<MudChip T="string"
Color="@(_currentPool?.IsCalculated == true ? Color.Success : Color.Warning)"
Size="Size.Small">
@(_currentPool?.IsCalculated == true ? "محاسبه شده" : "در انتظار")
</MudChip>
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
<!-- Club Membership Stats -->
<MudCard Class="mb-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.CardMembership" Class="mr-2" />
عضویت باشگاه
</MudText>
</CardHeaderContent>
<CardHeaderActions>
<MudButton Variant="Variant.Text"
Color="Color.Primary"
Href="/club/members">
مشاهده اعضا
</MudButton>
</CardHeaderActions>
</MudCardHeader>
<MudCardContent>
<MudGrid>
<MudItem xs="12" md="4">
<MudCard Outlined="true">
<MudCardContent>
<MudStack Spacing="1">
<MudText Typo="Typo.body2" Color="Color.Secondary">کل اعضا</MudText>
<MudText Typo="Typo.h5" Color="Color.Primary">@_totalClubMembers</MudText>
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" md="4">
<MudCard Outlined="true">
<MudCardContent>
<MudStack Spacing="1">
<MudText Typo="Typo.body2" Color="Color.Secondary">اعضای فعال</MudText>
<MudText Typo="Typo.h5" Color="Color.Success">@_activeClubMembers</MudText>
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" md="4">
<MudCard Outlined="true">
<MudCardContent>
<MudStack Spacing="1">
<MudText Typo="Typo.body2" Color="Color.Secondary">اعضای غیرفعال</MudText>
<MudText Typo="Typo.h5" Color="Color.Warning">@_inactiveClubMembers</MudText>
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
<!-- Quick Actions -->
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.Speed" Class="mr-2" />
دسترسی سریع
</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudGrid>
<MudItem xs="12" md="4">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
FullWidth="true"
Href="/commission/dashboard"
StartIcon="@Icons.Material.Filled.AccountBalanceWallet">
داشبورد کمیسیون
</MudButton>
</MudItem>
<MudItem xs="12" md="4">
<MudButton Variant="Variant.Filled"
Color="Color.Secondary"
FullWidth="true"
Href="/commission/payouts"
StartIcon="@Icons.Material.Filled.Payments">
پرداخت‌های کاربران
</MudButton>
</MudItem>
<MudItem xs="12" md="4">
<MudButton Variant="Variant.Filled"
Color="Color.Tertiary"
FullWidth="true"
Href="/commission/withdrawals"
StartIcon="@Icons.Material.Filled.RequestQuote">
درخواست‌های برداشت
</MudButton>
</MudItem>
<MudItem xs="12" md="4">
<MudButton Variant="Variant.Outlined"
Color="Color.Info"
FullWidth="true"
Href="/network/tree"
StartIcon="@Icons.Material.Filled.AccountTree">
درخت شبکه
</MudButton>
</MudItem>
<MudItem xs="12" md="4">
<MudButton Variant="Variant.Outlined"
Color="Color.Success"
FullWidth="true"
Href="/network/balances"
StartIcon="@Icons.Material.Filled.AccountBalance">
گزارش موجودی‌ها
</MudButton>
</MudItem>
<MudItem xs="12" md="4">
<MudButton Variant="Variant.Outlined"
Color="Color.Warning"
FullWidth="true"
Href="/club/members"
StartIcon="@Icons.Material.Filled.Groups">
مدیریت باشگاه
</MudButton>
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
}
</MudContainer>
@code {
[Inject] public CommissionContract.CommissionContractClient CommissionClient { get; set; }
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubClient { get; set; }
private bool _loading = false;
private string _currentWeek = "";
private GetWeeklyCommissionPoolResponse? _currentPool;
private long _totalClubMembers = 0;
private long _activeClubMembers = 0;
private long _inactiveClubMembers = 0;
protected override async Task OnInitializedAsync()
{
await LoadDashboardData();
}
private async Task LoadDashboardData()
{
_loading = true;
try
{
// Get current week in ISO 8601 format
_currentWeek = GetCurrentWeekNumber();
// Load Commission data
try
{
var commissionRequest = new GetWeeklyCommissionPoolRequest
{
WeekNumber = _currentWeek
};
var commissionResponse = await CommissionClient.GetWeeklyCommissionPoolAsync(commissionRequest);
_currentPool = commissionResponse;
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری داده‌های کمیسیون: {ex.Message}", Severity.Warning);
}
// Load Club membership data
try
{
// Get all members
var allMembersRequest = new GetAllClubMembershipsRequest
{
PageIndex = 1,
PageSize = 1
};
var allMembersResponse = await ClubClient.GetAllClubMembershipsAsync(allMembersRequest);
_totalClubMembers = allMembersResponse.MetaData?.TotalCount ?? 0;
// Get active members
var activeMembersRequest = new GetAllClubMembershipsRequest
{
PageIndex = 1,
PageSize = 1,
IsActive = true
};
var activeMembersResponse = await ClubClient.GetAllClubMembershipsAsync(activeMembersRequest);
_activeClubMembers = activeMembersResponse.MetaData?.TotalCount ?? 0;
_inactiveClubMembers = _totalClubMembers - _activeClubMembers;
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری داده‌های باشگاه: {ex.Message}", Severity.Warning);
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری داشبورد: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
private string GetCurrentWeekNumber()
{
var now = DateTime.Now;
var jan1 = new DateTime(now.Year, 1, 1);
var daysOffset = DayOfWeek.Monday - jan1.DayOfWeek;
var firstMonday = jan1.AddDays(daysOffset);
var cal = System.Globalization.CultureInfo.CurrentCulture.Calendar;
var weekNum = cal.GetWeekOfYear(now, System.Globalization.CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
return $"{now.Year}-W{weekNum:D2}";
}
}

View File

@@ -1,7 +1,7 @@
@page "/network/balances"
@using MudBlazor
@using BackOffice.BFF.NetworkMembership.Protobuf
@using BackOffice.BFF.Commission.Protobuf
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
<MudText Typo="Typo.h4" GutterBottom="true">گزارش موجودی‌های هفتگی</MudText>
@@ -67,31 +67,40 @@
Hover="true">
<Columns>
<PropertyColumn Property="x => x.UserId" Title="شناسه کاربر" />
<PropertyColumn Property="x => x.UserName" Title="نام کاربر" />
<PropertyColumn Property="x => x.WeekNumber" Title="هفته" />
<PropertyColumn Property="x => x.LeftBalance" Title="موجودی چپ">
<CellTemplate>
<MudChip T="string" Color="Color.Success" Size="Size.Small">
@context.Item.LeftBalance
@context.Item.LeftBalance.ToString("N0")
</MudChip>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.RightBalance" Title="موجودی راست">
<CellTemplate>
<MudChip T="string" Color="Color.Warning" Size="Size.Small">
@context.Item.RightBalance
@context.Item.RightBalance.ToString("N0")
</MudChip>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.MatchedBalance" Title="موجودی تطبیق‌یافته">
<CellTemplate>
<MudChip T="string" Color="Color.Info" Size="Size.Small">
@context.Item.MatchedBalance
@context.Item.MatchedBalance.ToString("N0")
</MudChip>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.PoolContribution" Title="سهم از استخر">
<CellTemplate>
@context.Item.PoolContribution.ToString("N0") ریال
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.IsExpired" Title="وضعیت">
<CellTemplate>
<MudChip T="string" Color="@(context.Item.IsExpired ? Color.Error : Color.Success)" Size="Size.Small">
@(context.Item.IsExpired ? "منقضی شده" : "فعال")
</MudChip>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.CarryOverLeft" Title="نقل‌شده چپ" />
<PropertyColumn Property="x => x.CarryOverRight" Title="نقل‌شده راست" />
<TemplateColumn Title="عملیات">
<CellTemplate>
<MudButton Size="Size.Small"
@@ -139,33 +148,27 @@
</MudContainer>
@code {
[Inject] public NetworkMembershipContract.NetworkMembershipContractClient NetworkClient { get; set; }
[Inject] public CommissionContract.CommissionContractClient CommissionClient { get; set; }
private long? _filterUserId = null;
private string _filterWeekNumber = "";
private int? _minBalance = null;
private int? _maxBalance = null;
private bool _onlyActive = false;
private int _totalLeftBalance = 0;
private int _totalRightBalance = 0;
private int _totalMatchedBalance = 0;
private long _totalLeftBalance = 0;
private long _totalRightBalance = 0;
private long _totalMatchedBalance = 0;
private async Task<GridData<UserWeeklyBalanceModel>> ServerReload(GridState<UserWeeklyBalanceModel> state)
{
try
{
// TODO: Implement GetUserWeeklyBalancesRequest in CMS Protobuf
// Mock data until API is ready
await Task.CompletedTask;
var items = new List<UserWeeklyBalanceModel>();
/*
var request = new GetUserWeeklyBalancesRequest
{
PageNumber = state.Page + 1,
PageSize = state.PageSize,
WeekNumber = _filterWeekNumber ?? ""
OnlyActive = _onlyActive,
PageIndex = state.Page + 1,
PageSize = state.PageSize
};
if (_filterUserId.HasValue && _filterUserId.Value > 0)
@@ -173,18 +176,23 @@
request.UserId = _filterUserId.Value;
}
var response = await NetworkClient.GetUserWeeklyBalancesAsync(request);
if (!string.IsNullOrWhiteSpace(_filterWeekNumber))
{
request.WeekNumber = _filterWeekNumber;
}
items = response.Balances.Select(b => new UserWeeklyBalanceModel
var response = await CommissionClient.GetUserWeeklyBalancesAsync(request);
var items = response.Models.Select(b => new UserWeeklyBalanceModel
{
UserId = b.UserId,
UserName = b.UserName,
WeekNumber = b.WeekNumber,
LeftBalance = b.LeftBalance,
RightBalance = b.RightBalance,
MatchedBalance = b.MatchedBalance,
CarryOverLeft = b.CarryOverLeft,
CarryOverRight = b.CarryOverRight
LeftBalance = b.LeftLegBalances,
RightBalance = b.RightLegBalances,
MatchedBalance = b.TotalBalances,
PoolContribution = b.WeeklyPoolContribution,
CalculatedAt = b.CalculatedAt?.ToDateTime(),
IsExpired = b.IsExpired
}).ToList();
// Apply balance range filter if specified
@@ -195,14 +203,13 @@
(!_maxBalance.HasValue || b.MatchedBalance <= _maxBalance.Value)
).ToList();
}
*/
CalculateTotals(items);
return new GridData<UserWeeklyBalanceModel>
{
Items = items,
TotalItems = 0
TotalItems = (int)(response.MetaData?.TotalCount ?? 0)
};
}
catch (Exception ex)
@@ -219,9 +226,9 @@
private void CalculateTotals(List<UserWeeklyBalanceModel> items)
{
_totalLeftBalance = items.Sum(b => b.LeftBalance);
_totalRightBalance = items.Sum(b => b.RightBalance);
_totalMatchedBalance = items.Sum(b => b.MatchedBalance);
_totalLeftBalance = items.Sum(b => (long)b.LeftBalance);
_totalRightBalance = items.Sum(b => (long)b.RightBalance);
_totalMatchedBalance = items.Sum(b => (long)b.MatchedBalance);
}
private async Task ExportToExcel()
@@ -231,18 +238,19 @@
"این ویژگی به زودی اضافه خواهد شد.",
yesText: "باشه");
// TODO: Implement Excel export using EPPlus or ClosedXML
// Future enhancement: Export to Excel using EPPlus or ClosedXML library
// Install: dotnet add package EPPlus or ClosedXML
}
private class UserWeeklyBalanceModel
{
public long UserId { get; set; }
public string UserName { get; set; }
public string WeekNumber { get; set; }
public int LeftBalance { get; set; }
public int RightBalance { get; set; }
public int MatchedBalance { get; set; }
public int CarryOverLeft { get; set; }
public int CarryOverRight { get; set; }
public long PoolContribution { get; set; }
public DateTime? CalculatedAt { get; set; }
public bool IsExpired { get; set; }
}
}

View File

@@ -197,9 +197,52 @@
_loading = true;
try
{
// TODO: Implement GetNetworkStatisticsQuery in CMS and BFF
// For now, generate mock data
GenerateMockStatistics();
var response = await NetworkClient.GetNetworkStatisticsAsync(new GetNetworkStatisticsRequest());
// Basic stats
_totalMembers = response.TotalMembers;
_leftCount = response.LeftLegCount;
_rightCount = response.RightLegCount;
_leftPercentage = (int)response.LeftPercentage;
_rightPercentage = (int)response.RightPercentage;
_averageDepth = response.AverageDepth;
// Distribution chart
_distributionData = new double[] { _leftCount, _rightCount };
_distributionLabels = new[] { "شاخه چپ", "شاخه راست" };
// Growth chart
_growthLabels = response.MonthlyGrowth.Select(m => m.Month).ToArray();
_growthSeries = new List<ChartSeries>
{
new ChartSeries
{
Name = "اعضای جدید",
Data = response.MonthlyGrowth.Select(m => (double)m.NewMembers).ToArray()
}
};
// Depth distribution
_depthLabels = response.LevelDistribution.Select(l => $"سطح {l.Level}").ToArray();
_depthSeries = new List<ChartSeries>
{
new ChartSeries
{
Name = "تعداد اعضا",
Data = response.LevelDistribution.Select(l => (double)l.Count).ToArray()
}
};
// Top users
_topUsers = response.TopUsers.Select(u => new TopUserModel
{
Rank = u.Rank,
UserId = u.UserId,
UserName = u.UserName,
TotalChildren = u.TotalChildren,
LeftCount = u.LeftCount,
RightCount = u.RightCount
}).ToList();
}
catch (Exception ex)
{
@@ -211,63 +254,7 @@
}
}
private void GenerateMockStatistics()
{
var random = new Random();
// Basic stats
_totalMembers = random.Next(1000, 5000);
_leftCount = random.Next(400, 2500);
_rightCount = _totalMembers - _leftCount;
_leftPercentage = (int)((_leftCount / (double)_totalMembers) * 100);
_rightPercentage = 100 - _leftPercentage;
_averageDepth = random.Next(3, 8) + random.NextDouble();
// Distribution chart
_distributionData = new double[] { _leftCount, _rightCount };
_distributionLabels = new[] { "شاخه چپ", "شاخه راست" };
// Growth chart
_growthLabels = new[] { "مهر", "آبان", "آذر", "دی", "بهمن", "اسفند" };
_growthSeries = new List<ChartSeries>
{
new ChartSeries
{
Name = "اعضای جدید",
Data = Enumerable.Range(0, 6).Select(_ => (double)random.Next(50, 200)).ToArray()
}
};
// Depth distribution
_depthLabels = new[] { "سطح 1", "سطح 2", "سطح 3", "سطح 4", "سطح 5", "سطح 6+" };
_depthSeries = new List<ChartSeries>
{
new ChartSeries
{
Name = "تعداد اعضا",
Data = new double[]
{
random.Next(100, 200),
random.Next(200, 400),
random.Next(300, 600),
random.Next(200, 400),
random.Next(100, 200),
random.Next(50, 100)
}
}
};
// Top users
_topUsers = Enumerable.Range(1, 10).Select(i => new TopUserModel
{
Rank = i,
UserId = random.Next(1000, 9999),
UserName = $"کاربر {i}",
TotalChildren = random.Next(50, 500),
LeftCount = random.Next(20, 250),
RightCount = random.Next(20, 250)
}).ToList();
}
private Color GetRankColor(int rank)
{

View File

@@ -219,7 +219,8 @@
private async Task ViewHistory()
{
// TODO: Implement history view
// Future enhancement: Show historical changes (parent changes, status updates, etc.)
// Requires: Historical tracking table in CMS database
Snackbar.Add("این قابلیت به زودی اضافه خواهد شد", Severity.Info);
}
}

View File

@@ -0,0 +1,417 @@
@page "/settings"
@attribute [Authorize]
@using MudBlazor
@using Microsoft.JSInterop
@inject IJSRuntime JSRuntime
@inject ISnackbar Snackbar
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
<MudText Typo="Typo.h4" GutterBottom="true">تنظیمات</MudText>
<MudText Typo="Typo.body1" Color="Color.Secondary" Class="mb-4">
مدیریت تنظیمات کاربری و سیستم
</MudText>
<MudTabs Elevation="2" Rounded="true" ApplyEffectsToContainer="true" PanelClass="pa-6">
<!-- General Settings -->
<MudTabPanel Text="عمومی" Icon="@Icons.Material.Filled.Settings">
<MudCard Class="mb-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">تنظیمات نمایش</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudStack Spacing="3">
<MudSelect @bind-Value="_selectedLanguage"
Label="زبان"
Variant="Variant.Outlined">
<MudSelectItem Value="@("fa")">فارسی</MudSelectItem>
<MudSelectItem Value="@("en")">English</MudSelectItem>
</MudSelect>
<MudSwitch @bind-Value="_darkMode"
Color="Color.Primary"
Label="حالت تاریک" />
<MudSwitch @bind-Value="_compactMode"
Color="Color.Secondary"
Label="حالت فشرده" />
<MudSlider @bind-Value="_pageSize"
Min="10"
Max="100"
Step="10"
Color="Color.Info">
تعداد ردیف در هر صفحه: @_pageSize
</MudSlider>
</MudStack>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="SaveGeneralSettings">
ذخیره تغییرات
</MudButton>
</MudCardActions>
</MudCard>
</MudTabPanel>
<!-- Notifications -->
<MudTabPanel Text="اعلان‌ها" Icon="@Icons.Material.Filled.Notifications">
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">تنظیمات اعلان‌ها</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudStack Spacing="3">
<MudSwitch @bind-Value="_emailNotifications"
Color="Color.Primary"
Label="دریافت اعلان‌های ایمیل" />
<MudSwitch @bind-Value="_smsNotifications"
Color="Color.Secondary"
Label="دریافت اعلان‌های پیامک" />
<MudSwitch @bind-Value="_systemNotifications"
Color="Color.Info"
Label="اعلان‌های سیستمی" />
<MudDivider />
<MudText Typo="Typo.subtitle2">نوع اعلان‌ها</MudText>
<MudCheckBox @bind-Value="_notifyOnNewWithdrawal"
Color="Color.Primary">
درخواست برداشت جدید
</MudCheckBox>
<MudCheckBox @bind-Value="_notifyOnCommissionCalculation"
Color="Color.Primary">
محاسبه کمیسیون هفتگی
</MudCheckBox>
<MudCheckBox @bind-Value="_notifyOnNewClubMember"
Color="Color.Primary">
عضو جدید باشگاه
</MudCheckBox>
<MudCheckBox @bind-Value="_notifyOnSystemError"
Color="Color.Error">
خطاهای سیستمی
</MudCheckBox>
</MudStack>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="SaveNotificationSettings">
ذخیره تغییرات
</MudButton>
</MudCardActions>
</MudCard>
</MudTabPanel>
<!-- Security -->
<MudTabPanel Text="امنیت" Icon="@Icons.Material.Filled.Security">
<MudCard Class="mb-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">تغییر رمز عبور</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudStack Spacing="3">
<MudTextField @bind-Value="_currentPassword"
Label="رمز عبور فعلی"
Variant="Variant.Outlined"
InputType="InputType.Password"
Adornment="Adornment.End"
AdornmentIcon="@Icons.Material.Filled.Lock" />
<MudTextField @bind-Value="_newPassword"
Label="رمز عبور جدید"
Variant="Variant.Outlined"
InputType="InputType.Password"
Adornment="Adornment.End"
AdornmentIcon="@Icons.Material.Filled.Lock" />
<MudTextField @bind-Value="_confirmPassword"
Label="تکرار رمز عبور جدید"
Variant="Variant.Outlined"
InputType="InputType.Password"
Adornment="Adornment.End"
AdornmentIcon="@Icons.Material.Filled.Lock" />
</MudStack>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled"
Color="Color.Warning"
OnClick="ChangePassword">
تغییر رمز عبور
</MudButton>
</MudCardActions>
</MudCard>
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">احراز هویت دو مرحله‌ای</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudStack Spacing="3">
<MudSwitch @bind-Value="_twoFactorEnabled"
Color="Color.Success"
Label="فعال‌سازی احراز هویت دو مرحله‌ای" />
@if (_twoFactorEnabled)
{
<MudAlert Severity="Severity.Success">
احراز هویت دو مرحله‌ای فعال است. برای ورود به حساب کاربری، علاوه بر رمز عبور، کد تایید ارسال شده به موبایل شما نیز لازم است.
</MudAlert>
}
</MudStack>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="SaveSecuritySettings">
ذخیره تغییرات
</MudButton>
</MudCardActions>
</MudCard>
</MudTabPanel>
<!-- About -->
<MudTabPanel Text="درباره" Icon="@Icons.Material.Filled.Info">
<MudCard>
<MudCardContent>
<MudStack Spacing="3">
<MudText Typo="Typo.h5">سیستم مدیریت شبکه فروش</MudText>
<MudDivider />
<MudStack Row="true" Spacing="2">
<MudText Typo="Typo.subtitle2" Style="min-width: 150px;">نسخه:</MudText>
<MudText Typo="Typo.body1">1.0.0</MudText>
</MudStack>
<MudStack Row="true" Spacing="2">
<MudText Typo="Typo.subtitle2" Style="min-width: 150px;">سازنده:</MudText>
<MudText Typo="Typo.body1">FourSat Team</MudText>
</MudStack>
<MudStack Row="true" Spacing="2">
<MudText Typo="Typo.subtitle2" Style="min-width: 150px;">تاریخ انتشار:</MudText>
<MudText Typo="Typo.body1">2025-11-30</MudText>
</MudStack>
<MudDivider />
<MudText Typo="Typo.subtitle2">ماژول‌های فعال:</MudText>
<MudList T="string">
<MudListItem T="string" Icon="@Icons.Material.Filled.Check" IconColor="Color.Success">
مدیریت کمیسیون
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.Check" IconColor="Color.Success">
مدیریت شبکه
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.Check" IconColor="Color.Success">
مدیریت باشگاه مشتریان
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.Check" IconColor="Color.Success">
گزارش‌گیری پیشرفته
</MudListItem>
</MudList>
</MudStack>
</MudCardContent>
</MudCard>
</MudTabPanel>
</MudTabs>
</MudContainer>
@code {
// General Settings
private string _selectedLanguage = "fa";
private bool _darkMode = false;
private bool _compactMode = false;
private int _pageSize = 20;
// Notification Settings
private bool _emailNotifications = true;
private bool _smsNotifications = false;
private bool _systemNotifications = true;
private bool _notifyOnNewWithdrawal = true;
private bool _notifyOnCommissionCalculation = true;
private bool _notifyOnNewClubMember = false;
private bool _notifyOnSystemError = true;
// Security Settings
private string _currentPassword = "";
private string _newPassword = "";
private string _confirmPassword = "";
private bool _twoFactorEnabled = false;
protected override async Task OnInitializedAsync()
{
// Load settings from local storage or API
await LoadSettings();
}
private async Task LoadSettings()
{
try
{
// Load General Settings
_selectedLanguage = await GetLocalStorage<string>("user_language", "fa");
_darkMode = await GetLocalStorage<bool>("user_darkMode", false);
_compactMode = await GetLocalStorage<bool>("user_compactMode", false);
_pageSize = await GetLocalStorage<int>("user_pageSize", 20);
// Load Notification Settings
_emailNotifications = await GetLocalStorage<bool>("user_emailNotifications", true);
_smsNotifications = await GetLocalStorage<bool>("user_smsNotifications", false);
_systemNotifications = await GetLocalStorage<bool>("user_systemNotifications", true);
_notifyOnNewWithdrawal = await GetLocalStorage<bool>("user_notifyOnNewWithdrawal", true);
_notifyOnCommissionCalculation = await GetLocalStorage<bool>("user_notifyOnCommissionCalculation", true);
_notifyOnNewClubMember = await GetLocalStorage<bool>("user_notifyOnNewClubMember", false);
_notifyOnSystemError = await GetLocalStorage<bool>("user_notifyOnSystemError", true);
// Load Security Settings
_twoFactorEnabled = await GetLocalStorage<bool>("user_twoFactorEnabled", false);
}
catch (Exception ex)
{
Console.WriteLine($"Error loading settings: {ex.Message}");
}
}
private async Task SaveGeneralSettings()
{
try
{
await SetLocalStorage("user_language", _selectedLanguage);
await SetLocalStorage("user_darkMode", _darkMode);
await SetLocalStorage("user_compactMode", _compactMode);
await SetLocalStorage("user_pageSize", _pageSize);
Snackbar.Add("تنظیمات عمومی با موفقیت ذخیره شد", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در ذخیره تنظیمات: {ex.Message}", Severity.Error);
}
}
private async Task SaveNotificationSettings()
{
try
{
await SetLocalStorage("user_emailNotifications", _emailNotifications);
await SetLocalStorage("user_smsNotifications", _smsNotifications);
await SetLocalStorage("user_systemNotifications", _systemNotifications);
await SetLocalStorage("user_notifyOnNewWithdrawal", _notifyOnNewWithdrawal);
await SetLocalStorage("user_notifyOnCommissionCalculation", _notifyOnCommissionCalculation);
await SetLocalStorage("user_notifyOnNewClubMember", _notifyOnNewClubMember);
await SetLocalStorage("user_notifyOnSystemError", _notifyOnSystemError);
Snackbar.Add("تنظیمات اعلان‌ها با موفقیت ذخیره شد", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در ذخیره تنظیمات: {ex.Message}", Severity.Error);
}
}
private async Task SaveSecuritySettings()
{
try
{
await SetLocalStorage("user_twoFactorEnabled", _twoFactorEnabled);
Snackbar.Add("تنظیمات امنیتی با موفقیت ذخیره شد", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در ذخیره تنظیمات: {ex.Message}", Severity.Error);
}
}
private async Task ChangePassword()
{
if (string.IsNullOrWhiteSpace(_currentPassword))
{
Snackbar.Add("رمز عبور فعلی را وارد کنید", Severity.Warning);
return;
}
if (string.IsNullOrWhiteSpace(_newPassword))
{
Snackbar.Add("رمز عبور جدید را وارد کنید", Severity.Warning);
return;
}
if (_newPassword != _confirmPassword)
{
Snackbar.Add("رمز عبور جدید و تکرار آن یکسان نیستند", Severity.Warning);
return;
}
if (_newPassword.Length < 8)
{
Snackbar.Add("رمز عبور باید حداقل 8 کاراکتر باشد", Severity.Warning);
return;
}
try
{
// NOTE: Password change requires Identity/Authentication API in BFF
// Future API: AuthClient.ChangePasswordAsync(currentPassword, newPassword)
await Task.CompletedTask;
_currentPassword = "";
_newPassword = "";
_confirmPassword = "";
Snackbar.Add("رمز عبور با موفقیت تغییر کرد", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در تغییر رمز عبور: {ex.Message}", Severity.Error);
}
}
// LocalStorage Helper Methods
private async Task<T> GetLocalStorage<T>(string key, T defaultValue)
{
try
{
var json = await JSRuntime.InvokeAsync<string>("localStorage.getItem", key);
if (string.IsNullOrEmpty(json))
return defaultValue;
if (typeof(T) == typeof(string))
return (T)(object)json;
if (typeof(T) == typeof(bool))
return (T)(object)bool.Parse(json);
if (typeof(T) == typeof(int))
return (T)(object)int.Parse(json);
return System.Text.Json.JsonSerializer.Deserialize<T>(json) ?? defaultValue;
}
catch
{
return defaultValue;
}
}
private async Task SetLocalStorage<T>(string key, T value)
{
var json = typeof(T) == typeof(string) || typeof(T) == typeof(bool) || typeof(T) == typeof(int)
? value?.ToString() ?? ""
: System.Text.Json.JsonSerializer.Serialize(value);
await JSRuntime.InvokeVoidAsync("localStorage.setItem", key, json);
}
}

View File

@@ -0,0 +1,421 @@
@page "/system/alerts"
@attribute [Authorize]
@using MudBlazor
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
<MudText Typo="Typo.h4" GutterBottom="true">مدیریت هشدارها</MudText>
<MudText Typo="Typo.body1" Color="Color.Secondary" Class="mb-4">
مشاهده و مدیریت هشدارهای سیستم
</MudText>
<!-- Summary Cards -->
<MudGrid Class="mb-4">
<MudItem xs="12" md="3">
<MudCard Elevation="2">
<MudCardContent>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudStack Spacing="1">
<MudText Typo="Typo.body2" Color="Color.Secondary">کل هشدارها</MudText>
<MudText Typo="Typo.h5">@_totalAlerts</MudText>
</MudStack>
<MudIcon Icon="@Icons.Material.Filled.NotificationsActive" Size="Size.Large" Color="Color.Default" />
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" md="3">
<MudCard Elevation="2">
<MudCardContent>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudStack Spacing="1">
<MudText Typo="Typo.body2" Color="Color.Secondary">بحرانی</MudText>
<MudText Typo="Typo.h5" Color="Color.Error">@_criticalAlerts</MudText>
</MudStack>
<MudIcon Icon="@Icons.Material.Filled.Error" Size="Size.Large" Color="Color.Error" />
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" md="3">
<MudCard Elevation="2">
<MudCardContent>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudStack Spacing="1">
<MudText Typo="Typo.body2" Color="Color.Secondary">هشدار</MudText>
<MudText Typo="Typo.h5" Color="Color.Warning">@_warningAlerts</MudText>
</MudStack>
<MudIcon Icon="@Icons.Material.Filled.Warning" Size="Size.Large" Color="Color.Warning" />
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" md="3">
<MudCard Elevation="2">
<MudCardContent>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudStack Spacing="1">
<MudText Typo="Typo.body2" Color="Color.Secondary">حل شده امروز</MudText>
<MudText Typo="Typo.h5" Color="Color.Success">@_resolvedToday</MudText>
</MudStack>
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Size="Size.Large" Color="Color.Success" />
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
<!-- Filters -->
<MudCard Class="mb-4">
<MudCardContent>
<MudGrid>
<MudItem xs="12" md="3">
<MudSelect @bind-Value="_filterSeverity"
Label="سطح هشدار"
Variant="Variant.Outlined"
Margin="Margin.Dense">
<MudSelectItem Value="@((string?)null)">همه</MudSelectItem>
<MudSelectItem Value="@("Critical")">بحرانی</MudSelectItem>
<MudSelectItem Value="@("Warning")">هشدار</MudSelectItem>
<MudSelectItem Value="@("Info")">اطلاعات</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" md="3">
<MudSelect @bind-Value="_filterStatus"
Label="وضعیت"
Variant="Variant.Outlined"
Margin="Margin.Dense">
<MudSelectItem Value="@((string?)null)">همه</MudSelectItem>
<MudSelectItem Value="@("Active")">فعال</MudSelectItem>
<MudSelectItem Value="@("Resolved")">حل شده</MudSelectItem>
<MudSelectItem Value="@("Acknowledged")">تایید شده</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" md="3">
<MudSelect @bind-Value="_filterSource"
Label="منبع"
Variant="Variant.Outlined"
Margin="Margin.Dense">
<MudSelectItem Value="@((string?)null)">همه</MudSelectItem>
<MudSelectItem Value="@("Commission")">کمیسیون</MudSelectItem>
<MudSelectItem Value="@("Network")">شبکه</MudSelectItem>
<MudSelectItem Value="@("Club")">باشگاه</MudSelectItem>
<MudSelectItem Value="@("System")">سیستم</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" md="3" Class="d-flex align-center">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="ApplyFilters"
StartIcon="@Icons.Material.Filled.FilterList"
FullWidth="true">
اعمال فیلتر
</MudButton>
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
<!-- Alerts Table -->
<MudCard>
<MudCardContent>
@if (_loading)
{
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
}
else
{
<MudTable Items="@_filteredAlerts"
Hover="true"
Breakpoint="Breakpoint.Sm"
Dense="true"
Striped="true">
<HeaderContent>
<MudTh>سطح</MudTh>
<MudTh>عنوان</MudTh>
<MudTh>منبع</MudTh>
<MudTh>توضیحات</MudTh>
<MudTh>زمان</MudTh>
<MudTh>وضعیت</MudTh>
<MudTh>عملیات</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="سطح">
<MudChip T="string"
Size="Size.Small"
Color="@GetSeverityColor(context.Severity)">
@GetSeverityText(context.Severity)
</MudChip>
</MudTd>
<MudTd DataLabel="عنوان">
<MudText Typo="Typo.body2"><strong>@context.Title</strong></MudText>
</MudTd>
<MudTd DataLabel="منبع">
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined">
@context.Source
</MudChip>
</MudTd>
<MudTd DataLabel="توضیحات">
<MudText Typo="Typo.body2" Style="max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
@context.Description
</MudText>
</MudTd>
<MudTd DataLabel="زمان">
<MudText Typo="Typo.caption">@context.CreatedAt.ToString("yyyy/MM/dd HH:mm")</MudText>
</MudTd>
<MudTd DataLabel="وضعیت">
<MudChip T="string"
Size="Size.Small"
Color="@GetStatusColor(context.Status)">
@GetStatusText(context.Status)
</MudChip>
</MudTd>
<MudTd DataLabel="عملیات">
<MudStack Row="true" Spacing="1">
<MudIconButton Icon="@Icons.Material.Filled.Visibility"
Size="Size.Small"
Color="Color.Info"
OnClick="@(() => ViewDetails(context))" />
@if (context.Status == "Active")
{
<MudIconButton Icon="@Icons.Material.Filled.Check"
Size="Size.Small"
Color="Color.Success"
OnClick="@(() => AcknowledgeAlert(context))" />
<MudIconButton Icon="@Icons.Material.Filled.Close"
Size="Size.Small"
Color="Color.Warning"
OnClick="@(() => ResolveAlert(context))" />
}
</MudStack>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager PageSizeOptions="new int[]{10, 25, 50, 100}" />
</PagerContent>
</MudTable>
}
</MudCardContent>
</MudCard>
</MudContainer>
@code {
private bool _loading = false;
private List<AlertModel> _alerts = new();
private List<AlertModel> _filteredAlerts = new();
// Statistics
private int _totalAlerts = 0;
private int _criticalAlerts = 0;
private int _warningAlerts = 0;
private int _resolvedToday = 0;
// Filters
private string? _filterSeverity;
private string? _filterStatus;
private string? _filterSource;
protected override async Task OnInitializedAsync()
{
await LoadAlerts();
}
private async Task LoadAlerts()
{
_loading = true;
try
{
// NOTE: This page uses mock data. Requires AlertLog table in CMS microservice.
// Future API: AlertClient.GetAllAlertsAsync() with filtering support
await Task.Delay(500); // Simulate API call
_alerts = GenerateMockAlerts();
_filteredAlerts = _alerts;
CalculateStatistics();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری هشدارها: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
private async Task ApplyFilters()
{
_filteredAlerts = _alerts
.Where(a => string.IsNullOrEmpty(_filterSeverity) || a.Severity == _filterSeverity)
.Where(a => string.IsNullOrEmpty(_filterStatus) || a.Status == _filterStatus)
.Where(a => string.IsNullOrEmpty(_filterSource) || a.Source == _filterSource)
.ToList();
await Task.CompletedTask;
}
private void CalculateStatistics()
{
_totalAlerts = _alerts.Count;
_criticalAlerts = _alerts.Count(a => a.Severity == "Critical");
_warningAlerts = _alerts.Count(a => a.Severity == "Warning");
_resolvedToday = _alerts.Count(a => a.Status == "Resolved" && a.ResolvedAt?.Date == DateTime.Today);
}
private Color GetSeverityColor(string severity)
{
return severity switch
{
"Critical" => Color.Error,
"Warning" => Color.Warning,
"Info" => Color.Info,
_ => Color.Default
};
}
private string GetSeverityText(string severity)
{
return severity switch
{
"Critical" => "بحرانی",
"Warning" => "هشدار",
"Info" => "اطلاعات",
_ => "نامشخص"
};
}
private Color GetStatusColor(string status)
{
return status switch
{
"Active" => Color.Error,
"Acknowledged" => Color.Warning,
"Resolved" => Color.Success,
_ => Color.Default
};
}
private string GetStatusText(string status)
{
return status switch
{
"Active" => "فعال",
"Acknowledged" => "تایید شده",
"Resolved" => "حل شده",
_ => "نامشخص"
};
}
private void ViewDetails(AlertModel alert)
{
// Future enhancement: Open dialog with full alert details, stack traces, related logs
Snackbar.Add($"جزئیات هشدار: {alert.Title}", Severity.Info);
}
private async Task AcknowledgeAlert(AlertModel alert)
{
var confirmed = await DialogService.ShowMessageBox(
"تایید هشدار",
$"آیا از تایید این هشدار مطمئن هستید؟\n\n{alert.Title}",
yesText: "بله", cancelText: "خیر");
if (confirmed == true)
{
try
{
// Future API: AlertClient.AcknowledgeAlertAsync(alert.Id)
alert.Status = "Acknowledged";
Snackbar.Add("هشدار تایید شد", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
}
}
private async Task ResolveAlert(AlertModel alert)
{
var confirmed = await DialogService.ShowMessageBox(
"حل هشدار",
$"آیا این هشدار حل شده است؟\n\n{alert.Title}",
yesText: "بله، حل شد", cancelText: "خیر");
if (confirmed == true)
{
try
{
// Future API: AlertClient.ResolveAlertAsync(alert.Id)
alert.Status = "Resolved";
alert.ResolvedAt = DateTime.Now;
CalculateStatistics();
Snackbar.Add("هشدار به عنوان حل شده علامت‌گذاری شد", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
}
}
private List<AlertModel> GenerateMockAlerts()
{
var random = new Random();
var severities = new[] { "Critical", "Warning", "Info" };
var statuses = new[] { "Active", "Acknowledged", "Resolved" };
var sources = new[] { "Commission", "Network", "Club", "System" };
var titles = new[]
{
"خطا در محاسبه کمیسیون هفتگی",
"تعداد درخواست‌های برداشت بیش از حد معمول",
"Worker کمیسیون متوقف شده",
"پایگاه داده بالای 80% ظرفیت",
"تعداد کاربران فعال کاهش یافته",
"خطا در ارسال ایمیل",
"زمان پاسخ سرور بیش از حد معمول",
"عضو باشگاه منقضی شده نیاز به تمدید",
"موجودی کیف پول اصلی کم است",
"سرویس پرداخت در دسترس نیست"
};
var alerts = new List<AlertModel>();
for (int i = 0; i < 25; i++)
{
var createdAt = DateTime.Now.AddHours(-random.Next(0, 72));
var status = statuses[random.Next(statuses.Length)];
alerts.Add(new AlertModel
{
Id = i + 1,
Severity = severities[random.Next(severities.Length)],
Title = titles[random.Next(titles.Length)],
Description = "توضیحات تکمیلی در مورد این هشدار. این متن برای نمایش جزئیات بیشتر در مورد مشکل یا وضعیتی که رخ داده است.",
Source = sources[random.Next(sources.Length)],
Status = status,
CreatedAt = createdAt,
AcknowledgedAt = status != "Active" ? createdAt.AddMinutes(random.Next(5, 60)) : null,
ResolvedAt = status == "Resolved" ? createdAt.AddHours(random.Next(1, 24)) : null
});
}
return alerts.OrderByDescending(a => a.CreatedAt).ToList();
}
private class AlertModel
{
public int Id { get; set; }
public string Severity { get; set; } = "";
public string Title { get; set; } = "";
public string Description { get; set; } = "";
public string Source { get; set; } = "";
public string Status { get; set; } = "";
public DateTime CreatedAt { get; set; }
public DateTime? AcknowledgedAt { get; set; }
public DateTime? ResolvedAt { get; set; }
}
}

View File

@@ -0,0 +1,634 @@
@page "/system/configuration"
@attribute [Authorize(Roles = "Administrator")]
@using MudBlazor
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
<MudText Typo="Typo.h4" GutterBottom="true">تنظیمات سیستم</MudText>
<MudText Typo="Typo.body1" Color="Color.Secondary" Class="mb-4">
مدیریت پارامترهای سیستم و قوانین محاسبات
</MudText>
<MudTabs Elevation="2" Rounded="true" ApplyEffectsToContainer="true" PanelClass="pa-6">
<!-- Commission Settings -->
<MudTabPanel Text="تنظیمات کمیسیون" Icon="@Icons.Material.Filled.AccountBalanceWallet">
<MudCard Elevation="0">
<MudCardContent>
<MudText Typo="Typo.h6" Class="mb-4">پارامترهای محاسبه کمیسیون</MudText>
<MudGrid>
<MudItem xs="12" md="6">
<MudNumericField @bind-Value="_commissionConfig.MinBalanceForPayout"
Label="حداقل موجودی برای پرداخت"
Variant="Variant.Outlined"
Adornment="Adornment.End"
AdornmentText="تومان"
Min="0"
HelperText="حداقل موجودی لازم برای درخواست برداشت" />
</MudItem>
<MudItem xs="12" md="6">
<MudNumericField @bind-Value="_commissionConfig.MaxWithdrawalAmount"
Label="حداکثر مبلغ برداشت"
Variant="Variant.Outlined"
Adornment="Adornment.End"
AdornmentText="تومان"
Min="0"
HelperText="حداکثر مبلغ قابل برداشت در هر درخواست" />
</MudItem>
<MudItem xs="12" md="6">
<MudNumericField @bind-Value="_commissionConfig.CommissionPercentage"
Label="درصد کمیسیون"
Variant="Variant.Outlined"
Adornment="Adornment.End"
AdornmentText="%"
Min="0"
Max="100"
HelperText="درصد کمیسیون از فروش" />
</MudItem>
<MudItem xs="12" md="6">
<MudNumericField @bind-Value="_commissionConfig.WeeklyPoolPercentage"
Label="درصد استخر هفتگی"
Variant="Variant.Outlined"
Adornment="Adornment.End"
AdornmentText="%"
Min="0"
Max="100"
HelperText="درصد اختصاص به استخر هفتگی" />
</MudItem>
<MudItem xs="12" md="6">
<MudSelect @bind-Value="_commissionConfig.CalculationDay"
Label="روز محاسبه هفتگی"
Variant="Variant.Outlined">
<MudSelectItem Value="@("Saturday")">شنبه</MudSelectItem>
<MudSelectItem Value="@("Sunday")">یکشنبه</MudSelectItem>
<MudSelectItem Value="@("Monday")">دوشنبه</MudSelectItem>
<MudSelectItem Value="@("Tuesday")">سه‌شنبه</MudSelectItem>
<MudSelectItem Value="@("Wednesday")">چهارشنبه</MudSelectItem>
<MudSelectItem Value="@("Thursday")">پنجشنبه</MudSelectItem>
<MudSelectItem Value="@("Friday")">جمعه</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" md="6">
<MudTimePicker @bind-Time="_commissionConfig.CalculationTime"
Label="ساعت محاسبه"
Variant="Variant.Outlined"
AmPm="false" />
</MudItem>
<MudItem xs="12">
<MudSwitch @bind-Value="_commissionConfig.AutoCalculationEnabled"
Color="Color.Primary"
Label="محاسبه خودکار هفتگی فعال باشد" />
</MudItem>
<MudItem xs="12">
<MudSwitch @bind-Value="_commissionConfig.AutoPayoutEnabled"
Color="Color.Primary"
Label="پرداخت خودکار فعال باشد" />
</MudItem>
</MudGrid>
<MudDivider Class="my-4" />
<MudStack Row="true" Justify="Justify.FlexEnd" Spacing="2">
<MudButton Variant="Variant.Text"
Color="Color.Default"
OnClick="ResetCommissionConfig">
بازگشت به پیش‌فرض
</MudButton>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="SaveCommissionConfig"
StartIcon="@Icons.Material.Filled.Save">
ذخیره تنظیمات
</MudButton>
</MudStack>
</MudCardContent>
</MudCard>
</MudTabPanel>
<!-- Network Settings -->
<MudTabPanel Text="تنظیمات شبکه" Icon="@Icons.Material.Filled.AccountTree">
<MudCard Elevation="0">
<MudCardContent>
<MudText Typo="Typo.h6" Class="mb-4">قوانین شبکه باینری</MudText>
<MudGrid>
<MudItem xs="12" md="6">
<MudNumericField @bind-Value="_networkConfig.MaxTreeDepth"
Label="حداکثر عمق درخت"
Variant="Variant.Outlined"
Min="1"
Max="20"
HelperText="حداکثر سطوح مجاز در درخت شبکه" />
</MudItem>
<MudItem xs="12" md="6">
<MudNumericField @bind-Value="_networkConfig.MaxDirectChildren"
Label="حداکثر زیرمجموعه مستقیم"
Variant="Variant.Outlined"
Min="2"
Max="10"
HelperText="حداکثر تعداد افراد زیر یک کاربر" />
</MudItem>
<MudItem xs="12" md="6">
<MudNumericField @bind-Value="_networkConfig.MinimumPurchaseForActivation"
Label="حداقل خرید برای فعال‌سازی"
Variant="Variant.Outlined"
Adornment="Adornment.End"
AdornmentText="تومان"
Min="0"
HelperText="حداقل مبلغ خرید برای فعال شدن در شبکه" />
</MudItem>
<MudItem xs="12" md="6">
<MudNumericField @bind-Value="_networkConfig.BalanceExpirationDays"
Label="مدت اعتبار موجودی"
Variant="Variant.Outlined"
Adornment="Adornment.End"
AdornmentText="روز"
Min="0"
HelperText="تعداد روز اعتبار موجودی (0 = نامحدود)" />
</MudItem>
<MudItem xs="12">
<MudSwitch @bind-Value="_networkConfig.BinaryTreeEnabled"
Color="Color.Primary"
Label="سیستم باینری فعال باشد" />
</MudItem>
<MudItem xs="12">
<MudSwitch @bind-Value="_networkConfig.AutoPlacementEnabled"
Color="Color.Primary"
Label="جایگذاری خودکار در درخت" />
</MudItem>
<MudItem xs="12">
<MudSwitch @bind-Value="_networkConfig.SpilloverEnabled"
Color="Color.Primary"
Label="Spillover فعال باشد"
HelperText="اعضای اضافی به پایین درخت منتقل شوند" />
</MudItem>
</MudGrid>
<MudDivider Class="my-4" />
<MudStack Row="true" Justify="Justify.FlexEnd" Spacing="2">
<MudButton Variant="Variant.Text"
Color="Color.Default"
OnClick="ResetNetworkConfig">
بازگشت به پیش‌فرض
</MudButton>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="SaveNetworkConfig"
StartIcon="@Icons.Material.Filled.Save">
ذخیره تنظیمات
</MudButton>
</MudStack>
</MudCardContent>
</MudCard>
</MudTabPanel>
<!-- Club Settings -->
<MudTabPanel Text="تنظیمات باشگاه" Icon="@Icons.Material.Filled.CardMembership">
<MudCard Elevation="0">
<MudCardContent>
<MudText Typo="Typo.h6" Class="mb-4">تنظیمات عضویت باشگاه</MudText>
<MudGrid>
<MudItem xs="12" md="6">
<MudNumericField @bind-Value="_clubConfig.MonthlyFee"
Label="هزینه ماهانه عضویت"
Variant="Variant.Outlined"
Adornment="Adornment.End"
AdornmentText="تومان"
Min="0"
HelperText="هزینه اشتراک ماهانه باشگاه" />
</MudItem>
<MudItem xs="12" md="6">
<MudNumericField @bind-Value="_clubConfig.GracePeriodDays"
Label="مهلت پرداخت"
Variant="Variant.Outlined"
Adornment="Adornment.End"
AdornmentText="روز"
Min="0"
HelperText="تعداد روز مهلت پس از انقضا" />
</MudItem>
<MudItem xs="12" md="6">
<MudNumericField @bind-Value="_clubConfig.MembershipDurationMonths"
Label="مدت عضویت"
Variant="Variant.Outlined"
Adornment="Adornment.End"
AdornmentText="ماه"
Min="1"
HelperText="طول دوره عضویت به ماه" />
</MudItem>
<MudItem xs="12" md="6">
<MudNumericField @bind-Value="_clubConfig.MinimumPurchaseForClub"
Label="حداقل خرید برای ورود"
Variant="Variant.Outlined"
Adornment="Adornment.End"
AdornmentText="تومان"
Min="0"
HelperText="حداقل خرید برای عضویت باشگاه" />
</MudItem>
<MudItem xs="12">
<MudSwitch @bind-Value="_clubConfig.AutoRenewalEnabled"
Color="Color.Primary"
Label="تمدید خودکار عضویت" />
</MudItem>
<MudItem xs="12">
<MudSwitch @bind-Value="_clubConfig.EmailNotificationsEnabled"
Color="Color.Primary"
Label="ارسال ایمیل یادآوری انقضا" />
</MudItem>
<MudItem xs="12">
<MudSwitch @bind-Value="_clubConfig.SmsNotificationsEnabled"
Color="Color.Primary"
Label="ارسال پیامک یادآوری انقضا" />
</MudItem>
</MudGrid>
<MudDivider Class="my-4" />
<MudStack Row="true" Justify="Justify.FlexEnd" Spacing="2">
<MudButton Variant="Variant.Text"
Color="Color.Default"
OnClick="ResetClubConfig">
بازگشت به پیش‌فرض
</MudButton>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="SaveClubConfig"
StartIcon="@Icons.Material.Filled.Save">
ذخیره تنظیمات
</MudButton>
</MudStack>
</MudCardContent>
</MudCard>
</MudTabPanel>
<!-- System Settings -->
<MudTabPanel Text="تنظیمات عمومی" Icon="@Icons.Material.Filled.Settings">
<MudCard Elevation="0">
<MudCardContent>
<MudText Typo="Typo.h6" Class="mb-4">تنظیمات سیستم</MudText>
<MudGrid>
<MudItem xs="12" md="6">
<MudTextField @bind-Value="_systemConfig.SystemName"
Label="نام سیستم"
Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="12" md="6">
<MudTextField @bind-Value="_systemConfig.SupportEmail"
Label="ایمیل پشتیبانی"
Variant="Variant.Outlined"
InputType="InputType.Email" />
</MudItem>
<MudItem xs="12" md="6">
<MudTextField @bind-Value="_systemConfig.SupportPhone"
Label="تلفن پشتیبانی"
Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="12" md="6">
<MudNumericField @bind-Value="_systemConfig.SessionTimeoutMinutes"
Label="مدت اعتبار نشست"
Variant="Variant.Outlined"
Adornment="Adornment.End"
AdornmentText="دقیقه"
Min="5"
Max="1440" />
</MudItem>
<MudItem xs="12" md="6">
<MudNumericField @bind-Value="_systemConfig.MaxLoginAttempts"
Label="حداکثر تلاش ورود"
Variant="Variant.Outlined"
Min="1"
Max="10"
HelperText="قبل از قفل شدن حساب" />
</MudItem>
<MudItem xs="12" md="6">
<MudNumericField @bind-Value="_systemConfig.LockoutDurationMinutes"
Label="مدت قفل حساب"
Variant="Variant.Outlined"
Adornment="Adornment.End"
AdornmentText="دقیقه"
Min="5" />
</MudItem>
<MudItem xs="12">
<MudSwitch @bind-Value="_systemConfig.MaintenanceMode"
Color="Color.Warning"
Label="حالت تعمیر و نگهداری"
HelperText="غیرفعال کردن موقت سیستم برای به‌روزرسانی" />
</MudItem>
<MudItem xs="12">
<MudSwitch @bind-Value="_systemConfig.TwoFactorAuthEnabled"
Color="Color.Primary"
Label="احراز هویت دو مرحله‌ای فعال باشد" />
</MudItem>
<MudItem xs="12">
<MudSwitch @bind-Value="_systemConfig.EmailVerificationRequired"
Color="Color.Primary"
Label="تایید ایمیل الزامی باشد" />
</MudItem>
</MudGrid>
<MudDivider Class="my-4" />
<MudStack Row="true" Justify="Justify.FlexEnd" Spacing="2">
<MudButton Variant="Variant.Text"
Color="Color.Default"
OnClick="ResetSystemConfig">
بازگشت به پیش‌فرض
</MudButton>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="SaveSystemConfig"
StartIcon="@Icons.Material.Filled.Save">
ذخیره تنظیمات
</MudButton>
</MudStack>
</MudCardContent>
</MudCard>
</MudTabPanel>
</MudTabs>
</MudContainer>
@code {
[Inject] public BackOffice.BFF.Configuration.Protobuf.ConfigurationContract.ConfigurationContractClient ConfigurationClient { get; set; }
private CommissionConfig _commissionConfig = new();
private NetworkConfig _networkConfig = new();
private ClubConfig _clubConfig = new();
private SystemConfig _systemConfig = new();
private Dictionary<string, string> _configurations = new();
protected override async Task OnInitializedAsync()
{
await LoadConfigurations();
}
private async Task LoadConfigurations()
{
try
{
var request = new BackOffice.BFF.Configuration.Protobuf.GetAllConfigurationsRequest
{
PageIndex = 1,
PageSize = 100
};
var response = await ConfigurationClient.GetAllConfigurationsAsync(request);
// Build dictionary from response
_configurations = response.Models.ToDictionary(m => m.Key, m => m.Value);
// Map to UI models
_commissionConfig = new CommissionConfig
{
MinBalanceForPayout = GetDecimalConfig("Commission.MinimumPayoutAmount", 100000),
MaxWithdrawalAmount = GetDecimalConfig("Commission.MaxWithdrawalAmount", 50000000),
CommissionPercentage = GetDoubleConfig("Commission.WeeklyPoolContributionPercent", 10),
WeeklyPoolPercentage = GetDoubleConfig("Commission.PoolPercentage", 40),
CalculationDay = GetStringConfig("Commission.CalculationDay", "Saturday"),
CalculationTime = TimeSpan.Parse(GetStringConfig("Commission.CalculationTime", "02:00:00")),
AutoCalculationEnabled = GetBoolConfig("Commission.AutoCalculationEnabled", true),
AutoPayoutEnabled = GetBoolConfig("Commission.AutoPayoutEnabled", false)
};
_networkConfig = new NetworkConfig
{
MaxTreeDepth = GetIntConfig("Network.MaxDepth", 10),
MaxDirectChildren = GetIntConfig("Network.MaxDirectChildren", 2),
MinimumPurchaseForActivation = GetDecimalConfig("Network.MinimumPurchase", 500000),
BalanceExpirationDays = GetIntConfig("Network.BalanceExpirationDays", 365),
BinaryTreeEnabled = GetBoolConfig("Network.BinaryTreeEnabled", true),
AutoPlacementEnabled = GetBoolConfig("Network.AllowOrphanNodes", false),
SpilloverEnabled = GetBoolConfig("Network.SpilloverEnabled", true)
};
_clubConfig = new ClubConfig
{
MonthlyFee = GetDecimalConfig("Club.MonthlyFee", 1000000),
GracePeriodDays = GetIntConfig("Club.GracePeriodDays", 7),
MembershipDurationMonths = GetIntConfig("Club.DefaultMembershipDurationMonths", 12),
MinimumPurchaseForClub = GetDecimalConfig("Club.MinimumActivationAmount", 5000000),
AutoRenewalEnabled = GetBoolConfig("Club.AutoRenewalEnabled", false),
EmailNotificationsEnabled = GetBoolConfig("Club.EmailNotifications", true),
SmsNotificationsEnabled = GetBoolConfig("Club.SmsNotifications", true)
};
_systemConfig = new SystemConfig
{
SystemName = GetStringConfig("System.Name", "FourSat Network"),
SupportEmail = GetStringConfig("System.SupportEmail", "support@foursat.com"),
SupportPhone = GetStringConfig("System.SupportPhone", "021-12345678"),
SessionTimeoutMinutes = GetIntConfig("System.SessionTimeout", 60),
MaxLoginAttempts = GetIntConfig("System.MaxLoginAttempts", 5),
LockoutDurationMinutes = GetIntConfig("System.LockoutDuration", 30),
MaintenanceMode = GetBoolConfig("System.MaintenanceMode", false),
TwoFactorAuthEnabled = GetBoolConfig("System.TwoFactorAuth", true),
EmailVerificationRequired = GetBoolConfig("System.EmailVerification", true)
};
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری تنظیمات: {ex.Message}", Severity.Error);
}
}
private async Task SaveCommissionConfig()
{
try
{
await SaveConfig("Commission.MinimumPayoutAmount", _commissionConfig.MinBalanceForPayout.ToString(), 3);
await SaveConfig("Commission.MaxWithdrawalAmount", _commissionConfig.MaxWithdrawalAmount.ToString(), 3);
await SaveConfig("Commission.WeeklyPoolContributionPercent", _commissionConfig.CommissionPercentage.ToString(), 3);
await SaveConfig("Commission.PoolPercentage", _commissionConfig.WeeklyPoolPercentage.ToString(), 3);
await SaveConfig("Commission.CalculationDay", _commissionConfig.CalculationDay, 3);
await SaveConfig("Commission.CalculationTime", _commissionConfig.CalculationTime?.ToString(@"hh\:mm\:ss") ?? "02:00:00", 3);
await SaveConfig("Commission.AutoCalculationEnabled", _commissionConfig.AutoCalculationEnabled.ToString(), 3);
await SaveConfig("Commission.AutoPayoutEnabled", _commissionConfig.AutoPayoutEnabled.ToString(), 3);
Snackbar.Add("تنظیمات کمیسیون با موفقیت ذخیره شد", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در ذخیره: {ex.Message}", Severity.Error);
}
}
private async Task SaveNetworkConfig()
{
try
{
await SaveConfig("Network.MaxDepth", _networkConfig.MaxTreeDepth.ToString(), 1);
await SaveConfig("Network.MaxDirectChildren", _networkConfig.MaxDirectChildren.ToString(), 1);
await SaveConfig("Network.MinimumPurchase", _networkConfig.MinimumPurchaseForActivation.ToString(), 1);
await SaveConfig("Network.BalanceExpirationDays", _networkConfig.BalanceExpirationDays.ToString(), 1);
await SaveConfig("Network.BinaryTreeEnabled", _networkConfig.BinaryTreeEnabled.ToString(), 1);
await SaveConfig("Network.AllowOrphanNodes", _networkConfig.AutoPlacementEnabled.ToString(), 1);
await SaveConfig("Network.SpilloverEnabled", _networkConfig.SpilloverEnabled.ToString(), 1);
Snackbar.Add("تنظیمات شبکه با موفقیت ذخیره شد", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در ذخیره: {ex.Message}", Severity.Error);
}
}
private async Task SaveClubConfig()
{
try
{
await SaveConfig("Club.MonthlyFee", _clubConfig.MonthlyFee.ToString(), 2);
await SaveConfig("Club.GracePeriodDays", _clubConfig.GracePeriodDays.ToString(), 2);
await SaveConfig("Club.DefaultMembershipDurationMonths", _clubConfig.MembershipDurationMonths.ToString(), 2);
await SaveConfig("Club.MinimumActivationAmount", _clubConfig.MinimumPurchaseForClub.ToString(), 2);
await SaveConfig("Club.AutoRenewalEnabled", _clubConfig.AutoRenewalEnabled.ToString(), 2);
await SaveConfig("Club.EmailNotifications", _clubConfig.EmailNotificationsEnabled.ToString(), 2);
await SaveConfig("Club.SmsNotifications", _clubConfig.SmsNotificationsEnabled.ToString(), 2);
Snackbar.Add("تنظیمات باشگاه با موفقیت ذخیره شد", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در ذخیره: {ex.Message}", Severity.Error);
}
}
private async Task SaveSystemConfig()
{
try
{
await SaveConfig("System.Name", _systemConfig.SystemName, 0);
await SaveConfig("System.SupportEmail", _systemConfig.SupportEmail, 0);
await SaveConfig("System.SupportPhone", _systemConfig.SupportPhone, 0);
await SaveConfig("System.SessionTimeout", _systemConfig.SessionTimeoutMinutes.ToString(), 0);
await SaveConfig("System.MaxLoginAttempts", _systemConfig.MaxLoginAttempts.ToString(), 0);
await SaveConfig("System.LockoutDuration", _systemConfig.LockoutDurationMinutes.ToString(), 0);
await SaveConfig("System.MaintenanceMode", _systemConfig.MaintenanceMode.ToString(), 0);
await SaveConfig("System.TwoFactorAuth", _systemConfig.TwoFactorAuthEnabled.ToString(), 0);
await SaveConfig("System.EmailVerification", _systemConfig.EmailVerificationRequired.ToString(), 0);
Snackbar.Add("تنظیمات سیستم با موفقیت ذخیره شد", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در ذخیره: {ex.Message}", Severity.Error);
}
}
private async Task SaveConfig(string key, string value, int scope)
{
var request = new BackOffice.BFF.Configuration.Protobuf.CreateOrUpdateConfigurationRequest
{
Key = key,
Value = value,
Scope = scope
};
await ConfigurationClient.CreateOrUpdateConfigurationAsync(request);
}
private string GetStringConfig(string key, string defaultValue)
{
return _configurations.TryGetValue(key, out var value) ? value : defaultValue;
}
private int GetIntConfig(string key, int defaultValue)
{
return _configurations.TryGetValue(key, out var value) && int.TryParse(value, out var result) ? result : defaultValue;
}
private decimal GetDecimalConfig(string key, decimal defaultValue)
{
return _configurations.TryGetValue(key, out var value) && decimal.TryParse(value, out var result) ? result : defaultValue;
}
private double GetDoubleConfig(string key, double defaultValue)
{
return _configurations.TryGetValue(key, out var value) && double.TryParse(value, out var result) ? result : defaultValue;
}
private bool GetBoolConfig(string key, bool defaultValue)
{
return _configurations.TryGetValue(key, out var value) && bool.TryParse(value, out var result) ? result : defaultValue;
}
private void ResetCommissionConfig() => _commissionConfig = new CommissionConfig();
private void ResetNetworkConfig() => _networkConfig = new NetworkConfig();
private void ResetClubConfig() => _clubConfig = new ClubConfig();
private void ResetSystemConfig() => _systemConfig = new SystemConfig();
private class CommissionConfig
{
public decimal MinBalanceForPayout { get; set; }
public decimal MaxWithdrawalAmount { get; set; }
public double CommissionPercentage { get; set; }
public double WeeklyPoolPercentage { get; set; }
public string CalculationDay { get; set; } = "Saturday";
public TimeSpan? CalculationTime { get; set; }
public bool AutoCalculationEnabled { get; set; }
public bool AutoPayoutEnabled { get; set; }
}
private class NetworkConfig
{
public int MaxTreeDepth { get; set; }
public int MaxDirectChildren { get; set; }
public decimal MinimumPurchaseForActivation { get; set; }
public int BalanceExpirationDays { get; set; }
public bool BinaryTreeEnabled { get; set; }
public bool AutoPlacementEnabled { get; set; }
public bool SpilloverEnabled { get; set; }
}
private class ClubConfig
{
public decimal MonthlyFee { get; set; }
public int GracePeriodDays { get; set; }
public int MembershipDurationMonths { get; set; }
public decimal MinimumPurchaseForClub { get; set; }
public bool AutoRenewalEnabled { get; set; }
public bool EmailNotificationsEnabled { get; set; }
public bool SmsNotificationsEnabled { get; set; }
}
private class SystemConfig
{
public string SystemName { get; set; } = "";
public string SupportEmail { get; set; } = "";
public string SupportPhone { get; set; } = "";
public int SessionTimeoutMinutes { get; set; }
public int MaxLoginAttempts { get; set; }
public int LockoutDurationMinutes { get; set; }
public bool MaintenanceMode { get; set; }
public bool TwoFactorAuthEnabled { get; set; }
public bool EmailVerificationRequired { get; set; }
}
}

View File

@@ -0,0 +1,466 @@
@page "/system/health"
@attribute [Authorize]
@using MudBlazor
@using BackOffice.BFF.Health.Protobuf
@inject HealthContract.HealthContractClient HealthClient
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
<MudText Typo="Typo.h4" GutterBottom="true">داشبورد سلامت سیستم</MudText>
<MudText Typo="Typo.body1" Color="Color.Secondary" Class="mb-4">
مانیتورینگ وضعیت سرویس‌ها و منابع سیستم
</MudText>
<!-- Overall System Status -->
<MudCard Class="mb-4" Elevation="3">
<MudCardContent>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudStack Spacing="2">
<MudText Typo="Typo.h5">وضعیت کلی سیستم</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">
آخرین بروزرسانی: @_lastUpdated.ToString("HH:mm:ss")
</MudText>
</MudStack>
<MudChip T="string"
Size="Size.Large"
Color="@(_overallStatus == "Healthy" ? Color.Success : Color.Error)"
Icon="@(_overallStatus == "Healthy" ? Icons.Material.Filled.CheckCircle : Icons.Material.Filled.Error)">
@(_overallStatus == "Healthy" ? "سالم" : "مشکل‌دار")
</MudChip>
</MudStack>
</MudCardContent>
</MudCard>
<!-- Services Health -->
<MudText Typo="Typo.h6" Class="mb-3">وضعیت سرویس‌ها</MudText>
<MudGrid Class="mb-4">
@foreach (var service in _services)
{
<MudItem xs="12" md="6" lg="3">
<MudCard Elevation="2">
<MudCardContent>
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h6">@service.Name</MudText>
<MudIcon Icon="@GetServiceIcon(service.Type)"
Color="@GetHealthColor(service.Status)"
Size="Size.Large" />
</MudStack>
<MudChip T="string"
Size="Size.Small"
Color="@GetHealthColor(service.Status)">
@GetHealthText(service.Status)
</MudChip>
<MudDivider />
<MudStack Spacing="1">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.caption" Color="Color.Secondary">Uptime:</MudText>
<MudText Typo="Typo.caption"><strong>@service.Uptime</strong></MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.caption" Color="Color.Secondary">زمان پاسخ:</MudText>
<MudText Typo="Typo.caption"><strong>@service.ResponseTime ms</strong></MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.caption" Color="Color.Secondary">نسخه:</MudText>
<MudText Typo="Typo.caption"><strong>@service.Version</strong></MudText>
</MudStack>
</MudStack>
@if (!string.IsNullOrEmpty(service.LastError))
{
<MudAlert Severity="Severity.Error" Dense="true" Class="mt-2">
@service.LastError
</MudAlert>
}
</MudStack>
</MudCardContent>
<MudCardActions>
<MudButton Size="Size.Small"
Color="Color.Primary"
OnClick="@(() => CheckServiceHealth(service))">
بررسی
</MudButton>
<MudButton Size="Size.Small"
Color="Color.Secondary"
OnClick="@(() => ViewServiceLogs(service))">
لاگ‌ها
</MudButton>
</MudCardActions>
</MudCard>
</MudItem>
}
</MudGrid>
<!-- System Resources -->
<MudText Typo="Typo.h6" Class="mb-3">منابع سیستم</MudText>
<MudGrid Class="mb-4">
<!-- CPU Usage -->
<MudItem xs="12" md="6" lg="3">
<MudCard Elevation="2">
<MudCardContent>
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2">CPU</MudText>
<MudText Typo="Typo.h6" Color="@GetResourceColor(_cpuUsage)">@_cpuUsage%</MudText>
</MudStack>
<MudProgressLinear Color="@GetResourceColor(_cpuUsage)"
Value="@_cpuUsage"
Class="my-2" />
<MudText Typo="Typo.caption" Color="Color.Secondary">
میانگین 5 دقیقه اخیر
</MudText>
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
<!-- Memory Usage -->
<MudItem xs="12" md="6" lg="3">
<MudCard Elevation="2">
<MudCardContent>
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2">حافظه</MudText>
<MudText Typo="Typo.h6" Color="@GetResourceColor(_memoryUsage)">@_memoryUsage%</MudText>
</MudStack>
<MudProgressLinear Color="@GetResourceColor(_memoryUsage)"
Value="@_memoryUsage"
Class="my-2" />
<MudText Typo="Typo.caption" Color="Color.Secondary">
@_memoryUsed GB از @_memoryTotal GB
</MudText>
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
<!-- Disk Usage -->
<MudItem xs="12" md="6" lg="3">
<MudCard Elevation="2">
<MudCardContent>
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2">دیسک</MudText>
<MudText Typo="Typo.h6" Color="@GetResourceColor(_diskUsage)">@_diskUsage%</MudText>
</MudStack>
<MudProgressLinear Color="@GetResourceColor(_diskUsage)"
Value="@_diskUsage"
Class="my-2" />
<MudText Typo="Typo.caption" Color="Color.Secondary">
@_diskUsed GB از @_diskTotal GB
</MudText>
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
<!-- Network -->
<MudItem xs="12" md="6" lg="3">
<MudCard Elevation="2">
<MudCardContent>
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2">شبکه</MudText>
<MudIcon Icon="@Icons.Material.Filled.NetworkCheck" Color="Color.Success" />
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.caption">ورودی:</MudText>
<MudText Typo="Typo.caption"><strong>@_networkIn MB/s</strong></MudText>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.caption">خروجی:</MudText>
<MudText Typo="Typo.caption"><strong>@_networkOut MB/s</strong></MudText>
</MudStack>
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
<!-- Recent Events -->
<MudText Typo="Typo.h6" Class="mb-3">رویدادهای اخیر سیستم</MudText>
<MudCard>
<MudCardContent>
<MudTimeline TimelineOrientation="TimelineOrientation.Vertical">
@foreach (var evt in _recentEvents)
{
<MudTimelineItem Color="@GetEventColor(evt.Type)" Size="Size.Small">
<ItemOpposite>
<MudText Typo="Typo.caption" Color="Color.Secondary">
@evt.Timestamp.ToString("HH:mm:ss")
</MudText>
</ItemOpposite>
<ItemContent>
<MudStack Spacing="0">
<MudText Typo="Typo.body2"><strong>@evt.Message</strong></MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">@evt.Source</MudText>
</MudStack>
</ItemContent>
</MudTimelineItem>
}
</MudTimeline>
</MudCardContent>
</MudCard>
</MudContainer>
@code {
private string _overallStatus = "Healthy";
private DateTime _lastUpdated = DateTime.Now;
private List<ServiceHealth> _services = new();
// Resource metrics
private double _cpuUsage = 45.2;
private double _memoryUsage = 62.8;
private double _diskUsage = 73.5;
private double _memoryUsed = 10.1;
private double _memoryTotal = 16.0;
private double _diskUsed = 147.0;
private double _diskTotal = 200.0;
private double _networkIn = 12.5;
private double _networkOut = 8.3;
private List<SystemEvent> _recentEvents = new();
protected override async Task OnInitializedAsync()
{
await LoadHealthData();
}
private async Task LoadHealthData()
{
try
{
var request = new GetSystemHealthRequest();
var response = await HealthClient.GetSystemHealthAsync(request);
_overallStatus = response.OverallHealthy ? "Healthy" : "Unhealthy";
_lastUpdated = DateTime.Now;
// Map gRPC services to UI model
_services = response.Services.Select(s => new ServiceHealth
{
Name = s.ServiceName,
Type = GetServiceType(s.ServiceName),
Status = s.Status,
Uptime = "99.9%", // Mock - not in API
ResponseTime = (int)s.ResponseTimeMs,
Version = "1.0.0", // Mock - not in API
LastError = s.Status != "Healthy" ? s.Description : null
}).ToList();
// Keep mock events for now
_recentEvents = GenerateMockEvents();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری اطلاعات: {ex.Message}", Severity.Error);
// Fallback to mock data
_services = GenerateMockServices();
_recentEvents = GenerateMockEvents();
}
}
private async Task CheckServiceHealth(ServiceHealth service)
{
try
{
Snackbar.Add($"در حال بررسی {service.Name}...", Severity.Info);
await Task.Delay(1000);
// Refresh health check for this specific service
await LoadHealthData();
service = _services.FirstOrDefault(s => s.Name == service.Name) ?? service;
Snackbar.Add($"{service.Name} سالم است", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
}
private void ViewServiceLogs(ServiceHealth service)
{
// Future enhancement: Open logs dialog showing service execution history
Snackbar.Add($"نمایش لاگ‌های {service.Name}", Severity.Info);
}
private Color GetHealthColor(string status)
{
return status switch
{
"Healthy" => Color.Success,
"Degraded" => Color.Warning,
"Unhealthy" => Color.Error,
_ => Color.Default
};
}
private string GetHealthText(string status)
{
return status switch
{
"Healthy" => "سالم",
"Degraded" => "کاهش عملکرد",
"Unhealthy" => "ناسالم",
_ => "نامشخص"
};
}
private Color GetResourceColor(double usage)
{
if (usage < 70) return Color.Success;
if (usage < 85) return Color.Warning;
return Color.Error;
}
private Color GetEventColor(string type)
{
return type switch
{
"Info" => Color.Info,
"Warning" => Color.Warning,
"Error" => Color.Error,
"Success" => Color.Success,
_ => Color.Default
};
}
private string GetServiceIcon(string type)
{
return type switch
{
"API" => Icons.Material.Filled.Api,
"Database" => Icons.Material.Filled.Storage,
"Cache" => Icons.Material.Filled.Memory,
"Queue" => Icons.Material.Filled.Queue,
_ => Icons.Material.Filled.Cloud
};
}
private string GetServiceType(string serviceName)
{
if (serviceName.Contains("Commission") || serviceName.Contains("Configuration")) return "API";
if (serviceName.Contains("Network") || serviceName.Contains("Club")) return "API";
if (serviceName.Contains("Database") || serviceName.Contains("SQL")) return "Database";
if (serviceName.Contains("Redis") || serviceName.Contains("Cache")) return "Cache";
return "API";
}
private List<ServiceHealth> GenerateMockServices()
{
return new List<ServiceHealth>
{
new() {
Name = "CMS API",
Type = "API",
Status = "Healthy",
Uptime = "99.98%",
ResponseTime = 85,
Version = "1.2.3"
},
new() {
Name = "BFF API",
Type = "API",
Status = "Healthy",
Uptime = "99.95%",
ResponseTime = 62,
Version = "1.1.0"
},
new() {
Name = "PostgreSQL",
Type = "Database",
Status = "Healthy",
Uptime = "99.99%",
ResponseTime = 12,
Version = "15.3"
},
new() {
Name = "Redis Cache",
Type = "Cache",
Status = "Degraded",
Uptime = "98.50%",
ResponseTime = 245,
Version = "7.0",
LastError = "زمان پاسخ بالاتر از حد معمول"
},
new() {
Name = "RabbitMQ",
Type = "Queue",
Status = "Healthy",
Uptime = "99.92%",
ResponseTime = 35,
Version = "3.12"
},
new() {
Name = "Email Service",
Type = "API",
Status = "Healthy",
Uptime = "99.80%",
ResponseTime = 156,
Version = "1.0.5"
}
};
}
private List<SystemEvent> GenerateMockEvents()
{
var now = DateTime.Now;
return new List<SystemEvent>
{
new() {
Timestamp = now.AddMinutes(-2),
Type = "Success",
Message = "محاسبه کمیسیون هفتگی با موفقیت انجام شد",
Source = "Commission Worker"
},
new() {
Timestamp = now.AddMinutes(-5),
Type = "Info",
Message = "پشتیبان‌گیری خودکار پایگاه داده آغاز شد",
Source = "Database Service"
},
new() {
Timestamp = now.AddMinutes(-12),
Type = "Warning",
Message = "Redis Cache زمان پاسخ بالای 200ms",
Source = "Health Monitor"
},
new() {
Timestamp = now.AddMinutes(-18),
Type = "Info",
Message = "سرویس BFF API راه‌اندازی مجدد شد",
Source = "System"
},
new() {
Timestamp = now.AddMinutes(-25),
Type = "Success",
Message = "10 درخواست برداشت پردازش شد",
Source = "Payment Service"
}
};
}
private class ServiceHealth
{
public string Name { get; set; } = "";
public string Type { get; set; } = "";
public string Status { get; set; } = "";
public string Uptime { get; set; } = "";
public int ResponseTime { get; set; }
public string Version { get; set; } = "";
public string? LastError { get; set; }
}
private class SystemEvent
{
public DateTime Timestamp { get; set; }
public string Type { get; set; } = "";
public string Message { get; set; } = "";
public string Source { get; set; } = "";
}
}

View File

@@ -139,22 +139,24 @@
<MudTh>هفته</MudTh>
<MudTh>وضعیت</MudTh>
<MudTh>مدت زمان</MudTh>
<MudTh>پیام</MudTh>
<MudTh>تعداد پردازش</MudTh>
<MudTh>پیام خطا</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="زمان">@context.ExecutedAt.ToString("yyyy/MM/dd HH:mm:ss")</MudTd>
<MudTd DataLabel="زمان">@context.ExecutionTime.ToString("yyyy/MM/dd HH:mm:ss")</MudTd>
<MudTd DataLabel="هفته">@context.WeekNumber</MudTd>
<MudTd DataLabel="وضعیت">
<MudChip T="string"
Color="@(context.IsSuccess ? Color.Success : Color.Error)"
Color="@(context.Status == "موفق" ? Color.Success : Color.Error)"
Size="Size.Small">
@(context.IsSuccess ? "موفق" : "خطا")
@context.Status
</MudChip>
</MudTd>
<MudTd DataLabel="مدت زمان">@context.DurationSeconds ثانیه</MudTd>
<MudTd DataLabel="پیام">
<MudText Typo="Typo.body2" Color="@(context.IsSuccess ? Color.Default : Color.Error)">
@context.Message
<MudTd DataLabel="مدت زمان">@context.Duration</MudTd>
<MudTd DataLabel="تعداد پردازش">@context.ProcessedCount</MudTd>
<MudTd DataLabel="پیام خطا">
<MudText Typo="Typo.body2" Color="@(string.IsNullOrEmpty(context.ErrorMessage) ? Color.Default : Color.Error)">
@(string.IsNullOrEmpty(context.ErrorMessage) ? "-" : context.ErrorMessage)
</MudText>
</MudTd>
</RowTemplate>
@@ -174,6 +176,7 @@
</MudContainer>
@code {
[Inject] public BackOffice.BFF.Commission.Protobuf.CommissionContract.CommissionContractClient CommissionClient { get; set; }
private WorkerStatus _workerStatus = WorkerStatus.Running;
private DateTime _lastRunTime = DateTime.Now.AddHours(-2);
@@ -186,8 +189,22 @@
protected override async Task OnInitializedAsync()
{
// TODO: Load actual worker status from API
GenerateMockLog();
try
{
var statusResponse = await CommissionClient.GetWorkerStatusAsync(new BackOffice.BFF.Commission.Protobuf.GetWorkerStatusRequest());
_workerStatus = statusResponse.IsRunning ? WorkerStatus.Running : WorkerStatus.Paused;
_lastRunTime = statusResponse.LastRunAt?.ToDateTime() ?? DateTime.MinValue;
_nextRunTime = statusResponse.NextScheduledRun?.ToDateTime() ?? DateTime.MinValue;
_successfulRuns = statusResponse.SuccessfulExecutions;
_failedRuns = statusResponse.FailedExecutions;
await RefreshLog();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری وضعیت: {ex.Message}", Severity.Error);
_executionLog = new List<ExecutionLogModel>();
}
}
private async Task RunManualCalculation()
@@ -208,8 +225,11 @@
_isProcessing = true;
try
{
// TODO: Call TriggerWeeklyCalculationCommand API
await Task.Delay(2000); // Simulate API call
var request = new BackOffice.BFF.Commission.Protobuf.TriggerWeeklyCalculationRequest
{
WeekNumber = _manualWeekNumber
};
await CommissionClient.TriggerWeeklyCalculationAsync(request);
Snackbar.Add($"محاسبات هفته {_manualWeekNumber} با موفقیت آغاز شد", Severity.Success);
_manualWeekNumber = "";
@@ -230,84 +250,44 @@
{
var confirmed = await DialogService.ShowMessageBox(
"توقف Worker",
"آیا از توقف موقت Worker اطمینان دارید؟ محاسبات خودکار متوقف خواهد شد.",
yesText: "بله، متوقف شود", cancelText: "لغو");
if (confirmed == true)
{
_isProcessing = true;
try
{
// TODO: Call PauseWorker API
await Task.Delay(1000);
_workerStatus = WorkerStatus.Paused;
Snackbar.Add("Worker با موفقیت متوقف شد", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_isProcessing = false;
}
}
"این قابلیت فعلا در دسترس نیست",
yesText: "تایید", cancelText: null);
}
private async Task ResumeWorker()
{
_isProcessing = true;
try
{
// TODO: Call ResumeWorker API
await Task.Delay(1000);
_workerStatus = WorkerStatus.Running;
Snackbar.Add("Worker با موفقیت ازسرگیری شد", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_isProcessing = false;
}
Snackbar.Add("این قابلیت فعلا در دسترس نیست", Severity.Info);
}
private async Task RestartWorker()
{
var confirmed = await DialogService.ShowMessageBox(
"راه‌اندازی مجدد Worker",
"آیا از راه‌اندازی مجدد Worker اطمینان دارید؟",
yesText: "بله", cancelText: "لغو");
if (confirmed == true)
{
_isProcessing = true;
try
{
// TODO: Call RestartWorker API
await Task.Delay(1500);
_workerStatus = WorkerStatus.Running;
Snackbar.Add("Worker با موفقیت راه‌اندازی مجدد شد", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_isProcessing = false;
}
}
"این قابلیت فعلا در دسترس نیست",
yesText: "تایید", cancelText: null);
}
private async Task RefreshLog()
{
try
{
// TODO: Load actual execution log from API
GenerateMockLog();
var request = new BackOffice.BFF.Commission.Protobuf.GetWorkerExecutionLogsRequest
{
PageIndex = 1,
PageSize = 20
};
var response = await CommissionClient.GetWorkerExecutionLogsAsync(request);
_executionLog = response.Models.Select(log => new ExecutionLogModel
{
ExecutionTime = log.StartedAt?.ToDateTime() ?? DateTime.MinValue,
WeekNumber = log.WeekNumber,
Status = log.Success ? "موفق" : "ناموفق",
Duration = $"{log.DurationMs / 1000.0:F1} ثانیه",
ProcessedCount = log.RecordsProcessed,
ErrorMessage = log.ErrorMessage ?? ""
}).ToList();
Snackbar.Add("تاریخچه بروزرسانی شد", Severity.Info);
}
catch (Exception ex)
@@ -327,19 +307,6 @@
};
}
private void GenerateMockLog()
{
var random = new Random();
_executionLog = Enumerable.Range(0, 20).Select(i => new ExecutionLogModel
{
ExecutedAt = DateTime.Now.AddHours(-i * 168), // Weekly intervals
WeekNumber = $"2025-W{48 - i:D2}",
IsSuccess = random.Next(0, 10) < 9, // 90% success rate
DurationSeconds = random.Next(30, 300),
Message = random.Next(0, 10) < 9 ? "محاسبات با موفقیت انجام شد" : "خطا در اتصال به سرویس CMS"
}).ToList();
}
private enum WorkerStatus
{
Running,
@@ -349,10 +316,11 @@
private class ExecutionLogModel
{
public DateTime ExecutedAt { get; set; }
public string WeekNumber { get; set; }
public bool IsSuccess { get; set; }
public int DurationSeconds { get; set; }
public string Message { get; set; }
public DateTime ExecutionTime { get; set; }
public string WeekNumber { get; set; } = "";
public string Status { get; set; } = "";
public string Duration { get; set; } = "";
public int ProcessedCount { get; set; }
public string ErrorMessage { get; set; } = "";
}
}

View File

@@ -10,6 +10,72 @@
داشبورد
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/dashboard/overview"
Icon="@Icons.Material.Filled.ViewQuilt">
نمای کلی سیستم
</MudNavLink>
<MudDivider Class="my-2" />
<MudText Class="nav-menu__title" Typo="Typo.subtitle2">کمیسیون و شبکه</MudText>
<MudNavGroup Title="کمیسیون" Icon="@Icons.Material.Filled.AccountBalanceWallet" Expanded="false">
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/commission/dashboard"
Icon="@Icons.Material.Filled.Dashboard">
داشبورد کمیسیون
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/commission/reports"
Icon="@Icons.Material.Filled.Assessment">
گزارش‌های هفتگی
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/commission/payouts"
Icon="@Icons.Material.Filled.Payments">
پرداخت کاربران
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/commission/withdrawals"
Icon="@Icons.Material.Filled.RequestQuote">
درخواست‌های برداشت
</MudNavLink>
</MudNavGroup>
<MudNavGroup Title="شبکه" Icon="@Icons.Material.Filled.AccountTree" Expanded="false">
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/network/tree"
Icon="@Icons.Material.Filled.AccountTree">
درخت شبکه
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/network/balances"
Icon="@Icons.Material.Filled.Balance">
گزارش موجودی‌ها
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/network/statistics"
Icon="@Icons.Material.Filled.BarChart">
آمار شبکه
</MudNavLink>
</MudNavGroup>
<MudNavGroup Title="باشگاه" Icon="@Icons.Material.Filled.CardMembership" Expanded="false">
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/club/members"
Icon="@Icons.Material.Filled.Groups">
اعضای باشگاه
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/club/statistics"
Icon="@Icons.Material.Filled.PieChart">
آمار باشگاه
</MudNavLink>
</MudNavGroup>
<MudDivider Class="my-2" />
<MudText Class="nav-menu__title" Typo="Typo.subtitle2">مدیریت</MudText>
<AuthorizeView Roles="Administrator">
<Authorized>
<MudNavLink Match="NavLinkMatch.Prefix"
@@ -51,6 +117,37 @@
</AuthorizeView>
<MudDivider Class="my-2" />
<MudText Class="nav-menu__title" Typo="Typo.subtitle2">سیستم</MudText>
<AuthorizeView Roles="Administrator">
<Authorized>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/system/alerts"
Icon="@Icons.Material.Filled.NotificationsActive">
مدیریت هشدارها
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/system/health"
Icon="@Icons.Material.Filled.MonitorHeart">
سلامت سیستم
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/system/configuration"
Icon="@Icons.Material.Filled.Tune">
تنظیمات سیستم
</MudNavLink>
</Authorized>
</AuthorizeView>
<MudDivider Class="my-2" />
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/settings"
Icon="@Icons.Material.Filled.Settings">
تنظیمات
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Color="Color.Error"