Add commission, network, and club management pages with dialogs

This commit is contained in:
masoodafar-web
2025-11-30 23:38:24 +03:30
parent ed09f9258a
commit aaa15d8839
23 changed files with 4641 additions and 0 deletions

1316
docs/development-plan.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -37,6 +37,9 @@
<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="HtmlAgilityPack" Version="1.12.1" />
<PackageReference Include="DateTimeConverterCL" Version="1.0.0" />
<PackageReference Include="Grpc.Core" Version="2.46.6" />

View File

@@ -8,6 +8,9 @@ using BackOffice.BFF.UserAddress.Protobuf.Protos.UserAddress;
using BackOffice.BFF.UserOrder.Protobuf.Protos.UserOrder;
using BackOffice.BFF.UserRole.Protobuf.Protos.UserRole;
using BackOffice.BFF.Category.Protobuf.Protos.Category;
using BackOffice.BFF.Commission.Protobuf;
using BackOffice.BFF.NetworkMembership.Protobuf;
using BackOffice.BFF.ClubMembership.Protobuf;
using BackOffice.Common.Utilities;
using Blazored.LocalStorage;
using Grpc.Core;
@@ -77,6 +80,11 @@ 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)
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>()));
return services;
}

View File

@@ -0,0 +1,112 @@
@page "/club/members"
@attribute [Authorize]
@using BackOffice.BFF.ClubMembership.Protobuf
@using Google.Protobuf.WellKnownTypes
@using BackOffice.Pages.Club.Components
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
<MudText Typo="Typo.h4" Class="mb-4">مدیریت اعضای باشگاه</MudText>
<MudDataGrid T="ClubMembershipModel"
ServerData="@(new Func<GridState<ClubMembershipModel>, Task<GridData<ClubMembershipModel>>>(ServerReload))"
Hover="true"
@ref="_gridData"
Height="75vh">
<ToolBarContent>
<MudText Typo="Typo.h6">لیست اعضا</MudText>
<MudSpacer />
<MudStack Row="true" Spacing="2">
<MudSwitch @bind-Value="_filterIsActive"
Color="Color.Success"
Label="فقط فعال‌ها" />
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="ApplyFilter"
StartIcon="@Icons.Material.Filled.Refresh">
بارگذاری
</MudButton>
<MudButton Variant="Variant.Filled"
Color="Color.Success"
OnClick="OpenActivateDialog"
StartIcon="@Icons.Material.Filled.Add">
فعال‌سازی عضو جدید
</MudButton>
</MudStack>
</ToolBarContent>
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<PropertyColumn Property="x => x.UserId" Title="کاربر">
<CellTemplate>
<MudStack Spacing="0">
<MudText Typo="Typo.body2"><strong>@context.Item.UserName</strong></MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">ID: @context.Item.UserId</MudText>
</MudStack>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.PackageName" Title="پکیج">
<CellTemplate>
<MudChip T="string" Color="Color.Info" Size="Size.Small">
@context.Item.PackageName
</MudChip>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.ActivationCode" Title="کد فعال‌سازی" />
<PropertyColumn Property="x => x.ActivatedAt" Title="تاریخ فعال‌سازی">
<CellTemplate>
@context.Item.ActivatedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd")
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.ExpiresAt" Title="تاریخ انقضا">
<CellTemplate>
<MudText Color="@(context.Item.IsExpired ? Color.Error : Color.Success)">
@context.Item.ExpiresAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd")
</MudText>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.IsActive" Title="وضعیت">
<CellTemplate>
<MudChip T="string"
Color="@(context.Item.IsActive ? Color.Success : Color.Error)"
Size="Size.Small">
@(context.Item.IsActive ? "فعال" : "غیرفعال")
</MudChip>
</CellTemplate>
</PropertyColumn>
<TemplateColumn Title="عملیات" Sortable="false">
<CellTemplate>
<MudStack Row="true" Spacing="1">
<MudTooltip Text="مشاهده جزئیات">
<MudIconButton Icon="@Icons.Material.Filled.Visibility"
Size="Size.Small"
Color="Color.Info"
OnClick="@(() => ViewDetails(context.Item))" />
</MudTooltip>
@if (context.Item.IsActive)
{
<MudTooltip Text="غیرفعال کردن">
<MudIconButton Icon="@Icons.Material.Filled.Block"
Size="Size.Small"
Color="Color.Error"
OnClick="@(() => DeactivateMembership(context.Item))" />
</MudTooltip>
}
</MudStack>
</CellTemplate>
</TemplateColumn>
</Columns>
<PagerContent>
<MudDataGridPager T="ClubMembershipModel"
PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد سطرهای صفحه" />
</PagerContent>
</MudDataGrid>
</MudContainer>

View File

@@ -0,0 +1,113 @@
using BackOffice.BFF.ClubMembership.Protobuf;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using Google.Protobuf.WellKnownTypes;
using BackOffice.Pages.Club.Components;
namespace BackOffice.Pages.Club;
public partial class ClubMembers
{
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubContract { get; set; }
private MudDataGrid<ClubMembershipModel> _gridData;
private bool? _filterIsActive = true;
private async Task<GridData<ClubMembershipModel>> ServerReload(GridState<ClubMembershipModel> state)
{
try
{
var request = new GetAllClubMembershipsRequest
{
PageIndex = state.Page + 1,
PageSize = state.PageSize
};
if (_filterIsActive.HasValue)
{
request.IsActive = _filterIsActive.Value;
}
var result = await ClubContract.GetAllClubMembershipsAsync(request);
if (result?.Models != null && result.Models.Any())
{
return new GridData<ClubMembershipModel>
{
Items = result.Models.ToList(),
TotalItems = (int)result.MetaData.TotalCount
};
}
return new GridData<ClubMembershipModel>();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری داده‌ها: {ex.Message}", Severity.Error);
return new GridData<ClubMembershipModel>();
}
}
private async Task ApplyFilter()
{
if (_gridData != null)
{
await _gridData.ReloadServerData();
}
}
private async Task OpenActivateDialog()
{
var dialog = await DialogService.ShowAsync<ActivateClubDialog>("فعال‌سازی عضویت باشگاه", new DialogOptions
{
CloseButton = true,
MaxWidth = MaxWidth.Small,
FullWidth = true
});
var result = await dialog.Result;
if (!result.Canceled)
{
Snackbar.Add("عضویت با موفقیت فعال شد", Severity.Success);
await ApplyFilter();
}
}
private async Task ViewDetails(ClubMembershipModel member)
{
var parameters = new DialogParameters
{
["Member"] = member
};
await DialogService.ShowAsync<MemberDetailsDialog>("جزئیات عضویت", parameters, new DialogOptions
{
CloseButton = true,
MaxWidth = MaxWidth.Medium,
FullWidth = true
});
}
private async Task DeactivateMembership(ClubMembershipModel member)
{
var parameters = new DialogParameters
{
["UserId"] = member.UserId,
["UserName"] = member.UserName
};
var dialog = await DialogService.ShowAsync<DeactivateClubDialog>("غیرفعال‌سازی عضویت", parameters, new DialogOptions
{
CloseButton = true,
MaxWidth = MaxWidth.Small,
FullWidth = true
});
var result = await dialog.Result;
if (!result.Canceled)
{
Snackbar.Add("عضویت با موفقیت غیرفعال شد", Severity.Warning);
await ApplyFilter();
}
}
}

View File

@@ -0,0 +1,101 @@
@using BackOffice.BFF.ClubMembership.Protobuf
<MudDialog>
<DialogContent>
<MudForm @ref="_form" @bind-IsValid="_formValid">
<MudStack Spacing="3">
<MudNumericField @bind-Value="_model.UserId"
Label="شناسه کاربر"
Required="true"
RequiredError="شناسه کاربر الزامی است"
Variant="Variant.Outlined" />
<MudNumericField @bind-Value="_model.PackageId"
Label="شناسه پکیج"
Required="true"
RequiredError="شناسه پکیج الزامی است"
Variant="Variant.Outlined" />
<MudNumericField @bind-Value="_model.DurationMonths"
Label="مدت زمان (ماه)"
Required="true"
RequiredError="مدت زمان الزامی است"
Min="1"
Max="12"
Variant="Variant.Outlined" />
<MudAlert Severity="Severity.Info" Dense="true">
<MudText Typo="Typo.body2">
با تایید، عضویت باشگاه برای کاربر فعال می‌شود.
</MudText>
</MudAlert>
</MudStack>
</MudForm>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">لغو</MudButton>
<MudButton Color="Color.Success"
Variant="Variant.Filled"
OnClick="Submit"
Disabled="@(!_formValid || _isSubmitting)">
@if (_isSubmitting)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" />
<MudText Class="ms-2">در حال پردازش...</MudText>
}
else
{
<text>تایید و فعال‌سازی</text>
}
</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; }
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubContract { get; set; }
private MudForm _form;
private bool _formValid;
private bool _isSubmitting;
private ActivateModel _model = new();
private void Cancel() => MudDialog.Cancel();
private async Task Submit()
{
await _form.Validate();
if (!_formValid) return;
_isSubmitting = true;
try
{
var request = new ActivateClubMembershipRequest
{
UserId = _model.UserId,
PackageId = _model.PackageId,
DurationMonths = _model.DurationMonths
};
var response = await ClubContract.ActivateClubMembershipAsync(request);
Snackbar.Add($"عضویت با موفقیت فعال شد.", Severity.Success);
MudDialog.Close(DialogResult.Ok(true));
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_isSubmitting = false;
}
}
private class ActivateModel
{
public long UserId { get; set; }
public long PackageId { get; set; }
public int DurationMonths { get; set; } = 1;
}
}

View File

@@ -0,0 +1,87 @@
@using BackOffice.BFF.ClubMembership.Protobuf
<MudDialog>
<DialogContent>
<MudStack Spacing="3">
<MudAlert Severity="Severity.Warning" Dense="true">
<MudText Typo="Typo.body1">
<strong>آیا از غیرفعال کردن عضویت باشگاه کاربر "@UserName" مطمئن هستید؟</strong>
</MudText>
</MudAlert>
<MudText Typo="Typo.body2">
این عمل باعث می‌شود:
</MudText>
<MudList T="string" Dense="true">
<MudListItem T="string" Icon="@Icons.Material.Filled.Block">
کاربر دیگر به مزایای باشگاه دسترسی نخواهد داشت
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.Warning">
Commission های جاری تا پایان هفته ادامه خواهد یافت
</MudListItem>
</MudList>
<MudTextField @bind-Value="_reason"
Label="دلیل غیرفعال‌سازی"
Lines="3"
Variant="Variant.Outlined"
Placeholder="دلیل خود را وارد کنید..." />
</MudStack>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">لغو</MudButton>
<MudButton Color="Color.Error"
Variant="Variant.Filled"
OnClick="Submit"
Disabled="_isSubmitting">
@if (_isSubmitting)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" />
<MudText Class="ms-2">در حال پردازش...</MudText>
}
else
{
<text>غیرفعال کردن</text>
}
</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; }
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubContract { get; set; }
[Parameter] public long UserId { get; set; }
[Parameter] public string UserName { get; set; }
private string _reason;
private bool _isSubmitting;
private void Cancel() => MudDialog.Cancel();
private async Task Submit()
{
_isSubmitting = true;
try
{
var request = new DeactivateClubMembershipRequest
{
UserId = UserId,
Reason = _reason ?? string.Empty
};
var response = await ClubContract.DeactivateClubMembershipAsync(request);
Snackbar.Add("عضویت با موفقیت غیرفعال شد", Severity.Success);
MudDialog.Close(DialogResult.Ok(true));
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_isSubmitting = false;
}
}
}

View File

@@ -0,0 +1,103 @@
@using BackOffice.BFF.ClubMembership.Protobuf
<MudDialog>
<DialogContent>
@if (Member != null)
{
<MudStack Spacing="3">
<MudPaper Class="pa-3" Elevation="0">
<MudText Typo="Typo.h6" Color="Color.Primary">اطلاعات کاربر</MudText>
<MudDivider Class="my-2" />
<MudSimpleTable Dense="true">
<tbody>
<tr>
<td><strong>شناسه:</strong></td>
<td>@Member.UserId</td>
</tr>
<tr>
<td><strong>نام کاربر:</strong></td>
<td>@Member.UserName</td>
</tr>
</tbody>
</MudSimpleTable>
</MudPaper>
<MudPaper Class="pa-3" Elevation="0">
<MudText Typo="Typo.h6" Color="Color.Primary">اطلاعات عضویت</MudText>
<MudDivider Class="my-2" />
<MudSimpleTable Dense="true">
<tbody>
<tr>
<td><strong>شناسه عضویت:</strong></td>
<td>@Member.Id</td>
</tr>
<tr>
<td><strong>پکیج:</strong></td>
<td>@Member.PackageName</td>
</tr>
<tr>
<td><strong>کد فعال‌سازی:</strong></td>
<td dir="ltr">@Member.ActivationCode</td>
</tr>
<tr>
<td><strong>تاریخ فعال‌سازی:</strong></td>
<td>@Member.ActivatedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm")</td>
</tr>
<tr>
<td><strong>تاریخ انقضا:</strong></td>
<td>
<MudText Color="@(Member.IsExpired ? Color.Error : Color.Success)">
@Member.ExpiresAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm")
</MudText>
</td>
</tr>
<tr>
<td><strong>وضعیت:</strong></td>
<td>
<MudChip T="string"
Color="@(Member.IsActive ? Color.Success : Color.Error)"
Size="Size.Small">
@(Member.IsActive ? "فعال" : "غیرفعال")
</MudChip>
</td>
</tr>
<tr>
<td><strong>منقضی شده:</strong></td>
<td>
<MudChip T="string"
Color="@(Member.IsExpired ? Color.Warning : Color.Default)"
Size="Size.Small">
@(Member.IsExpired ? "بله" : "خیر")
</MudChip>
</td>
</tr>
</tbody>
</MudSimpleTable>
</MudPaper>
<MudPaper Class="pa-3" Elevation="0">
<MudText Typo="Typo.h6" Color="Color.Primary">تاریخچه</MudText>
<MudDivider Class="my-2" />
<MudSimpleTable Dense="true">
<tbody>
<tr>
<td><strong>تاریخ ایجاد:</strong></td>
<td>@Member.Created.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss")</td>
</tr>
</tbody>
</MudSimpleTable>
</MudPaper>
</MudStack>
}
</DialogContent>
<DialogActions>
<MudButton Color="Color.Primary" OnClick="Close">بستن</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; }
[Parameter] public ClubMembershipModel Member { get; set; }
private void Close() => MudDialog.Close();
}

View File

@@ -0,0 +1,286 @@
@page "/club/statistics"
@using MudBlazor
@using BackOffice.BFF.ClubMembership.Protobuf
<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
{
<!-- Summary Cards -->
<MudGrid Class="mb-4">
<MudItem xs="12" md="3">
<MudCard Elevation="2">
<MudCardContent>
<div class="d-flex justify-space-between align-center">
<div>
<MudText Typo="Typo.body2" Color="Color.Secondary">کل اعضا</MudText>
<MudText Typo="Typo.h4">@_totalMembers.ToString("N0")</MudText>
</div>
<MudIcon Icon="@Icons.Material.Filled.CardMembership" Color="Color.Primary" Size="Size.Large" />
</div>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" md="3">
<MudCard Elevation="2">
<MudCardContent>
<div class="d-flex justify-space-between align-center">
<div>
<MudText Typo="Typo.body2" Color="Color.Secondary">اعضای فعال</MudText>
<MudText Typo="Typo.h4">@_activeMembers.ToString("N0")</MudText>
<MudText Typo="Typo.caption" Color="Color.Success">@_activePercentage%</MudText>
</div>
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Large" />
</div>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" md="3">
<MudCard Elevation="2">
<MudCardContent>
<div class="d-flex justify-space-between align-center">
<div>
<MudText Typo="Typo.body2" Color="Color.Secondary">اعضای غیرفعال</MudText>
<MudText Typo="Typo.h4">@_inactiveMembers.ToString("N0")</MudText>
<MudText Typo="Typo.caption" Color="Color.Error">@_inactivePercentage%</MudText>
</div>
<MudIcon Icon="@Icons.Material.Filled.Cancel" Color="Color.Error" Size="Size.Large" />
</div>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" md="3">
<MudCard Elevation="2">
<MudCardContent>
<div class="d-flex justify-space-between align-center">
<div>
<MudText Typo="Typo.body2" Color="Color.Secondary">میانگین مدت عضویت</MudText>
<MudText Typo="Typo.h4">@_averageDuration.ToString("F0")</MudText>
<MudText Typo="Typo.caption" Color="Color.Info">روز</MudText>
</div>
<MudIcon Icon="@Icons.Material.Filled.Timeline" Color="Color.Info" Size="Size.Large" />
</div>
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
<!-- Charts -->
<MudGrid Class="mb-4">
<MudItem xs="12" md="6">
<MudCard>
<MudCardHeader>
<MudText Typo="Typo.h6">وضعیت عضویت‌ها</MudText>
</MudCardHeader>
<MudCardContent>
<MudChart ChartType="ChartType.Donut"
Width="300px"
Height="300px"
InputData="@_statusData"
InputLabels="@_statusLabels">
</MudChart>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" md="6">
<MudCard>
<MudCardHeader>
<MudText Typo="Typo.h6">روند عضویت‌ها (6 ماه اخیر)</MudText>
</MudCardHeader>
<MudCardContent>
<MudChart ChartType="ChartType.Line"
ChartSeries="@_membershipTrend"
XAxisLabels="@_trendLabels"
Width="100%"
Height="300px">
</MudChart>
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
<!-- Package Distribution -->
<MudCard Class="mb-4">
<MudCardHeader>
<MudText Typo="Typo.h6">توزیع پکیج‌ها</MudText>
</MudCardHeader>
<MudCardContent>
<MudChart ChartType="ChartType.Bar"
ChartSeries="@_packageSeries"
XAxisLabels="@_packageLabels"
Width="100%"
Height="300px">
</MudChart>
</MudCardContent>
</MudCard>
<!-- Recent Memberships -->
<MudCard>
<MudCardHeader>
<MudText Typo="Typo.h6">عضویت‌های اخیر (30 روز گذشته)</MudText>
</MudCardHeader>
<MudCardContent>
<MudTable T="RecentMembershipModel" Items="@_recentMemberships" Hover="true" Dense="true">
<HeaderContent>
<MudTh>تاریخ</MudTh>
<MudTh>شناسه کاربر</MudTh>
<MudTh>نام کاربر</MudTh>
<MudTh>پکیج</MudTh>
<MudTh>وضعیت</MudTh>
<MudTh>عملیات</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="تاریخ">@context.ActivatedAt.ToString("yyyy/MM/dd")</MudTd>
<MudTd DataLabel="شناسه کاربر">@context.UserId</MudTd>
<MudTd DataLabel="نام کاربر">@context.UserName</MudTd>
<MudTd DataLabel="پکیج">
<MudChip T="string" Color="Color.Primary" Size="Size.Small">@context.PackageName</MudChip>
</MudTd>
<MudTd DataLabel="وضعیت">
<MudChip T="string"
Color="@(context.IsActive ? Color.Success : Color.Error)"
Size="Size.Small">
@(context.IsActive ? "فعال" : "غیرفعال")
</MudChip>
</MudTd>
<MudTd DataLabel="عملیات">
<MudButton Size="Size.Small"
Variant="Variant.Text"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Visibility"
Href="/club/members">
مشاهده
</MudButton>
</MudTd>
</RowTemplate>
</MudTable>
</MudCardContent>
</MudCard>
}
</MudContainer>
@code {
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubClient { get; set; }
private bool _loading = false;
// Statistics
private int _totalMembers = 0;
private int _activeMembers = 0;
private int _inactiveMembers = 0;
private int _activePercentage = 0;
private int _inactivePercentage = 0;
private double _averageDuration = 0;
// Chart data
private double[] _statusData = Array.Empty<double>();
private string[] _statusLabels = Array.Empty<string>();
private List<ChartSeries> _membershipTrend = new();
private string[] _trendLabels = Array.Empty<string>();
private List<ChartSeries> _packageSeries = new();
private string[] _packageLabels = Array.Empty<string>();
private List<RecentMembershipModel> _recentMemberships = new();
protected override async Task OnInitializedAsync()
{
await LoadStatistics();
}
private async Task LoadStatistics()
{
_loading = true;
try
{
// TODO: Implement GetClubStatisticsQuery in CMS and BFF
// For now, generate mock data
GenerateMockStatistics();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری آمار: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
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
{
public DateTime ActivatedAt { get; set; }
public long UserId { get; set; }
public string UserName { get; set; }
public string PackageName { get; set; }
public bool IsActive { get; set; }
}
}

View File

@@ -0,0 +1,130 @@
@using BackOffice.BFF.Commission.Protobuf
<MudDialog>
<DialogContent>
@if (Payout != null)
{
<MudStack Spacing="3">
<MudPaper Class="pa-3" Elevation="0">
<MudText Typo="Typo.h6" Color="Color.Primary">اطلاعات کاربر</MudText>
<MudDivider Class="my-2" />
<MudSimpleTable Dense="true">
<tbody>
<tr>
<td><strong>شناسه کاربر:</strong></td>
<td>@Payout.UserId</td>
</tr>
<tr>
<td><strong>نام کاربر:</strong></td>
<td>@Payout.UserName</td>
</tr>
</tbody>
</MudSimpleTable>
</MudPaper>
<MudPaper Class="pa-3" Elevation="0">
<MudText Typo="Typo.h6" Color="Color.Primary">جزئیات Payout</MudText>
<MudDivider Class="my-2" />
<MudSimpleTable Dense="true">
<tbody>
<tr>
<td><strong>شناسه Payout:</strong></td>
<td>@Payout.Id</td>
</tr>
<tr>
<td><strong>شماره هفته:</strong></td>
<td>@Payout.WeekNumber</td>
</tr>
<tr>
<td><strong>تعداد بالانس:</strong></td>
<td>@Payout.BalancesEarned</td>
</tr>
<tr>
<td><strong>ارزش هر بالانس:</strong></td>
<td>@Payout.ValuePerBalance.ToString("N0") ریال</td>
</tr>
<tr>
<td><strong>مبلغ کل:</strong></td>
<td><strong>@Payout.TotalAmount.ToString("N0") ریال</strong></td>
</tr>
<tr>
<td><strong>وضعیت:</strong></td>
<td>
<MudChip T="string" Color="@GetStatusColor(Payout.Status)" Size="Size.Small">
@GetStatusText(Payout.Status)
</MudChip>
</td>
</tr>
</tbody>
</MudSimpleTable>
</MudPaper>
@if (Payout.WithdrawalMethod != null)
{
<MudPaper Class="pa-3" Elevation="0">
<MudText Typo="Typo.h6" Color="Color.Primary">اطلاعات برداشت</MudText>
<MudDivider Class="my-2" />
<MudSimpleTable Dense="true">
<tbody>
<tr>
<td><strong>روش برداشت:</strong></td>
<td>@(Payout.WithdrawalMethod.Value == 0 ? "نقدی" : "الماس")</td>
</tr>
@if (!string.IsNullOrEmpty(Payout.IbanNumber))
{
<tr>
<td><strong>شماره شبا:</strong></td>
<td dir="ltr">@Payout.IbanNumber</td>
</tr>
}
</tbody>
</MudSimpleTable>
</MudPaper>
}
<MudPaper Class="pa-3" Elevation="0">
<MudText Typo="Typo.h6" Color="Color.Primary">تاریخچه</MudText>
<MudDivider Class="my-2" />
<MudSimpleTable Dense="true">
<tbody>
<tr>
<td><strong>تاریخ ایجاد:</strong></td>
<td>@Payout.Created.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss")</td>
</tr>
<tr>
<td><strong>آخرین ویرایش:</strong></td>
<td>@Payout.LastModified.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss")</td>
</tr>
</tbody>
</MudSimpleTable>
</MudPaper>
</MudStack>
}
</DialogContent>
<DialogActions>
<MudButton Color="Color.Primary" OnClick="Close">بستن</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; }
[Parameter] public UserCommissionPayoutModel Payout { get; set; }
private void Close() => MudDialog.Close();
private Color GetStatusColor(int status) => status switch
{
0 => Color.Warning,
1 => Color.Success,
2 => Color.Error,
_ => Color.Default
};
private string GetStatusText(int status) => status switch
{
0 => "در انتظار",
1 => "پرداخت شده",
2 => "شکست خورده",
_ => "نامشخص"
};
}

View File

@@ -0,0 +1,174 @@
@page "/commission/dashboard"
@attribute [Authorize]
@using BackOffice.BFF.Commission.Protobuf
@using MudBlazor
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
<MudText Typo="Typo.h4" Class="mb-4">داشبورد کمیسیون</MudText>
@if (_isLoading)
{
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
}
else
{
<!-- Pool Summary Section -->
<MudGrid>
<MudItem xs="12" sm="6" md="3">
<MudCard Elevation="2">
<MudCardContent>
<MudText Typo="Typo.h6" Color="Color.Primary">Pool هفتگی</MudText>
<MudText Typo="Typo.h4">@(_poolData?.TotalPoolAmount.ToString("N0") ?? "0") ریال</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">هفته @(_currentWeekNumber)</MudText>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudCard Elevation="2">
<MudCardContent>
<MudText Typo="Typo.h6" Color="Color.Success">تعداد بالانس‌ها</MudText>
<MudText Typo="Typo.h4">@(_poolData?.TotalBalances.ToString("N0") ?? "0")</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">مجموع بالانس‌های فعال</MudText>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudCard Elevation="2">
<MudCardContent>
<MudText Typo="Typo.h6" Color="Color.Warning">ارزش هر بالانس</MudText>
<MudText Typo="Typo.h4">@(_poolData?.ValuePerBalance.ToString("N0") ?? "0") ریال</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">قیمت واحد بالانس</MudText>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudCard Elevation="2">
<MudCardContent>
<MudText Typo="Typo.h6" Color="@(_poolData?.IsCalculated == true ? Color.Success : Color.Error)">
وضعیت محاسبه
</MudText>
<MudText Typo="Typo.h4">
@(_poolData?.IsCalculated == true ? "محاسبه شده ✓" : "محاسبه نشده ✗")
</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">
@if (_poolData?.CalculatedAt != null)
{
@($"در تاریخ {_poolData.CalculatedAt.ToDateTime().ToLocalTime():yyyy/MM/dd}")
}
else
{
@("در انتظار محاسبه")
}
</MudText>
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
<!-- Week Selector -->
<MudPaper Class="pa-4 mt-4" Elevation="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
<MudText Typo="Typo.h6">انتخاب هفته:</MudText>
<MudTextField @bind-Value="_currentWeekNumber"
Label="شماره هفته"
Variant="Variant.Outlined"
Margin="Margin.Dense"
HelperText="فرمت: YYYY-Www (مثلاً 2025-W48)"
Style="max-width: 200px;" />
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="LoadPoolData"
StartIcon="@Icons.Material.Filled.Refresh">
بارگذاری
</MudButton>
</MudStack>
</MudPaper>
<!-- Pool Details -->
@if (_poolData != null)
{
<MudPaper Class="pa-4 mt-4" Elevation="2">
<MudText Typo="Typo.h6" Class="mb-3">جزئیات Pool</MudText>
<MudSimpleTable Hover="true" Dense="true">
<tbody>
<tr>
<td><strong>شناسه Pool:</strong></td>
<td>@_poolData.Id</td>
</tr>
<tr>
<td><strong>شماره هفته:</strong></td>
<td>@_poolData.WeekNumber</td>
</tr>
<tr>
<td><strong>مجموع Pool:</strong></td>
<td>@_poolData.TotalPoolAmount.ToString("N0") ریال</td>
</tr>
<tr>
<td><strong>تعداد بالانس‌ها:</strong></td>
<td>@_poolData.TotalBalances</td>
</tr>
<tr>
<td><strong>ارزش هر بالانس:</strong></td>
<td>@_poolData.ValuePerBalance.ToString("N0") ریال</td>
</tr>
<tr>
<td><strong>وضعیت:</strong></td>
<td>
<MudChip T="string" Color="@(_poolData.IsCalculated ? Color.Success : Color.Error)" Size="Size.Small">
@(_poolData.IsCalculated ? "محاسبه شده" : "محاسبه نشده")
</MudChip>
</td>
</tr>
<tr>
<td><strong>تاریخ محاسبه:</strong></td>
<td>
@if (_poolData.CalculatedAt != null)
{
@_poolData.CalculatedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm")
}
else
{
<MudText Color="Color.Secondary">-</MudText>
}
</td>
</tr>
<tr>
<td><strong>تاریخ ایجاد:</strong></td>
<td>@_poolData.Created.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm")</td>
</tr>
</tbody>
</MudSimpleTable>
</MudPaper>
}
<!-- Quick Actions -->
<MudPaper Class="pa-4 mt-4" Elevation="2">
<MudText Typo="Typo.h6" Class="mb-3">عملیات سریع</MudText>
<MudStack Row="true" Spacing="2">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Payments"
Href="/commission/payouts">
مشاهده Payout ها
</MudButton>
<MudButton Variant="Variant.Filled"
Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.AccountBalance"
Href="/commission/withdrawals">
درخواست‌های برداشت
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Info"
StartIcon="@Icons.Material.Filled.Calculate"
Disabled="@(_poolData?.IsCalculated == true)"
OnClick="TriggerCalculation">
اجرای محاسبه دستی
</MudButton>
</MudStack>
</MudPaper>
}
</MudContainer>

View File

@@ -0,0 +1,83 @@
using BackOffice.BFF.Commission.Protobuf;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using System.Globalization;
namespace BackOffice.Pages.Commission;
public partial class Dashboard
{
[Inject] public CommissionContract.CommissionContractClient CommissionContract { get; set; }
private bool _isLoading = true;
private string _currentWeekNumber = string.Empty;
private GetWeeklyCommissionPoolResponse? _poolData;
protected override async Task OnInitializedAsync()
{
// محاسبه شماره هفته جاری
_currentWeekNumber = GetCurrentWeekNumber();
await LoadPoolData();
}
private async Task LoadPoolData()
{
try
{
_isLoading = true;
StateHasChanged();
var request = new GetWeeklyCommissionPoolRequest
{
WeekNumber = _currentWeekNumber
};
_poolData = await CommissionContract.GetWeeklyCommissionPoolAsync(request);
Snackbar.Add($"اطلاعات Pool هفته {_currentWeekNumber} بارگذاری شد", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری اطلاعات: {ex.Message}", Severity.Error);
_poolData = null;
}
finally
{
_isLoading = false;
StateHasChanged();
}
}
private async Task TriggerCalculation()
{
var confirmed = await DialogService.ShowMessageBox(
"تایید",
$"آیا از اجرای محاسبه دستی برای هفته {_currentWeekNumber} مطمئن هستید؟",
yesText: "بله", cancelText: "خیر");
if (confirmed == true)
{
try
{
// TODO: Call CalculateWeeklyBalances, CalculateWeeklyCommissionPool, ProcessUserPayouts
Snackbar.Add("محاسبه با موفقیت آغاز شد. این عملیات ممکن است چند دقیقه طول بکشد.", Severity.Info);
// Reload data after a delay
await Task.Delay(2000);
await LoadPoolData();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در اجرای محاسبه: {ex.Message}", Severity.Error);
}
}
}
private string GetCurrentWeekNumber()
{
var today = DateTime.Now;
var calendar = CultureInfo.CurrentCulture.Calendar;
var weekOfYear = calendar.GetWeekOfYear(today, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
return $"{today.Year}-W{weekOfYear:D2}";
}
}

View File

@@ -0,0 +1,18 @@
namespace BackOffice.Pages.Commission;
public class WithdrawalRequestModel
{
public long Id { get; set; }
public long UserId { get; set; }
public string UserName { get; set; } = string.Empty;
public string PhoneNumber { get; set; } = string.Empty;
public long Amount { get; set; }
public int Status { get; set; } // 0=Pending, 1=Approved, 2=Rejected, 3=Processed
public string? BankAccount { get; set; }
public string? BankName { get; set; }
public string Method { get; set; } = "Bank"; // Bank, Crypto, etc.
public Google.Protobuf.WellKnownTypes.Timestamp RequestedAt { get; set; } = new();
public DateTime RequestDate { get; set; }
public DateTime? ProcessedDate { get; set; }
public string? AdminNote { get; set; }
}

View File

@@ -0,0 +1,133 @@
@page "/commission/payouts"
@attribute [Authorize]
@using BackOffice.BFF.Commission.Protobuf
@using Google.Protobuf.WellKnownTypes
@using MudBlazor
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
<MudText Typo="Typo.h4" Class="mb-4">Payout های کاربران</MudText>
<MudDataGrid T="UserCommissionPayoutModel"
ServerData="@(new Func<GridState<UserCommissionPayoutModel>, Task<GridData<UserCommissionPayoutModel>>>(ServerReload))"
Hover="true"
@ref="_gridData"
Height="75vh">
<ToolBarContent>
<MudText Typo="Typo.h6">لیست Payout ها</MudText>
<MudSpacer />
<MudStack Row="true" Spacing="2">
<MudTextField @bind-Value="_filterUserId"
Label="شناسه کاربر"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Clearable="true"
Style="max-width: 150px;" />
<MudTextField @bind-Value="_filterWeekNumber"
Label="شماره هفته"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Clearable="true"
Placeholder="2025-W48"
Style="max-width: 150px;" />
<MudSelect @bind-Value="_filterStatus"
Label="وضعیت"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Clearable="true"
Style="max-width: 150px;">
<MudSelectItem Value="@((int?)null)">همه</MudSelectItem>
<MudSelectItem Value="@((int?)0)">در انتظار</MudSelectItem>
<MudSelectItem Value="@((int?)1)">پرداخت شده</MudSelectItem>
<MudSelectItem Value="@((int?)2)">شکست خورده</MudSelectItem>
</MudSelect>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="ApplyFilter"
StartIcon="@Icons.Material.Filled.Search">
جستجو
</MudButton>
</MudStack>
</ToolBarContent>
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<PropertyColumn Property="x => x.UserId" Title="کاربر">
<CellTemplate>
<MudStack Spacing="0">
<MudText Typo="Typo.body2"><strong>ID:</strong> @context.Item.UserId</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">@context.Item.UserName</MudText>
</MudStack>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.WeekNumber" Title="هفته" />
<PropertyColumn Property="x => x.BalancesEarned" Title="بالانس‌ها">
<CellTemplate>
<MudChip T="string" Color="Color.Info" Size="Size.Small">
@context.Item.BalancesEarned بالانس
</MudChip>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.ValuePerBalance" Title="ارزش هر بالانس">
<CellTemplate>
@context.Item.ValuePerBalance.ToString("N0") ریال
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.TotalAmount" Title="مبلغ کل">
<CellTemplate>
<MudText Typo="Typo.body2" Color="Color.Primary">
<strong>@context.Item.TotalAmount.ToString("N0") ریال</strong>
</MudText>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.Status" Title="وضعیت">
<CellTemplate>
<MudChip T="string"
Color="@GetStatusColor(context.Item.Status)"
Size="Size.Small">
@GetStatusText(context.Item.Status)
</MudChip>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.Created" Title="تاریخ ایجاد">
<CellTemplate>
@context.Item.Created.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm")
</CellTemplate>
</PropertyColumn>
<TemplateColumn Title="عملیات" Sortable="false">
<CellTemplate>
<MudStack Row="true" Spacing="1">
<MudTooltip Text="مشاهده جزئیات">
<MudIconButton Icon="@Icons.Material.Filled.Visibility"
Size="Size.Small"
Color="Color.Info"
OnClick="@(() => ViewDetails(context.Item))" />
</MudTooltip>
@if (context.Item.Status == 0)
{
<MudTooltip Text="پردازش برداشت">
<MudIconButton Icon="@Icons.Material.Filled.Payment"
Size="Size.Small"
Color="Color.Success"
OnClick="@(() => ProcessWithdrawal(context.Item))" />
</MudTooltip>
}
</MudStack>
</CellTemplate>
</TemplateColumn>
</Columns>
<PagerContent>
<MudDataGridPager T="UserCommissionPayoutModel"
PageSizeOptions="@(new int[] { 10, 25, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد سطرهای صفحه" />
</PagerContent>
</MudDataGrid>
</MudContainer>

View File

@@ -0,0 +1,129 @@
using BackOffice.BFF.Commission.Protobuf;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using Google.Protobuf.WellKnownTypes;
using BackOffice.Pages.Commission.Components;
namespace BackOffice.Pages.Commission;
public partial class UserPayouts
{
[Inject] public CommissionContract.CommissionContractClient CommissionContract { get; set; }
private MudDataGrid<UserCommissionPayoutModel> _gridData;
private long? _filterUserId;
private string? _filterWeekNumber;
private int? _filterStatus;
private async Task<GridData<UserCommissionPayoutModel>> ServerReload(GridState<UserCommissionPayoutModel> state)
{
try
{
var request = new GetUserCommissionPayoutsRequest
{
PageIndex = state.Page + 1,
PageSize = state.PageSize
};
if (_filterUserId.HasValue)
{
request.UserId = _filterUserId.Value;
}
if (!string.IsNullOrEmpty(_filterWeekNumber))
{
request.WeekNumber = _filterWeekNumber;
}
if (_filterStatus.HasValue)
{
request.Status = _filterStatus.Value;
}
var result = await CommissionContract.GetUserCommissionPayoutsAsync(request);
if (result?.Models != null && result.Models.Any())
{
return new GridData<UserCommissionPayoutModel>
{
Items = result.Models.ToList(),
TotalItems = (int)result.MetaData.TotalCount
};
}
return new GridData<UserCommissionPayoutModel>();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری داده‌ها: {ex.Message}", Severity.Error);
return new GridData<UserCommissionPayoutModel>();
}
}
private async Task ApplyFilter()
{
if (_gridData != null)
{
await _gridData.ReloadServerData();
}
}
private Color GetStatusColor(int status)
{
return status switch
{
0 => Color.Warning, // Pending
1 => Color.Success, // Paid
2 => Color.Error, // Failed
_ => Color.Default
};
}
private string GetStatusText(int status)
{
return status switch
{
0 => "در انتظار",
1 => "پرداخت شده",
2 => "شکست خورده",
_ => "نامشخص"
};
}
private async Task ViewDetails(UserCommissionPayoutModel payout)
{
var parameters = new DialogParameters
{
["Payout"] = payout
};
await DialogService.ShowAsync<PayoutDetailsDialog>("جزئیات Payout", parameters, new DialogOptions
{
CloseButton = true,
MaxWidth = MaxWidth.Medium,
FullWidth = true
});
}
private async Task ProcessWithdrawal(UserCommissionPayoutModel payout)
{
var confirmed = await DialogService.ShowMessageBox(
"تایید پردازش برداشت",
$"آیا از پردازش برداشت برای کاربر {payout.UserName} (مبلغ: {payout.TotalAmount:N0} ریال) مطمئن هستید؟",
yesText: "تایید", cancelText: "لغو");
if (confirmed == true)
{
try
{
// TODO: Call ProcessWithdrawal API
Snackbar.Add("درخواست برداشت با موفقیت پردازش شد", Severity.Success);
await ApplyFilter();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در پردازش: {ex.Message}", Severity.Error);
}
}
}
}

View File

@@ -0,0 +1,272 @@
@page "/commission/reports"
@using MudBlazor
@using BackOffice.BFF.Commission.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>
<MudCard Class="mb-4">
<MudCardContent>
<MudGrid>
<MudItem xs="12" md="4">
<MudTextField @bind-Value="_filterFromWeek"
Label="از هفته"
Placeholder="2025-W01"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.DateRange" />
</MudItem>
<MudItem xs="12" md="4">
<MudTextField @bind-Value="_filterToWeek"
Label="تا هفته"
Placeholder="2025-W48"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.DateRange" />
</MudItem>
<MudItem xs="12" md="4" Class="d-flex align-center">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="ApplyFilter"
StartIcon="@Icons.Material.Filled.Search"
FullWidth="true">
جستجو
</MudButton>
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
<MudCard>
<MudCardContent>
@if (_loading)
{
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
}
else if (_reports == null || !_reports.Any())
{
<MudAlert Severity="Severity.Info">هیچ گزارشی یافت نشد</MudAlert>
}
else
{
<MudDataGrid T="WeeklyPoolReportModel"
Items="@_reports"
Hover="true"
Filterable="true"
SortMode="SortMode.Multiple">
<Columns>
<PropertyColumn Property="x => x.WeekNumber" Title="هفته" />
<PropertyColumn Property="x => x.TotalPoolAmount" Title="مبلغ کل استخر" Format="N0">
<CellTemplate>
<MudText>@context.Item.TotalPoolAmount.ToString("N0") ریال</MudText>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.TotalBalances" Title="مجموع موجودی‌ها" />
<PropertyColumn Property="x => x.ValuePerBalance" Title="ارزش هر موجودی" Format="N0">
<CellTemplate>
<MudText>@context.Item.ValuePerBalance.ToString("N0") ریال</MudText>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.IsCalculated" Title="وضعیت">
<CellTemplate>
<MudChip T="string"
Color="@(context.Item.IsCalculated ? Color.Success : Color.Warning)"
Size="Size.Small">
@(context.Item.IsCalculated ? "محاسبه شده" : "در انتظار")
</MudChip>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.CalculatedAt" Title="تاریخ محاسبه">
<CellTemplate>
@if (context.Item.CalculatedAt != null)
{
<MudText>@context.Item.CalculatedAt.ToDateTime().ToString("yyyy/MM/dd HH:mm")</MudText>
}
else
{
<MudText Color="Color.Secondary">-</MudText>
}
</CellTemplate>
</PropertyColumn>
<TemplateColumn Title="عملیات">
<CellTemplate>
<MudButton Size="Size.Small"
Variant="Variant.Text"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Visibility"
OnClick="@(() => ViewDetails(context.Item))">
جزئیات
</MudButton>
<MudButton Size="Size.Small"
Variant="Variant.Text"
Color="Color.Info"
StartIcon="@Icons.Material.Filled.People"
Href="@($"/commission/payouts?week={context.Item.WeekNumber}")">
پرداخت‌ها
</MudButton>
</CellTemplate>
</TemplateColumn>
</Columns>
</MudDataGrid>
}
</MudCardContent>
</MudCard>
<MudCard Class="mt-4">
<MudCardContent>
<MudGrid>
<MudItem xs="12" md="3">
<MudPaper Elevation="0" Class="pa-4 text-center">
<MudText Typo="Typo.h6" Color="Color.Primary">مجموع استخرها</MudText>
<MudText Typo="Typo.h4">@_totalPoolSum.ToString("N0")</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">ریال</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" md="3">
<MudPaper Elevation="0" Class="pa-4 text-center">
<MudText Typo="Typo.h6" Color="Color.Success">محاسبه شده</MudText>
<MudText Typo="Typo.h4">@_calculatedCount</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">هفته</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" md="3">
<MudPaper Elevation="0" Class="pa-4 text-center">
<MudText Typo="Typo.h6" Color="Color.Warning">در انتظار</MudText>
<MudText Typo="Typo.h4">@_pendingCount</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">هفته</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" md="3">
<MudPaper Elevation="0" Class="pa-4 text-center">
<MudText Typo="Typo.h6" Color="Color.Info">میانگین ارزش</MudText>
<MudText Typo="Typo.h4">@_averageValue.ToString("N0")</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">ریال</MudText>
</MudPaper>
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
</MudContainer>
@code {
[Inject] public CommissionContract.CommissionContractClient CommissionClient { get; set; }
private List<WeeklyPoolReportModel> _reports = new();
private bool _loading = false;
private string _filterFromWeek = "";
private string _filterToWeek = "";
// Statistics
private long _totalPoolSum = 0;
private int _calculatedCount = 0;
private int _pendingCount = 0;
private long _averageValue = 0;
protected override async Task OnInitializedAsync()
{
await LoadData();
}
private async Task LoadData()
{
_loading = true;
try
{
var request = new GetAllWeeklyPoolsRequest
{
PageIndex = 1,
PageSize = 100
};
if (!string.IsNullOrWhiteSpace(_filterFromWeek))
{
request.FromWeek = _filterFromWeek;
}
if (!string.IsNullOrWhiteSpace(_filterToWeek))
{
request.ToWeek = _filterToWeek;
}
var response = await CommissionClient.GetAllWeeklyPoolsAsync(request);
_reports = response.Models.Select(x => new WeeklyPoolReportModel
{
WeekNumber = x.WeekNumber,
TotalPoolAmount = x.TotalPoolAmount,
TotalBalances = x.TotalBalances,
ValuePerBalance = x.ValuePerBalance,
IsCalculated = x.IsCalculated,
CalculatedAt = x.CalculatedAt
}).ToList();
CalculateStatistics();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری داده‌ها: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
private async Task ApplyFilter()
{
await LoadData();
}
private void ViewDetails(WeeklyPoolReportModel report)
{
Navigation.NavigateTo($"/commission/dashboard?week={report.WeekNumber}");
}
private void CalculateStatistics()
{
if (_reports == null || !_reports.Any()) return;
_totalPoolSum = _reports.Sum(r => r.TotalPoolAmount);
_calculatedCount = _reports.Count(r => r.IsCalculated);
_pendingCount = _reports.Count(r => !r.IsCalculated);
_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
{
public string WeekNumber { get; set; }
public long TotalPoolAmount { get; set; }
public int TotalBalances { get; set; }
public long ValuePerBalance { get; set; }
public bool IsCalculated { get; set; }
public Timestamp CalculatedAt { get; set; }
}
}

View File

@@ -0,0 +1,125 @@
@page "/commission/withdrawals"
@attribute [Authorize]
@using BackOffice.BFF.Commission.Protobuf
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
<MudText Typo="Typo.h4" Class="mb-4">درخواست‌های برداشت</MudText>
<MudDataGrid T="WithdrawalRequestModel"
ServerData="@(new Func<GridState<WithdrawalRequestModel>, Task<GridData<WithdrawalRequestModel>>>(ServerReload))"
Hover="true"
@ref="_gridData"
Height="75vh">
<ToolBarContent>
<MudText Typo="Typo.h6">درخواست‌های برداشت</MudText>
<MudSpacer />
<MudStack Row="true" Spacing="2">
<MudSelect @bind-Value="_filterStatus"
Label="وضعیت"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Style="max-width: 150px;">
<MudSelectItem Value="@((int?)null)">همه</MudSelectItem>
<MudSelectItem Value="@((int?)0)">در انتظار</MudSelectItem>
<MudSelectItem Value="@((int?)1)">تایید شده</MudSelectItem>
<MudSelectItem Value="@((int?)2)">رد شده</MudSelectItem>
<MudSelectItem Value="@((int?)3)">پردازش شده</MudSelectItem>
</MudSelect>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="ApplyFilter"
StartIcon="@Icons.Material.Filled.Search">
جستجو
</MudButton>
</MudStack>
</ToolBarContent>
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<PropertyColumn Property="x => x.UserId" Title="کاربر">
<CellTemplate>
<MudStack Spacing="0">
<MudText Typo="Typo.body2"><strong>@context.Item.UserName</strong></MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">ID: @context.Item.UserId</MudText>
</MudStack>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.Amount" Title="مبلغ">
<CellTemplate>
<MudText Typo="Typo.body2" Color="Color.Primary">
<strong>@context.Item.Amount.ToString("N0") ریال</strong>
</MudText>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.Method" Title="روش برداشت">
<CellTemplate>
<MudChip T="string" Color="Color.Info" Size="Size.Small">
@GetMethodText(context.Item.Method)
</MudChip>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.Status" Title="وضعیت">
<CellTemplate>
<MudChip T="string"
Color="@GetStatusColor(context.Item.Status)"
Size="Size.Small">
@GetStatusText(context.Item.Status)
</MudChip>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.RequestedAt" Title="تاریخ درخواست">
<CellTemplate>
@context.Item.RequestedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm")
</CellTemplate>
</PropertyColumn>
<TemplateColumn Title="عملیات" Sortable="false">
<CellTemplate>
<MudStack Row="true" Spacing="1">
<MudTooltip Text="مشاهده جزئیات">
<MudIconButton Icon="@Icons.Material.Filled.Visibility"
Size="Size.Small"
Color="Color.Info"
OnClick="@(() => ViewDetails(context.Item))" />
</MudTooltip>
@if (context.Item.Status == 0)
{
<MudTooltip Text="تایید">
<MudIconButton Icon="@Icons.Material.Filled.Check"
Size="Size.Small"
Color="Color.Success"
OnClick="@(() => ApproveRequest(context.Item))" />
</MudTooltip>
<MudTooltip Text="رد">
<MudIconButton Icon="@Icons.Material.Filled.Close"
Size="Size.Small"
Color="Color.Error"
OnClick="@(() => RejectRequest(context.Item))" />
</MudTooltip>
}
@if (context.Item.Status == 1)
{
<MudTooltip Text="پردازش">
<MudIconButton Icon="@Icons.Material.Filled.Payment"
Size="Size.Small"
Color="Color.Primary"
OnClick="@(() => ProcessRequest(context.Item))" />
</MudTooltip>
}
</MudStack>
</CellTemplate>
</TemplateColumn>
</Columns>
<PagerContent>
<MudDataGridPager T="WithdrawalRequestModel"
PageSizeOptions="@(new int[] { 10, 25, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد سطرهای صفحه" />
</PagerContent>
</MudDataGrid>
</MudContainer>

View File

@@ -0,0 +1,170 @@
using BackOffice.BFF.Commission.Protobuf;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace BackOffice.Pages.Commission;
public partial class WithdrawalRequests
{
[Inject] public CommissionContract.CommissionContractClient CommissionContract { get; set; }
private MudDataGrid<WithdrawalRequestModel> _gridData;
private int? _filterStatus;
private async Task<GridData<WithdrawalRequestModel>> ServerReload(GridState<WithdrawalRequestModel> state)
{
try
{
// TODO: Implement GetWithdrawalRequestsRequest in CMS Protobuf
/*
var request = new GetWithdrawalRequestsRequest
{
PageIndex = state.Page + 1,
PageSize = state.PageSize
};
if (_filterStatus.HasValue)
{
request.Status = _filterStatus.Value;
}
var result = await CommissionContract.GetWithdrawalRequestsAsync(request);
*/
// Mock data until API is ready
await Task.CompletedTask;
var result = new { Models = new List<WithdrawalRequestModel>(), TotalCount = 0 };
if (result?.Models != null && result.Models.Any())
{
return new GridData<WithdrawalRequestModel>
{
Items = result.Models.ToList(),
TotalItems = result.TotalCount
};
}
return new GridData<WithdrawalRequestModel>();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری داده‌ها: {ex.Message}", Severity.Error);
return new GridData<WithdrawalRequestModel>();
}
}
private async Task ApplyFilter()
{
if (_gridData != null)
{
await _gridData.ReloadServerData();
}
}
private Color GetStatusColor(int status)
{
return status switch
{
0 => Color.Warning, // Pending
1 => Color.Success, // Approved
2 => Color.Error, // Rejected
3 => Color.Info, // Processed
_ => Color.Default
};
}
private string GetStatusText(int status)
{
return status switch
{
0 => "در انتظار",
1 => "تایید شده",
2 => "رد شده",
3 => "پردازش شده",
_ => "نامشخص"
};
}
private string GetMethodText(string method)
{
return method switch
{
"Bank" => "انتقال بانکی",
"Crypto" => "ارز دیجیتال",
"Cash" => "نقدی",
_ => "نامشخص"
};
}
private void ViewDetails(WithdrawalRequestModel request)
{
Snackbar.Add($"جزئیات درخواست {request.Id}", Severity.Info);
// TODO: Open details dialog
}
private async Task ApproveRequest(WithdrawalRequestModel request)
{
var confirmed = await DialogService.ShowMessageBox(
"تایید درخواست",
$"آیا از تایید درخواست برداشت {request.Amount:N0} ریال برای {request.UserName} مطمئن هستید؟",
yesText: "تایید", cancelText: "لغو");
if (confirmed == true)
{
try
{
// TODO: Call ApproveWithdrawal API
Snackbar.Add("درخواست با موفقیت تایید شد", Severity.Success);
await ApplyFilter();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در تایید: {ex.Message}", Severity.Error);
}
}
}
private async Task RejectRequest(WithdrawalRequestModel request)
{
var confirmed = await DialogService.ShowMessageBox(
"رد درخواست",
$"آیا از رد درخواست برداشت {request.Amount:N0} ریال برای {request.UserName} مطمئن هستید؟",
yesText: "رد", cancelText: "لغو");
if (confirmed == true)
{
try
{
// TODO: Call RejectWithdrawal API
Snackbar.Add("درخواست رد شد", Severity.Warning);
await ApplyFilter();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در رد درخواست: {ex.Message}", Severity.Error);
}
}
}
private async Task ProcessRequest(WithdrawalRequestModel request)
{
var confirmed = await DialogService.ShowMessageBox(
"پردازش پرداخت",
$"آیا پرداخت {request.Amount:N0} ریال به {request.UserName} انجام شده است؟",
yesText: "بله، پردازش شد", cancelText: "لغو");
if (confirmed == true)
{
try
{
// TODO: Call ProcessWithdrawal API
Snackbar.Add("درخواست با موفقیت پردازش شد", Severity.Success);
await ApplyFilter();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در پردازش: {ex.Message}", Severity.Error);
}
}
}
}

View File

@@ -0,0 +1,248 @@
@page "/network/balances"
@using MudBlazor
@using BackOffice.BFF.NetworkMembership.Protobuf
<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>
<MudCard Class="mb-4">
<MudCardContent>
<MudGrid>
<MudItem xs="12" md="3">
<MudNumericField @bind-Value="_filterUserId"
Label="شناسه کاربر"
Variant="Variant.Outlined"
Min="0" />
</MudItem>
<MudItem xs="12" md="3">
<MudTextField @bind-Value="_filterWeekNumber"
Label="شماره هفته"
Placeholder="2025-W48"
Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="12" md="2">
<MudNumericField @bind-Value="_minBalance"
Label="حداقل موجودی"
Variant="Variant.Outlined"
Min="0" />
</MudItem>
<MudItem xs="12" md="2">
<MudNumericField @bind-Value="_maxBalance"
Label="حداکثر موجودی"
Variant="Variant.Outlined"
Min="0" />
</MudItem>
<MudItem xs="12" md="2" Class="d-flex align-center">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="ApplyFilter"
StartIcon="@Icons.Material.Filled.Search"
FullWidth="true">
جستجو
</MudButton>
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
<MudCard>
<MudCardContent>
<div class="d-flex justify-space-between align-center mb-3">
<MudText Typo="Typo.h6">موجودی‌های هفتگی</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Success"
StartIcon="@Icons.Material.Filled.Download"
OnClick="ExportToExcel">
خروجی Excel
</MudButton>
</div>
<MudDataGrid T="UserWeeklyBalanceModel"
ServerData="@(new Func<GridState<UserWeeklyBalanceModel>, Task<GridData<UserWeeklyBalanceModel>>>(ServerReload))"
Filterable="true"
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
</MudChip>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.RightBalance" Title="موجودی راست">
<CellTemplate>
<MudChip T="string" Color="Color.Warning" Size="Size.Small">
@context.Item.RightBalance
</MudChip>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.MatchedBalance" Title="موجودی تطبیق‌یافته">
<CellTemplate>
<MudChip T="string" Color="Color.Info" Size="Size.Small">
@context.Item.MatchedBalance
</MudChip>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.CarryOverLeft" Title="نقل‌شده چپ" />
<PropertyColumn Property="x => x.CarryOverRight" Title="نقل‌شده راست" />
<TemplateColumn Title="عملیات">
<CellTemplate>
<MudButton Size="Size.Small"
Variant="Variant.Text"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Person"
Href="@($"/network/user-info/{context.Item.UserId}")">
مشاهده کاربر
</MudButton>
</CellTemplate>
</TemplateColumn>
</Columns>
<PagerContent>
<MudDataGridPager T="UserWeeklyBalanceModel"
PageSizeOptions="new int[] { 10, 25, 50, 100 }" />
</PagerContent>
</MudDataGrid>
</MudCardContent>
</MudCard>
<MudCard Class="mt-4">
<MudCardContent>
<MudGrid>
<MudItem xs="12" md="4">
<MudPaper Elevation="0" Class="pa-4 text-center">
<MudText Typo="Typo.h6" Color="Color.Success">مجموع موجودی چپ</MudText>
<MudText Typo="Typo.h4">@_totalLeftBalance.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" md="4">
<MudPaper Elevation="0" Class="pa-4 text-center">
<MudText Typo="Typo.h6" Color="Color.Warning">مجموع موجودی راست</MudText>
<MudText Typo="Typo.h4">@_totalRightBalance.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" md="4">
<MudPaper Elevation="0" Class="pa-4 text-center">
<MudText Typo="Typo.h6" Color="Color.Info">مجموع تطبیق‌یافته</MudText>
<MudText Typo="Typo.h4">@_totalMatchedBalance.ToString("N0")</MudText>
</MudPaper>
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
</MudContainer>
@code {
[Inject] public NetworkMembershipContract.NetworkMembershipContractClient NetworkClient { get; set; }
private long? _filterUserId = null;
private string _filterWeekNumber = "";
private int? _minBalance = null;
private int? _maxBalance = null;
private int _totalLeftBalance = 0;
private int _totalRightBalance = 0;
private int _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 ?? ""
};
if (_filterUserId.HasValue && _filterUserId.Value > 0)
{
request.UserId = _filterUserId.Value;
}
var response = await NetworkClient.GetUserWeeklyBalancesAsync(request);
items = response.Balances.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
}).ToList();
// Apply balance range filter if specified
if (_minBalance.HasValue || _maxBalance.HasValue)
{
items = items.Where(b =>
(!_minBalance.HasValue || b.MatchedBalance >= _minBalance.Value) &&
(!_maxBalance.HasValue || b.MatchedBalance <= _maxBalance.Value)
).ToList();
}
*/
CalculateTotals(items);
return new GridData<UserWeeklyBalanceModel>
{
Items = items,
TotalItems = 0
};
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری داده‌ها: {ex.Message}", Severity.Error);
return new GridData<UserWeeklyBalanceModel> { Items = new List<UserWeeklyBalanceModel>(), TotalItems = 0 };
}
}
private async Task ApplyFilter()
{
StateHasChanged();
}
private void CalculateTotals(List<UserWeeklyBalanceModel> items)
{
_totalLeftBalance = items.Sum(b => b.LeftBalance);
_totalRightBalance = items.Sum(b => b.RightBalance);
_totalMatchedBalance = items.Sum(b => b.MatchedBalance);
}
private async Task ExportToExcel()
{
var confirmed = await DialogService.ShowMessageBox(
"خروجی Excel",
"این ویژگی به زودی اضافه خواهد شد.",
yesText: "باشه");
// TODO: Implement Excel export using 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; }
}
}

View File

@@ -0,0 +1,155 @@
@page "/network/tree"
@attribute [Authorize]
@using BackOffice.BFF.NetworkMembership.Protobuf
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
<MudText Typo="Typo.h4" Class="mb-4">درخت شبکه</MudText>
<MudPaper Class="pa-4 mb-4">
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center">
<MudNumericField @bind-Value="_searchUserId"
Label="شناسه کاربر"
Variant="Variant.Outlined"
Style="max-width: 200px;" />
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="LoadTree"
StartIcon="@Icons.Material.Filled.Search"
Disabled="_isLoading">
نمایش درخت
</MudButton>
<MudSpacer />
<MudStack Row="true" Spacing="2">
<MudChip T="string" Color="Color.Info" Size="Size.Small">
کل اعضا: @_totalMembers
</MudChip>
<MudChip T="string" Color="Color.Success" Size="Size.Small">
زیرمجموعه چپ: @_leftCount
</MudChip>
<MudChip T="string" Color="Color.Warning" Size="Size.Small">
زیرمجموعه راست: @_rightCount
</MudChip>
</MudStack>
</MudStack>
</MudPaper>
@if (_isLoading)
{
<MudPaper Class="pa-8 d-flex justify-center">
<MudProgressCircular Color="Color.Primary" Size="Size.Large" Indeterminate="true" />
</MudPaper>
}
else if (_treeData != null && _treeData.Nodes.Any())
{
<MudPaper Class="pa-4">
<MudDataGrid T="NetworkTreeNodeModel" Items="@_treeData.Nodes" Hover="true" Filterable="true">
<Columns>
<PropertyColumn Property="x => x.UserId" Title="شناسه کاربر" />
<PropertyColumn Property="x => x.UserName" Title="نام کاربر" />
<PropertyColumn Property="x => x.NetworkLeg" Title="موقعیت">
<CellTemplate>
<MudChip T="string"
Color="@(context.Item.NetworkLeg == 0 ? Color.Success : Color.Warning)"
Size="Size.Small">
@(context.Item.NetworkLeg == 0 ? "چپ" : "راست")
</MudChip>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.NetworkLevel" Title="سطح" />
<PropertyColumn Property="x => x.IsActive" Title="وضعیت">
<CellTemplate>
<MudChip T="string"
Color="@(context.Item.IsActive ? Color.Success : Color.Error)"
Size="Size.Small">
@(context.Item.IsActive ? "فعال" : "غیرفعال")
</MudChip>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.JoinedAt" Title="تاریخ عضویت">
<CellTemplate>
@context.Item.JoinedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd")
</CellTemplate>
</PropertyColumn>
<TemplateColumn Title="عملیات" Sortable="false">
<CellTemplate>
<MudButton Size="Size.Small"
Variant="Variant.Text"
Color="Color.Primary"
OnClick="@(() => ViewUserDetails(context.Item.UserId))">
جزئیات
</MudButton>
</CellTemplate>
</TemplateColumn>
</Columns>
</MudDataGrid>
</MudPaper>
}
else
{
<MudPaper Class="pa-8">
<MudAlert Severity="Severity.Info">
برای نمایش درخت شبکه، شناسه کاربر را وارد کنید و دکمه "نمایش درخت" را بزنید.
</MudAlert>
</MudPaper>
}
</MudContainer>
@code {
[Inject] public NetworkMembershipContract.NetworkMembershipContractClient NetworkContract { get; set; }
[Inject] public NavigationManager NavigationManager { get; set; }
private long _searchUserId;
private GetNetworkTreeResponse _treeData;
private bool _isLoading;
private int _totalMembers;
private int _leftCount;
private int _rightCount;
private async Task LoadTree()
{
if (_searchUserId <= 0)
{
Snackbar.Add("لطفاً شناسه کاربر را وارد کنید", Severity.Warning);
return;
}
_isLoading = true;
try
{
var request = new GetNetworkTreeRequest { RootUserId = _searchUserId };
_treeData = await NetworkContract.GetNetworkTreeAsync(request);
CalculateStats();
Snackbar.Add($"درخت بارگذاری شد - {_treeData.Nodes.Count} عضو", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری درخت: {ex.Message}", Severity.Error);
}
finally
{
_isLoading = false;
}
}
private void CalculateStats()
{
if (_treeData == null || !_treeData.Nodes.Any()) return;
_totalMembers = _treeData.Nodes.Count;
_leftCount = _treeData.Nodes.Count(n => n.NetworkLeg == 0);
_rightCount = _treeData.Nodes.Count(n => n.NetworkLeg == 1);
}
private void ViewUserDetails(long userId)
{
NavigationManager.NavigateTo($"/network/user-info/{userId}");
}
}

View File

@@ -0,0 +1,292 @@
@page "/network/statistics"
@using MudBlazor
@using BackOffice.BFF.NetworkMembership.Protobuf
<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
{
<!-- Summary Cards -->
<MudGrid Class="mb-4">
<MudItem xs="12" md="3">
<MudCard Elevation="2">
<MudCardContent>
<div class="d-flex justify-space-between align-center">
<div>
<MudText Typo="Typo.body2" Color="Color.Secondary">کل اعضا</MudText>
<MudText Typo="Typo.h4">@_totalMembers.ToString("N0")</MudText>
</div>
<MudIcon Icon="@Icons.Material.Filled.People" Color="Color.Primary" Size="Size.Large" />
</div>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" md="3">
<MudCard Elevation="2">
<MudCardContent>
<div class="d-flex justify-space-between align-center">
<div>
<MudText Typo="Typo.body2" Color="Color.Secondary">شاخه چپ</MudText>
<MudText Typo="Typo.h4">@_leftCount.ToString("N0")</MudText>
<MudText Typo="Typo.caption" Color="Color.Success">@_leftPercentage%</MudText>
</div>
<MudIcon Icon="@Icons.Material.Filled.TrendingUp" Color="Color.Success" Size="Size.Large" />
</div>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" md="3">
<MudCard Elevation="2">
<MudCardContent>
<div class="d-flex justify-space-between align-center">
<div>
<MudText Typo="Typo.body2" Color="Color.Secondary">شاخه راست</MudText>
<MudText Typo="Typo.h4">@_rightCount.ToString("N0")</MudText>
<MudText Typo="Typo.caption" Color="Color.Warning">@_rightPercentage%</MudText>
</div>
<MudIcon Icon="@Icons.Material.Filled.TrendingDown" Color="Color.Warning" Size="Size.Large" />
</div>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" md="3">
<MudCard Elevation="2">
<MudCardContent>
<div class="d-flex justify-space-between align-center">
<div>
<MudText Typo="Typo.body2" Color="Color.Secondary">میانگین عمق</MudText>
<MudText Typo="Typo.h4">@_averageDepth.ToString("F1")</MudText>
<MudText Typo="Typo.caption" Color="Color.Info">سطح</MudText>
</div>
<MudIcon Icon="@Icons.Material.Filled.Height" Color="Color.Info" Size="Size.Large" />
</div>
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
<!-- Distribution Chart -->
<MudGrid Class="mb-4">
<MudItem xs="12" md="6">
<MudCard>
<MudCardHeader>
<MudText Typo="Typo.h6">توزیع شاخه‌ها</MudText>
</MudCardHeader>
<MudCardContent>
<MudChart ChartType="ChartType.Donut"
Width="300px"
Height="300px"
InputData="@_distributionData"
InputLabels="@_distributionLabels">
</MudChart>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" md="6">
<MudCard>
<MudCardHeader>
<MudText Typo="Typo.h6">رشد ماهانه</MudText>
</MudCardHeader>
<MudCardContent>
<MudChart ChartType="ChartType.Line"
ChartSeries="@_growthSeries"
XAxisLabels="@_growthLabels"
Width="100%"
Height="300px">
</MudChart>
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
<!-- Depth Distribution -->
<MudCard Class="mb-4">
<MudCardHeader>
<MudText Typo="Typo.h6">توزیع عمق شبکه</MudText>
</MudCardHeader>
<MudCardContent>
<MudChart ChartType="ChartType.Bar"
ChartSeries="@_depthSeries"
XAxisLabels="@_depthLabels"
Width="100%"
Height="300px">
</MudChart>
</MudCardContent>
</MudCard>
<!-- Top 10 Users -->
<MudCard>
<MudCardHeader>
<MudText Typo="Typo.h6">10 کاربر برتر (بیشترین زیرمجموعه)</MudText>
</MudCardHeader>
<MudCardContent>
<MudTable T="TopUserModel" Items="@_topUsers" Hover="true" Dense="true">
<HeaderContent>
<MudTh>رتبه</MudTh>
<MudTh>شناسه کاربر</MudTh>
<MudTh>نام کاربر</MudTh>
<MudTh>تعداد زیرمجموعه</MudTh>
<MudTh>شاخه چپ</MudTh>
<MudTh>شاخه راست</MudTh>
<MudTh>عملیات</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="رتبه">
<MudChip T="string" Color="@GetRankColor(context.Rank)" Size="Size.Small">@context.Rank</MudChip>
</MudTd>
<MudTd DataLabel="شناسه کاربر">@context.UserId</MudTd>
<MudTd DataLabel="نام کاربر">@context.UserName</MudTd>
<MudTd DataLabel="تعداد زیرمجموعه">@context.TotalChildren.ToString("N0")</MudTd>
<MudTd DataLabel="شاخه چپ">@context.LeftCount.ToString("N0")</MudTd>
<MudTd DataLabel="شاخه راست">@context.RightCount.ToString("N0")</MudTd>
<MudTd DataLabel="عملیات">
<MudButton Size="Size.Small"
Variant="Variant.Text"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Visibility"
Href="@($"/network/user-info/{context.UserId}")">
مشاهده
</MudButton>
</MudTd>
</RowTemplate>
</MudTable>
</MudCardContent>
</MudCard>
}
</MudContainer>
@code {
[Inject] public NetworkMembershipContract.NetworkMembershipContractClient NetworkClient { get; set; }
private bool _loading = false;
// Statistics
private int _totalMembers = 0;
private int _leftCount = 0;
private int _rightCount = 0;
private int _leftPercentage = 0;
private int _rightPercentage = 0;
private double _averageDepth = 0;
// Chart data
private double[] _distributionData = Array.Empty<double>();
private string[] _distributionLabels = Array.Empty<string>();
private List<ChartSeries> _growthSeries = new();
private string[] _growthLabels = Array.Empty<string>();
private List<ChartSeries> _depthSeries = new();
private string[] _depthLabels = Array.Empty<string>();
private List<TopUserModel> _topUsers = new();
protected override async Task OnInitializedAsync()
{
await LoadStatistics();
}
private async Task LoadStatistics()
{
_loading = true;
try
{
// TODO: Implement GetNetworkStatisticsQuery in CMS and BFF
// For now, generate mock data
GenerateMockStatistics();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری آمار: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
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)
{
return rank switch
{
1 => Color.Warning,
2 => Color.Default,
3 => Color.Tertiary,
_ => Color.Primary
};
}
private class TopUserModel
{
public int Rank { get; set; }
public long UserId { get; set; }
public string UserName { get; set; }
public int TotalChildren { get; set; }
public int LeftCount { get; set; }
public int RightCount { get; set; }
}
}

View File

@@ -0,0 +1,225 @@
@page "/network/user-info/{UserId:long}"
@attribute [Authorize]
@using BackOffice.BFF.NetworkMembership.Protobuf
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
<MudBreadcrumbs Items="_breadcrumbs" Class="mb-4"></MudBreadcrumbs>
@if (_isLoading)
{
<MudPaper Class="pa-8 d-flex justify-center">
<MudProgressCircular Color="Color.Primary" Size="Size.Large" Indeterminate="true" />
</MudPaper>
}
else if (_userInfo != null)
{
<MudGrid>
<MudItem xs="12" md="6">
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">اطلاعات کاربر</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudSimpleTable Dense="true">
<tbody>
<tr>
<td><strong>شناسه:</strong></td>
<td>@_userInfo.UserId</td>
</tr>
<tr>
<td><strong>نام کاربر:</strong></td>
<td>@_userInfo.UserName</td>
</tr>
<tr>
<td><strong>موقعیت:</strong></td>
<td>
<MudChip T="string"
Color="@(_userInfo.NetworkLeg == 0 ? Color.Success : Color.Warning)"
Size="Size.Small">
@(_userInfo.NetworkLeg == 0 ? "چپ" : "راست")
</MudChip>
</td>
</tr>
<tr>
<td><strong>عمق در درخت:</strong></td>
<td>@_userInfo.NetworkLevel</td>
</tr>
<tr>
<td><strong>تاریخ عضویت:</strong></td>
<td>@_userInfo.JoinedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm")</td>
</tr>
</tbody>
</MudSimpleTable>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" md="6">
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">ساختار شبکه</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudSimpleTable Dense="true">
<tbody>
@if (_userInfo.ParentId.HasValue && _userInfo.ParentId.Value > 0)
{
<tr>
<td><strong>والد:</strong></td>
<td>
<MudButton Size="Size.Small"
Variant="Variant.Text"
Color="Color.Primary"
OnClick="@(() => NavigateToUser(_userInfo.ParentId))">
@_userInfo.ParentName (ID: @_userInfo.ParentId)
</MudButton>
</td>
</tr>
}
<tr>
<td><strong>زیرمجموعه چپ:</strong></td>
<td>
@if (_userInfo.LeftChildId > 0)
{
<MudButton Size="Size.Small"
Variant="Variant.Text"
Color="Color.Success"
OnClick="@(() => NavigateToUser(_userInfo.LeftChildId))">
@_userInfo.LeftChildName (ID: @_userInfo.LeftChildId)
</MudButton>
}
else
{
<MudText Color="Color.Secondary">خالی</MudText>
}
</td>
</tr>
<tr>
<td><strong>زیرمجموعه راست:</strong></td>
<td>
@if (_userInfo.RightChildId > 0)
{
<MudButton Size="Size.Small"
Variant="Variant.Text"
Color="Color.Warning"
OnClick="@(() => NavigateToUser(_userInfo.RightChildId))">
@_userInfo.RightChildName (ID: @_userInfo.RightChildId)
</MudButton>
}
else
{
<MudText Color="Color.Secondary">خالی</MudText>
}
</td>
</tr>
</tbody>
</MudSimpleTable>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12">
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">آمار شبکه</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudAlert Severity="Severity.Info">
<MudText>آمار تجمعی شبکه در نسخه بعدی اضافه خواهد شد</MudText>
</MudAlert>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12">
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">عملیات</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudStack Row="true" Spacing="2">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.AccountTree"
OnClick="@(() => NavigationManager.NavigateTo($"/network/tree?userId={UserId}"))">
نمایش درخت کامل
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Info"
StartIcon="@Icons.Material.Filled.History"
OnClick="ViewHistory">
تاریخچه تغییرات
</MudButton>
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
}
else
{
<MudAlert Severity="Severity.Error">
کاربر یافت نشد یا خطایی رخ داده است.
</MudAlert>
}
</MudContainer>
@code {
[Parameter] public long UserId { get; set; }
[Inject] public NetworkMembershipContract.NetworkMembershipContractClient NetworkContract { get; set; }
[Inject] public NavigationManager NavigationManager { get; set; }
private GetUserNetworkResponse _userInfo;
private bool _isLoading = true;
private List<BreadcrumbItem> _breadcrumbs = new();
protected override async Task OnParametersSetAsync()
{
await LoadUserInfo();
}
private async Task LoadUserInfo()
{
_isLoading = true;
try
{
var request = new GetUserNetworkRequest { UserId = UserId };
_userInfo = await NetworkContract.GetUserNetworkAsync(request);
_breadcrumbs = new List<BreadcrumbItem>
{
new BreadcrumbItem("شبکه", href: "/network/tree"),
new BreadcrumbItem($"کاربر: {_userInfo.UserName}", href: null, disabled: true)
};
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری اطلاعات: {ex.Message}", Severity.Error);
}
finally
{
_isLoading = false;
}
}
private void NavigateToUser(long? parentIdValue)
{
if (parentIdValue.HasValue)
NavigationManager.NavigateTo($"/network/user-info/{parentIdValue.Value}");
}
private async Task ViewHistory()
{
// TODO: Implement history view
Snackbar.Add("این قابلیت به زودی اضافه خواهد شد", Severity.Info);
}
}

View File

@@ -0,0 +1,358 @@
@page "/system/worker-control"
@using MudBlazor
@using BackOffice.Pages.SystemManagement
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
<MudText Typo="Typo.h4" GutterBottom="true">کنترل Worker محاسبات</MudText>
<MudText Typo="Typo.body1" Color="Color.Secondary" Class="mb-4">
مدیریت و نظارت بر Worker محاسبات هفتگی کمیسیون
</MudText>
<MudGrid>
<!-- Worker Status -->
<MudItem xs="12" md="6">
<MudCard Elevation="2">
<MudCardHeader>
<MudText Typo="Typo.h6">وضعیت Worker</MudText>
</MudCardHeader>
<MudCardContent>
<MudSimpleTable Dense="true">
<tbody>
<tr>
<td><strong>وضعیت:</strong></td>
<td>
<MudChip T="string"
Color="@(_workerStatus == WorkerStatus.Running ? Color.Success : Color.Error)"
Size="Size.Small">
@GetWorkerStatusText()
</MudChip>
</td>
</tr>
<tr>
<td><strong>آخرین اجرا:</strong></td>
<td>@_lastRunTime.ToString("yyyy/MM/dd HH:mm:ss")</td>
</tr>
<tr>
<td><strong>اجرای بعدی:</strong></td>
<td>@_nextRunTime.ToString("yyyy/MM/dd HH:mm:ss")</td>
</tr>
<tr>
<td><strong>تعداد اجراهای موفق:</strong></td>
<td><MudChip T="string" Color="Color.Success" Size="Size.Small">@_successfulRuns</MudChip></td>
</tr>
<tr>
<td><strong>تعداد خطاها:</strong></td>
<td><MudChip T="string" Color="Color.Error" Size="Size.Small">@_failedRuns</MudChip></td>
</tr>
</tbody>
</MudSimpleTable>
</MudCardContent>
</MudCard>
</MudItem>
<!-- Control Panel -->
<MudItem xs="12" md="6">
<MudCard Elevation="2">
<MudCardHeader>
<MudText Typo="Typo.h6">پنل کنترل</MudText>
</MudCardHeader>
<MudCardContent>
<MudStack Spacing="3">
<MudTextField @bind-Value="_manualWeekNumber"
Label="شماره هفته برای اجرای دستی"
Placeholder="2025-W48"
Variant="Variant.Outlined" />
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
FullWidth="true"
StartIcon="@Icons.Material.Filled.PlayArrow"
OnClick="RunManualCalculation"
Disabled="@_isProcessing">
اجرای دستی محاسبات
</MudButton>
<MudDivider />
@if (_workerStatus == WorkerStatus.Running)
{
<MudButton Variant="Variant.Filled"
Color="Color.Warning"
FullWidth="true"
StartIcon="@Icons.Material.Filled.Pause"
OnClick="PauseWorker"
Disabled="@_isProcessing">
توقف موقت Worker
</MudButton>
}
else if (_workerStatus == WorkerStatus.Paused)
{
<MudButton Variant="Variant.Filled"
Color="Color.Success"
FullWidth="true"
StartIcon="@Icons.Material.Filled.PlayArrow"
OnClick="ResumeWorker"
Disabled="@_isProcessing">
ازسرگیری Worker
</MudButton>
}
<MudButton Variant="Variant.Outlined"
Color="Color.Info"
FullWidth="true"
StartIcon="@Icons.Material.Filled.Refresh"
OnClick="RestartWorker"
Disabled="@_isProcessing">
راه‌اندازی مجدد Worker
</MudButton>
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
<!-- Execution Log -->
<MudItem xs="12">
<MudCard Elevation="2">
<MudCardHeader>
<div class="d-flex justify-space-between align-center w-100">
<MudText Typo="Typo.h6">تاریخچه اجرا</MudText>
<MudButton Size="Size.Small"
Variant="Variant.Text"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Refresh"
OnClick="RefreshLog">
بروزرسانی
</MudButton>
</div>
</MudCardHeader>
<MudCardContent>
@if (_executionLog == null || !_executionLog.Any())
{
<MudAlert Severity="Severity.Info">هیچ رکوردی یافت نشد</MudAlert>
}
else
{
<MudTable T="ExecutionLogModel" Items="@_executionLog" Dense="true" Hover="true" FixedHeader="true" Height="400px">
<HeaderContent>
<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.WeekNumber</MudTd>
<MudTd DataLabel="وضعیت">
<MudChip T="string"
Color="@(context.IsSuccess ? Color.Success : Color.Error)"
Size="Size.Small">
@(context.IsSuccess ? "موفق" : "خطا")
</MudChip>
</MudTd>
<MudTd DataLabel="مدت زمان">@context.DurationSeconds ثانیه</MudTd>
<MudTd DataLabel="پیام">
<MudText Typo="Typo.body2" Color="@(context.IsSuccess ? Color.Default : Color.Error)">
@context.Message
</MudText>
</MudTd>
</RowTemplate>
</MudTable>
}
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
@if (_isProcessing)
{
<MudOverlay Visible="true" DarkBackground="true">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large" />
</MudOverlay>
}
</MudContainer>
@code {
private WorkerStatus _workerStatus = WorkerStatus.Running;
private DateTime _lastRunTime = DateTime.Now.AddHours(-2);
private DateTime _nextRunTime = DateTime.Now.AddDays(1);
private int _successfulRuns = 45;
private int _failedRuns = 2;
private string _manualWeekNumber = "";
private bool _isProcessing = false;
private List<ExecutionLogModel> _executionLog = new();
protected override async Task OnInitializedAsync()
{
// TODO: Load actual worker status from API
GenerateMockLog();
}
private async Task RunManualCalculation()
{
if (string.IsNullOrWhiteSpace(_manualWeekNumber))
{
Snackbar.Add("لطفا شماره هفته را وارد کنید", Severity.Warning);
return;
}
var confirmed = await DialogService.ShowMessageBox(
"اجرای دستی محاسبات",
$"آیا از اجرای محاسبات برای هفته {_manualWeekNumber} اطمینان دارید؟",
yesText: "بله، اجرا شود", cancelText: "لغو");
if (confirmed == true)
{
_isProcessing = true;
try
{
// TODO: Call TriggerWeeklyCalculationCommand API
await Task.Delay(2000); // Simulate API call
Snackbar.Add($"محاسبات هفته {_manualWeekNumber} با موفقیت آغاز شد", Severity.Success);
_manualWeekNumber = "";
await RefreshLog();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در اجرای محاسبات: {ex.Message}", Severity.Error);
}
finally
{
_isProcessing = false;
}
}
}
private async Task PauseWorker()
{
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;
}
}
}
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;
}
}
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;
}
}
}
private async Task RefreshLog()
{
try
{
// TODO: Load actual execution log from API
GenerateMockLog();
Snackbar.Add("تاریخچه بروزرسانی شد", Severity.Info);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری تاریخچه: {ex.Message}", Severity.Error);
}
}
private string GetWorkerStatusText()
{
return _workerStatus switch
{
WorkerStatus.Running => "در حال اجرا",
WorkerStatus.Paused => "متوقف",
WorkerStatus.Stopped => "خاموش",
_ => "نامشخص"
};
}
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,
Paused,
Stopped
}
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; }
}
}