feat: Add network tree visualization and Persian date service
All checks were successful
Build and Deploy / build (push) Successful in 2m39s
All checks were successful
Build and Deploy / build (push) Successful in 2m39s
This commit is contained in:
@@ -116,13 +116,13 @@
|
||||
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.Category.Protobuf" Version="0.0.4" />
|
||||
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.ClubMembership.Protobuf" Version="0.0.3" />
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.ClubMembership.Protobuf" Version="0.0.6" />
|
||||
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.Commission.Protobuf" Version="0.0.3" />
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.Commission.Protobuf" Version="0.0.7" />
|
||||
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.Common.Protobuf" Version="0.0.2" />
|
||||
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.Configuration.Protobuf" Version="1.0.3" />
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.Configuration.Protobuf" Version="1.0.6" />
|
||||
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.DiscountCategory.Protobuf" Version="0.0.2" />
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.ManualPayment.Protobuf" Version="0.0.2" />
|
||||
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.NetworkMembership.Protobuf" Version="0.0.2" />
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.NetworkMembership.Protobuf" Version="0.0.7" />
|
||||
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.Otp.Protobuf" Version="0.0.112" />
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.Tag.Protobuf" Version="0.0.2" />
|
||||
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.User.Protobuf" Version="0.0.112" />
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.User.Protobuf" Version="0.0.113" />
|
||||
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.UserRole.Protobuf" Version="0.0.112" />
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.UserAddress.Protobuf" Version="0.0.112" />
|
||||
|
||||
@@ -7,8 +7,11 @@ 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 Foursat.BackOffice.BFF.Commission.Protos;
|
||||
using Foursat.BackOffice.BFF.ClubMembership.Protos;
|
||||
using Foursat.BackOffice.BFF.Configuration.Protos;
|
||||
using Foursat.BackOffice.BFF.NetworkMembership.Protos;
|
||||
using Foursat.BackOffice.BFF.Health.Protobuf;
|
||||
|
||||
// TODO: Create these proto projects - temporarily disabled
|
||||
// using BackOffice.BFF.DiscountProduct.Protobuf.Protos.DiscountProduct;
|
||||
@@ -32,9 +35,9 @@ using Microsoft.AspNetCore.Components.Authorization;
|
||||
using MudBlazor.Services;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Foursat.BackOffice.BFF.ClubMembership.Protobuf;
|
||||
using Foursat.BackOffice.BFF.Configuration.Protobuf;
|
||||
|
||||
using Foursat.BackOffice.BFF.Health.Protobuf;
|
||||
using Foursat.BackOffice.BFF.NetworkMembership.Protos;
|
||||
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
@@ -60,6 +63,9 @@ public static class ConfigureServices
|
||||
services.AddMudServices();
|
||||
services.AddGrpcServices(configuration);
|
||||
|
||||
// Persian DateTime Service
|
||||
services.AddSingleton<BackOffice.Services.IPersianDateTimeService, BackOffice.Services.PersianDateTimeService>();
|
||||
|
||||
// Application Services
|
||||
services.AddScoped<BackOffice.Services.Authorization.IAuthorizationService, BackOffice.Services.Authorization.AuthorizationService>();
|
||||
// TODO: Re-enable when proto projects are created
|
||||
|
||||
15
src/BackOffice/Pages/AutoComplete/UserAutoComplete.razor
Normal file
15
src/BackOffice/Pages/AutoComplete/UserAutoComplete.razor
Normal file
@@ -0,0 +1,15 @@
|
||||
@using BackOffice.BFF.User.Protobuf.Protos.User
|
||||
|
||||
<MudAutocomplete T="GetAllUserByFilterResponseModel"
|
||||
Label="@Label"
|
||||
Value="@_item"
|
||||
DebounceInterval="500"
|
||||
ValueChanged="@((e) => OnSelected(e))"
|
||||
ToStringFunc="@(e=> e == null ? null : $"{e.FirstName} {e.LastName} ({e.Mobile})")"
|
||||
SearchFunc="@Search"
|
||||
Variant="Variant.Outlined"
|
||||
Clearable="true"
|
||||
ShowProgressIndicator="true"
|
||||
CoerceText="false"
|
||||
ResetValueOnEmptyText="true"
|
||||
OnClearButtonClick="()=> OnSelected(null)"/>
|
||||
89
src/BackOffice/Pages/AutoComplete/UserAutoComplete.razor.cs
Normal file
89
src/BackOffice/Pages/AutoComplete/UserAutoComplete.razor.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using BackOffice.BFF.User.Protobuf.Protos.User;
|
||||
using Grpc.Core;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace BackOffice.Pages.AutoComplete;
|
||||
|
||||
public partial class UserAutoComplete
|
||||
{
|
||||
[Inject] public UserContract.UserContractClient UserContract { get; set; }
|
||||
|
||||
[Parameter] public string Label { get; set; } = "انتخاب کاربر";
|
||||
[Parameter] public long? SelectedUserId { get; set; }
|
||||
[Parameter] public EventCallback<long?> SelectedUserIdChanged { get; set; }
|
||||
|
||||
private GetAllUserByFilterResponseModel _item;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (SelectedUserId.HasValue && SelectedUserId.Value > 0 && _item?.Id != SelectedUserId.Value)
|
||||
{
|
||||
await LoadUser(SelectedUserId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<GetAllUserByFilterResponseModel>> Search(string value, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value) || value.Length < 2)
|
||||
return new List<GetAllUserByFilterResponseModel>();
|
||||
|
||||
try
|
||||
{
|
||||
var request = new GetAllUserByFilterRequest
|
||||
{
|
||||
PaginationState = new() { PageNumber = 1, PageSize = 100 },
|
||||
Filter = new()
|
||||
{
|
||||
|
||||
SearchText = value,
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
var response = await UserContract.GetAllUserByFilterAsync(request, cancellationToken: cancellationToken);
|
||||
return response.Models?.ToList() ?? new List<GetAllUserByFilterResponseModel>();
|
||||
}
|
||||
catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled)
|
||||
{
|
||||
return new List<GetAllUserByFilterResponseModel>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new List<GetAllUserByFilterResponseModel>();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnSelected(GetAllUserByFilterResponseModel selected)
|
||||
{
|
||||
_item = selected;
|
||||
var userId = selected?.Id;
|
||||
SelectedUserId = userId;
|
||||
await SelectedUserIdChanged.InvokeAsync(userId);
|
||||
}
|
||||
|
||||
private async Task LoadUser(long userId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new GetAllUserByFilterRequest
|
||||
{
|
||||
PaginationState = new() { PageNumber = 1, PageSize = 1 },
|
||||
Filter = new()
|
||||
{
|
||||
Id = userId
|
||||
}
|
||||
};
|
||||
|
||||
var response = await UserContract.GetAllUserByFilterAsync(request);
|
||||
|
||||
if (response.Models != null && response.Models.Count > 0)
|
||||
{
|
||||
_item = response.Models[0];
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Silent fail
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
@using BackOffice.Pages.Club.Components
|
||||
@using Foursat.BackOffice.BFF.ClubMembership.Protobuf
|
||||
@using Foursat.BackOffice.BFF.ClubMembership.Protos
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<MudText Typo="Typo.h4" Class="mb-4">مدیریت اعضای باشگاه</MudText>
|
||||
|
||||
@@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using BackOffice.Pages.Club.Components;
|
||||
using Foursat.BackOffice.BFF.ClubMembership.Protobuf;
|
||||
using Foursat.BackOffice.BFF.ClubMembership.Protos;
|
||||
|
||||
namespace BackOffice.Pages.Club;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@using Foursat.BackOffice.BFF.ClubMembership.Protobuf
|
||||
@using Foursat.BackOffice.BFF.ClubMembership.Protos
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@using Foursat.BackOffice.BFF.ClubMembership.Protobuf
|
||||
@using Foursat.BackOffice.BFF.ClubMembership.Protos
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@using Foursat.BackOffice.BFF.ClubMembership.Protobuf
|
||||
@using Foursat.BackOffice.BFF.ClubMembership.Protos
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@page "/club/statistics"
|
||||
|
||||
@using MudBlazor
|
||||
@using Foursat.BackOffice.BFF.ClubMembership.Protobuf
|
||||
@using Foursat.BackOffice.BFF.ClubMembership.Protos
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">آمار باشگاه</MudText>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@using BackOffice.BFF.Commission.Protobuf
|
||||
@using Foursat.BackOffice.BFF.Commission.Protos
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@page "/commission/dashboard"
|
||||
@attribute [Authorize]
|
||||
|
||||
@using BackOffice.BFF.Commission.Protobuf
|
||||
@using Foursat.BackOffice.BFF.Commission.Protos
|
||||
@using MudBlazor
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
@@ -18,9 +18,9 @@
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudCard Elevation="2">
|
||||
<MudCardContent>
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary">Pool هفتگی</MudText>
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary">استخر هفتگی</MudText>
|
||||
<MudText Typo="Typo.h4">@(_poolData?.TotalPoolAmount.ToString("N0") ?? "0") ریال</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">هفته @(_currentWeekNumber)</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">هفته @(_currentWeekNumberPersian)</MudText>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
@@ -28,9 +28,9 @@
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudCard Elevation="2">
|
||||
<MudCardContent>
|
||||
<MudText Typo="Typo.h6" Color="Color.Success">تعداد بالانسها</MudText>
|
||||
<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>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">مجموع تعادل های فعال</MudText>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
@@ -38,9 +38,9 @@
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudCard Elevation="2">
|
||||
<MudCardContent>
|
||||
<MudText Typo="Typo.h6" Color="Color.Warning">ارزش هر بالانس</MudText>
|
||||
<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>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">قیمت واحد تعادل</MudText>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
@@ -57,7 +57,9 @@
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">
|
||||
@if (_poolData?.CalculatedAt != null)
|
||||
{
|
||||
@($"در تاریخ {_poolData.CalculatedAt.ToDateTime().ToLocalTime():yyyy/MM/dd}")
|
||||
var calculatedDate = _poolData.CalculatedAt.ToDateTime().ToLocalTime();
|
||||
var persianDate = PersianDateTime.ConvertToPersianDateTime(calculatedDate);
|
||||
@($"در تاریخ {persianDate}")
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -73,12 +75,90 @@
|
||||
<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;" />
|
||||
|
||||
@if (_allWeeks.Any())
|
||||
{
|
||||
<MudSelect T="string"
|
||||
Value="_currentWeekNumber"
|
||||
Label="شماره هفته"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
AnchorOrigin="Origin.BottomCenter"
|
||||
Style="min-width: 400px;"
|
||||
ValueChanged="OnWeekChanged">
|
||||
|
||||
@if (_availableWeeks?.CurrentWeek != null)
|
||||
{
|
||||
<MudSelectItem Value="@_availableWeeks.CurrentWeek.WeekNumber">
|
||||
<MudText>
|
||||
<MudIcon Icon="@Icons.Material.Filled.Today" Size="Size.Small" Class="ml-2" />
|
||||
@_availableWeeks.CurrentWeek.DisplayText
|
||||
</MudText>
|
||||
</MudSelectItem>
|
||||
<MudDivider />
|
||||
}
|
||||
|
||||
@if (_availableWeeks?.CalculatedWeeks?.Any() == true)
|
||||
{
|
||||
<MudListSubheader>هفتههای محاسبه شده</MudListSubheader>
|
||||
@foreach (var week in _availableWeeks.CalculatedWeeks)
|
||||
{
|
||||
<MudSelectItem Value="@week.WeekNumber">
|
||||
<MudText>
|
||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" Class="ml-2" />
|
||||
@week.DisplayText
|
||||
@if (week.TotalPoolAmount > 0)
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Success" Class="mr-2">
|
||||
@week.TotalPoolAmount.ToString("N0") ریال
|
||||
</MudChip>
|
||||
}
|
||||
</MudText>
|
||||
</MudSelectItem>
|
||||
}
|
||||
<MudDivider />
|
||||
}
|
||||
|
||||
@if (_availableWeeks?.PendingWeeks?.Any() == true)
|
||||
{
|
||||
<MudListSubheader>هفتههای محاسبه نشده</MudListSubheader>
|
||||
@foreach (var week in _availableWeeks.PendingWeeks)
|
||||
{
|
||||
<MudSelectItem Value="@week.WeekNumber">
|
||||
<MudText>
|
||||
<MudIcon Icon="@Icons.Material.Filled.AccessTime" Color="Color.Warning" Size="Size.Small" Class="ml-2" />
|
||||
@week.DisplayText
|
||||
</MudText>
|
||||
</MudSelectItem>
|
||||
}
|
||||
<MudDivider />
|
||||
}
|
||||
|
||||
@if (_availableWeeks?.FutureWeeks?.Any() == true)
|
||||
{
|
||||
<MudListSubheader>هفتههای آینده</MudListSubheader>
|
||||
@foreach (var week in _availableWeeks.FutureWeeks)
|
||||
{
|
||||
<MudSelectItem Value="@week.WeekNumber">
|
||||
<MudText>
|
||||
<MudIcon Icon="@Icons.Material.Filled.CalendarMonth" Color="Color.Info" Size="Size.Small" Class="ml-2" />
|
||||
@week.DisplayText
|
||||
</MudText>
|
||||
</MudSelectItem>
|
||||
}
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
else
|
||||
{
|
||||
<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"
|
||||
@@ -92,11 +172,11 @@
|
||||
@if (_poolData != null)
|
||||
{
|
||||
<MudPaper Class="pa-4 mt-4" Elevation="2">
|
||||
<MudText Typo="Typo.h6" Class="mb-3">جزئیات Pool</MudText>
|
||||
<MudText Typo="Typo.h6" Class="mb-3">جزئیات استخر</MudText>
|
||||
<MudSimpleTable Hover="true" Dense="true">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>شناسه Pool:</strong></td>
|
||||
<td><strong>شناسه استخر:</strong></td>
|
||||
<td>@_poolData.Id</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -104,15 +184,15 @@
|
||||
<td>@_poolData.WeekNumber</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>مجموع Pool:</strong></td>
|
||||
<td><strong>مجموع استخر:</strong></td>
|
||||
<td>@_poolData.TotalPoolAmount.ToString("N0") ریال</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>تعداد بالانسها:</strong></td>
|
||||
<td><strong>تعداد تعادل:</strong></td>
|
||||
<td>@_poolData.TotalBalances</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>ارزش هر بالانس:</strong></td>
|
||||
<td><strong>ارزش هر تعادل:</strong></td>
|
||||
<td>@_poolData.ValuePerBalance.ToString("N0") ریال</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -1,22 +1,71 @@
|
||||
using BackOffice.BFF.Commission.Protobuf;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using Foursat.BackOffice.BFF.Commission.Protos;
|
||||
using BackOffice.Services;
|
||||
|
||||
namespace BackOffice.Pages.Commission;
|
||||
|
||||
public partial class Dashboard
|
||||
{
|
||||
[Inject] public CommissionContract.CommissionContractClient CommissionContract { get; set; }
|
||||
[Inject] public IPersianDateTimeService PersianDateTime { get; set; }
|
||||
|
||||
private bool _isLoading = true;
|
||||
private string _currentWeekNumber = string.Empty;
|
||||
private string _currentWeekNumberPersian = string.Empty; // هفته به شمسی
|
||||
private GetWeeklyCommissionPoolResponse? _poolData;
|
||||
|
||||
// Available weeks data
|
||||
private Foursat.BackOffice.BFF.Commission.Protos.GetAvailableWeeksResponse? _availableWeeks;
|
||||
private List<Foursat.BackOffice.BFF.Commission.Protos.WeekInfo> _allWeeks = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// محاسبه شماره هفته جاری
|
||||
// بارگذاری لیست هفتههای قابل انتخاب
|
||||
await LoadAvailableWeeks();
|
||||
|
||||
// محاسبه شماره هفته جاری (میلادی برای API)
|
||||
_currentWeekNumber = GetCurrentWeekNumber();
|
||||
// تبدیل به شمسی برای نمایش
|
||||
_currentWeekNumberPersian = PersianDateTime.ConvertWeekNumberToPersian(_currentWeekNumber);
|
||||
|
||||
await LoadPoolData();
|
||||
}
|
||||
|
||||
private async Task LoadAvailableWeeks()
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new GetAvailableWeeksRequest
|
||||
{
|
||||
FutureWeeksCount = 4,
|
||||
PastWeeksCount = 12
|
||||
};
|
||||
|
||||
_availableWeeks = await CommissionContract.GetAvailableWeeksAsync(request);
|
||||
|
||||
// ترکیب همه هفتهها برای dropdown
|
||||
_allWeeks.Clear();
|
||||
if (_availableWeeks.CurrentWeek != null)
|
||||
_allWeeks.Add(_availableWeeks.CurrentWeek);
|
||||
_allWeeks.AddRange(_availableWeeks.CalculatedWeeks);
|
||||
_allWeeks.AddRange(_availableWeeks.PendingWeeks);
|
||||
_allWeeks.AddRange(_availableWeeks.FutureWeeks);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری لیست هفتهها: {ex.Message}", Severity.Warning);
|
||||
// Fallback to current week
|
||||
_currentWeekNumber = GetCurrentWeekNumber();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnWeekChanged(string weekNumber)
|
||||
{
|
||||
_currentWeekNumber = weekNumber;
|
||||
_currentWeekNumberPersian = PersianDateTime.ConvertWeekNumberToPersian(weekNumber);
|
||||
await LoadPoolData();
|
||||
}
|
||||
|
||||
@@ -33,7 +82,7 @@ public partial class Dashboard
|
||||
};
|
||||
|
||||
_poolData = await CommissionContract.GetWeeklyCommissionPoolAsync(request);
|
||||
|
||||
Console.WriteLine(JsonSerializer.Serialize(_poolData));
|
||||
Snackbar.Add($"اطلاعات Pool هفته {_currentWeekNumber} بارگذاری شد", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -59,9 +108,16 @@ public partial class Dashboard
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO: Call CalculateWeeklyBalances, CalculateWeeklyCommissionPool, ProcessUserPayouts
|
||||
await CommissionContract.TriggerWeeklyCalculationAsync(new TriggerWeeklyCalculationRequest()
|
||||
{
|
||||
WeekNumber = _currentWeekNumber,
|
||||
ForceRecalculate = true,
|
||||
SkipBalances = false,
|
||||
SkipPayouts = false,
|
||||
SkipPool = false,
|
||||
});
|
||||
Snackbar.Add("محاسبه با موفقیت آغاز شد. این عملیات ممکن است چند دقیقه طول بکشد.", Severity.Info);
|
||||
|
||||
|
||||
// Reload data after a delay
|
||||
await Task.Delay(2000);
|
||||
await LoadPoolData();
|
||||
@@ -77,7 +133,7 @@ public partial class Dashboard
|
||||
{
|
||||
var today = DateTime.Now;
|
||||
var calendar = CultureInfo.CurrentCulture.Calendar;
|
||||
var weekOfYear = calendar.GetWeekOfYear(today, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
|
||||
var weekOfYear = calendar.GetWeekOfYear(today, CalendarWeekRule.FirstDay, DayOfWeek.Saturday);
|
||||
return $"{today.Year}-W{weekOfYear:D2}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
@page "/commission/payouts"
|
||||
@attribute [Authorize]
|
||||
|
||||
@using BackOffice.BFF.Commission.Protobuf
|
||||
@using Foursat.BackOffice.BFF.Commission.Protos
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
@using MudBlazor
|
||||
|
||||
@@ -61,7 +61,14 @@
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.WeekNumber" Title="هفته" />
|
||||
<PropertyColumn Property="x => x.WeekNumber" Title="هفته">
|
||||
<CellTemplate>
|
||||
@{
|
||||
var persianWeek = PersianDateTime.ConvertWeekNumberToPersian(context.Item.WeekNumber);
|
||||
}
|
||||
<MudText Typo="Typo.body2">@persianWeek</MudText>
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.BalancesEarned" Title="بالانسها">
|
||||
<CellTemplate>
|
||||
@@ -97,7 +104,10 @@
|
||||
|
||||
<PropertyColumn Property="x => x.Created" Title="تاریخ ایجاد">
|
||||
<CellTemplate>
|
||||
@context.Item.Created.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm")
|
||||
@{
|
||||
var persianDate = PersianDateTime.ConvertToPersianDateTime(context.Item.Created.ToDateTime().ToLocalTime());
|
||||
}
|
||||
@persianDate
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
using BackOffice.BFF.Commission.Protobuf;
|
||||
using Foursat.BackOffice.BFF.Commission.Protos;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using BackOffice.Pages.Commission.Components;
|
||||
using BackOffice.Services;
|
||||
|
||||
namespace BackOffice.Pages.Commission;
|
||||
|
||||
public partial class UserPayouts
|
||||
{
|
||||
[Inject] public CommissionContract.CommissionContractClient CommissionContract { get; set; }
|
||||
[Inject] public IPersianDateTimeService PersianDateTime { get; set; }
|
||||
|
||||
private MudDataGrid<UserCommissionPayoutModel> _gridData;
|
||||
private long? _filterUserId;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@page "/commission/reports"
|
||||
|
||||
@using MudBlazor
|
||||
@using BackOffice.BFF.Commission.Protobuf
|
||||
@using Foursat.BackOffice.BFF.Commission.Protos
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@attribute [Authorize]
|
||||
|
||||
@using MudBlazor
|
||||
@using BackOffice.BFF.Commission.Protobuf
|
||||
@using Foursat.BackOffice.BFF.Commission.Protos
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
@using Microsoft.JSInterop
|
||||
@using System.Text
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@page "/commission/withdrawals"
|
||||
@attribute [Authorize]
|
||||
|
||||
@using BackOffice.BFF.Commission.Protobuf
|
||||
@using Foursat.BackOffice.BFF.Commission.Protos
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<MudText Typo="Typo.h4" Class="mb-4">درخواستهای برداشت</MudText>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using BackOffice.BFF.Commission.Protobuf;
|
||||
using Foursat.BackOffice.BFF.Commission.Protos;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using GrpcWithdrawalRequestModel = BackOffice.BFF.Commission.Protobuf.WithdrawalRequestModel;
|
||||
using GrpcGetWithdrawalRequestsRequest = BackOffice.BFF.Commission.Protobuf.GetWithdrawalRequestsRequest;
|
||||
using GrpcWithdrawalRequestModel = Foursat.BackOffice.BFF.Commission.Protos.WithdrawalRequestModel;
|
||||
using GrpcGetWithdrawalRequestsRequest = Foursat.BackOffice.BFF.Commission.Protos.GetWithdrawalRequestsRequest;
|
||||
|
||||
namespace BackOffice.Pages.Commission;
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
@page "/dashboard/overview"
|
||||
@attribute [Authorize]
|
||||
|
||||
@using BackOffice.BFF.Commission.Protobuf
|
||||
@using BackOffice.BFF.NetworkMembership.Protobuf
|
||||
@using Foursat.BackOffice.BFF.ClubMembership.Protobuf
|
||||
@using Foursat.BackOffice.BFF.Commission.Protos
|
||||
@using Foursat.BackOffice.BFF.ClubMembership.Protos
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
@@ -301,7 +300,7 @@
|
||||
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);
|
||||
var weekNum = cal.GetWeekOfYear(now, System.Globalization.CalendarWeekRule.FirstDay, DayOfWeek.Saturday);
|
||||
return $"{now.Year}-W{weekNum:D2}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
@page "/network/balances"
|
||||
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
@using MudBlazor
|
||||
@using BackOffice.BFF.Commission.Protobuf
|
||||
@using Foursat.BackOffice.BFF.Commission.Protos
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">گزارش موجودیهای هفتگی</MudText>
|
||||
@@ -13,10 +14,12 @@
|
||||
<MudCardContent>
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="3">
|
||||
<MudNumericField @bind-Value="_filterUserId"
|
||||
Label="شناسه کاربر"
|
||||
Variant="Variant.Outlined"
|
||||
Min="0" />
|
||||
@* <MudNumericField @bind-Value="_filterUserId" *@
|
||||
@* Label="شناسه کاربر" *@
|
||||
@* Variant="Variant.Outlined" *@
|
||||
@* Min="0" /> *@
|
||||
<UserAutoComplete Label="جستجوی کاربر"
|
||||
@bind-SelectedUserId="_filterUserId" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="3">
|
||||
<MudTextField @bind-Value="_filterWeekNumber"
|
||||
@@ -61,7 +64,7 @@
|
||||
</MudButton>
|
||||
</div>
|
||||
|
||||
<MudDataGrid T="UserWeeklyBalanceModel"
|
||||
<MudDataGrid T="UserWeeklyBalanceModel" @ref="_grid"
|
||||
ServerData="@(new Func<GridState<UserWeeklyBalanceModel>, Task<GridData<UserWeeklyBalanceModel>>>(ServerReload))"
|
||||
Filterable="true"
|
||||
Hover="true">
|
||||
@@ -149,7 +152,7 @@
|
||||
|
||||
@code {
|
||||
[Inject] public CommissionContract.CommissionContractClient CommissionClient { get; set; }
|
||||
|
||||
private MudDataGrid <UserWeeklyBalanceModel> _grid { get; set; }
|
||||
private long? _filterUserId = null;
|
||||
private string _filterWeekNumber = "";
|
||||
private int? _minBalance = null;
|
||||
@@ -221,7 +224,7 @@
|
||||
|
||||
private async Task ApplyFilter()
|
||||
{
|
||||
StateHasChanged();
|
||||
await _grid.ReloadServerData();
|
||||
}
|
||||
|
||||
private void CalculateTotals(List<UserWeeklyBalanceModel> items)
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
@page "/network/tree"
|
||||
@attribute [Authorize]
|
||||
@inject IJSRuntime JS
|
||||
|
||||
@using BackOffice.BFF.NetworkMembership.Protobuf
|
||||
@using Foursat.BackOffice.BFF.NetworkMembership.Protos
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" 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;" />
|
||||
<div style="max-width: 300px; min-width: 250px;">
|
||||
<UserAutoComplete Label="جستجوی کاربر"
|
||||
@bind-SelectedUserId="_searchUserId" />
|
||||
</div>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
OnClick="LoadTree"
|
||||
@@ -42,8 +44,13 @@
|
||||
}
|
||||
else if (_treeData != null && _treeData.Nodes.Any())
|
||||
{
|
||||
<MudPaper Class="pa-4">
|
||||
<MudDataGrid T="NetworkTreeNodeModel" Items="@_treeData.Nodes" Hover="true" Filterable="true">
|
||||
<MudPaper Class="pa-4" Style="min-height: 800px;">
|
||||
<div id="network-tree-container" style="width: 100%; height: 800px; overflow: auto;"></div>
|
||||
</MudPaper>
|
||||
|
||||
<MudPaper Class="pa-4 mt-4">
|
||||
<MudText Typo="Typo.h6" Class="mb-3">جدول اعضای شبکه</MudText>
|
||||
<MudDataGrid T="NetworkTreeNodeModel" Items="@_treeData.Nodes" Hover="true" Filterable="true" Dense="true">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.UserId" Title="شناسه کاربر" />
|
||||
<PropertyColumn Property="x => x.UserName" Title="نام کاربر" />
|
||||
@@ -62,17 +69,28 @@
|
||||
|
||||
<PropertyColumn Property="x => x.IsActive" Title="وضعیت">
|
||||
<CellTemplate>
|
||||
<MudChip T="string"
|
||||
Color="@(context.Item.IsActive ? Color.Success : Color.Error)"
|
||||
Size="Size.Small">
|
||||
@(context.Item.IsActive ? "فعال" : "غیرفعال")
|
||||
@if (context.Item.IsActive!=null)
|
||||
{
|
||||
|
||||
<MudChip T="string"
|
||||
Color="@((bool)context.Item.IsActive ? Color.Success : Color.Error)"
|
||||
Size="Size.Small">
|
||||
@((bool)context.Item.IsActive ? "فعال" : "غیرفعال")
|
||||
</MudChip>
|
||||
}
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.JoinedAt" Title="تاریخ عضویت">
|
||||
<CellTemplate>
|
||||
@context.Item.JoinedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd")
|
||||
@if (context.Item.JoinedAt != null)
|
||||
{
|
||||
@context.Item.JoinedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd")
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.body2" Color="Color.Default">-</MudText>
|
||||
}
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
|
||||
@@ -104,41 +122,84 @@
|
||||
[Inject] public NetworkMembershipContract.NetworkMembershipContractClient NetworkContract { get; set; }
|
||||
[Inject] public NavigationManager NavigationManager { get; set; }
|
||||
|
||||
private long _searchUserId;
|
||||
private long? _searchUserId;
|
||||
private GetNetworkTreeResponse _treeData;
|
||||
private bool _isLoading;
|
||||
private int _totalMembers;
|
||||
private int _leftCount;
|
||||
private int _rightCount;
|
||||
private DotNetObjectReference<NetworkTreeViewer> _dotNetRef;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_dotNetRef = DotNetObjectReference.Create(this);
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await JS.InvokeVoidAsync("NetworkTreeViewer.setDotNetReference", _dotNetRef);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadTree()
|
||||
{
|
||||
if (_searchUserId <= 0)
|
||||
if (!_searchUserId.HasValue || _searchUserId.Value <= 0)
|
||||
{
|
||||
Snackbar.Add("لطفاً شناسه کاربر را وارد کنید", Severity.Warning);
|
||||
Snackbar.Add("لطفاً کاربر را انتخاب کنید", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
StateHasChanged(); // Force render to show loading state
|
||||
|
||||
try
|
||||
{
|
||||
var request = new GetNetworkTreeRequest { RootUserId = _searchUserId };
|
||||
var request = new GetNetworkTreeRequest { UserId = _searchUserId.Value, MaxDepth = 20 };
|
||||
_treeData = await NetworkContract.GetNetworkTreeAsync(request);
|
||||
|
||||
CalculateStats();
|
||||
|
||||
_isLoading = false;
|
||||
StateHasChanged(); // Render the container first
|
||||
|
||||
await Task.Delay(100); // Wait for DOM to be ready
|
||||
await RenderTree();
|
||||
|
||||
Snackbar.Add($"درخت بارگذاری شد - {_treeData.Nodes.Count} عضو", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری درخت: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RenderTree()
|
||||
{
|
||||
if (_treeData == null || !_treeData.Nodes.Any()) return;
|
||||
|
||||
var jsNodes = _treeData.Nodes.Select(n => new
|
||||
{
|
||||
userId = n.UserId,
|
||||
userName = n.UserName,
|
||||
parentId = n.ParentId,
|
||||
networkLevel = n.NetworkLevel,
|
||||
networkLeg = n.NetworkLeg,
|
||||
isActive = n.IsActive ?? false
|
||||
}).ToArray();
|
||||
|
||||
await JS.InvokeVoidAsync("NetworkTreeViewer.initialize", "network-tree-container", jsNodes);
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public async Task OnNodeClicked(long userId)
|
||||
{
|
||||
_searchUserId = userId;
|
||||
await LoadTree();
|
||||
}
|
||||
|
||||
private void CalculateStats()
|
||||
{
|
||||
if (_treeData == null || !_treeData.Nodes.Any()) return;
|
||||
@@ -152,4 +213,9 @@
|
||||
{
|
||||
NavigationManager.NavigateTo($"/network/user-info/{userId}");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_dotNetRef?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@page "/network/statistics"
|
||||
|
||||
@using Foursat.BackOffice.BFF.NetworkMembership.Protos
|
||||
@using MudBlazor
|
||||
@using BackOffice.BFF.NetworkMembership.Protobuf
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">آمار شبکه</MudText>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
@page "/network/user-info/{UserId:long}"
|
||||
@using Foursat.BackOffice.BFF.NetworkMembership.Protos
|
||||
@attribute [Authorize]
|
||||
|
||||
@using BackOffice.BFF.NetworkMembership.Protobuf
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<MudBreadcrumbs Items="_breadcrumbs" Class="mb-4"></MudBreadcrumbs>
|
||||
|
||||
@if (_isLoading)
|
||||
@@ -15,6 +15,7 @@
|
||||
else if (_userInfo != null)
|
||||
{
|
||||
<MudGrid>
|
||||
<!-- اطلاعات کاربر -->
|
||||
<MudItem xs="12" md="6">
|
||||
<MudCard>
|
||||
<MudCardHeader>
|
||||
@@ -33,6 +34,34 @@
|
||||
<td><strong>نام کاربر:</strong></td>
|
||||
<td>@_userInfo.UserName</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>موبایل:</strong></td>
|
||||
<td>
|
||||
@_userInfo.Mobile
|
||||
@if (_userInfo.IsMobileVerified)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Success" Size="Size.Small" Icon="@Icons.Material.Filled.Verified">تایید شده</MudChip>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
@if (!string.IsNullOrEmpty(_userInfo.Email))
|
||||
{
|
||||
<tr>
|
||||
<td><strong>ایمیل:</strong></td>
|
||||
<td>@_userInfo.Email</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(_userInfo.NationalCode))
|
||||
{
|
||||
<tr>
|
||||
<td><strong>کد ملی:</strong></td>
|
||||
<td>@_userInfo.NationalCode</td>
|
||||
</tr>
|
||||
}
|
||||
<tr>
|
||||
<td><strong>کد ارجاع:</strong></td>
|
||||
<td><MudChip T="string" Color="Color.Info" Size="Size.Small">@_userInfo.ReferralCode</MudChip></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>موقعیت:</strong></td>
|
||||
<td>
|
||||
@@ -43,13 +72,9 @@
|
||||
</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>
|
||||
<td>@PersianDateTime.ConvertToPersianDateTime(_userInfo.JoinedAt.ToDateTime().ToLocalTime())</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
@@ -57,6 +82,7 @@
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
|
||||
<!-- ساختار شبکه -->
|
||||
<MudItem xs="12" md="6">
|
||||
<MudCard>
|
||||
<MudCardHeader>
|
||||
@@ -67,17 +93,20 @@
|
||||
<MudCardContent>
|
||||
<MudSimpleTable Dense="true">
|
||||
<tbody>
|
||||
@if (_userInfo.ParentId.HasValue && _userInfo.ParentId.Value > 0)
|
||||
@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>
|
||||
<MudStack Spacing="1">
|
||||
<MudButton Size="Size.Small"
|
||||
Variant="Variant.Text"
|
||||
Color="Color.Primary"
|
||||
OnClick="@(() => NavigateToUser(_userInfo.ParentId))">
|
||||
@_userInfo.ParentName (ID: @_userInfo.ParentId)
|
||||
</MudButton>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">@_userInfo.ParentMobile</MudText>
|
||||
</MudStack>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@@ -86,12 +115,21 @@
|
||||
<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>
|
||||
<MudStack Spacing="1">
|
||||
<MudButton Size="Size.Small"
|
||||
Variant="Variant.Text"
|
||||
Color="Color.Success"
|
||||
OnClick="@(() => NavigateToUser(_userInfo.LeftChildId))">
|
||||
@_userInfo.LeftChildName (ID: @_userInfo.LeftChildId)
|
||||
</MudButton>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">@_userInfo.LeftChildMobile</MudText>
|
||||
@if (_userInfo.LeftChildJoinedAt != null)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||
عضو شده: @PersianDateTime.ConvertToPersianDate(_userInfo.LeftChildJoinedAt.ToDateTime().ToLocalTime())
|
||||
</MudText>
|
||||
}
|
||||
</MudStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -104,12 +142,21 @@
|
||||
<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>
|
||||
<MudStack Spacing="1">
|
||||
<MudButton Size="Size.Small"
|
||||
Variant="Variant.Text"
|
||||
Color="Color.Warning"
|
||||
OnClick="@(() => NavigateToUser(_userInfo.RightChildId))">
|
||||
@_userInfo.RightChildName (ID: @_userInfo.RightChildId)
|
||||
</MudButton>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">@_userInfo.RightChildMobile</MudText>
|
||||
@if (_userInfo.RightChildJoinedAt != null)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||
عضو شده: @PersianDateTime.ConvertToPersianDate(_userInfo.RightChildJoinedAt.ToDateTime().ToLocalTime())
|
||||
</MudText>
|
||||
}
|
||||
</MudStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -123,21 +170,183 @@
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
|
||||
<!-- آمار شبکه -->
|
||||
<MudItem xs="12">
|
||||
<MudCard>
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
<MudText Typo="Typo.h6">آمار شبکه</MudText>
|
||||
<MudText Typo="Typo.h6">آمار کامل شبکه</MudText>
|
||||
</CardHeaderContent>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudAlert Severity="Severity.Info">
|
||||
<MudText>آمار تجمعی شبکه در نسخه بعدی اضافه خواهد شد</MudText>
|
||||
</MudAlert>
|
||||
<MudGrid>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4" Elevation="0">
|
||||
<MudStack Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.AccountTree" Color="Color.Primary" Size="Size.Large" />
|
||||
<MudText Typo="Typo.h4">@_userInfo.TotalNetworkSize</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">کل اعضای شبکه</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4" Elevation="0">
|
||||
<MudStack Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.TrendingUp" Color="Color.Success" Size="Size.Large" />
|
||||
<MudText Typo="Typo.h4">@_userInfo.TotalLeftLegMembers</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">اعضای شاخه چپ</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4" Elevation="0">
|
||||
<MudStack Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.TrendingDown" Color="Color.Warning" Size="Size.Large" />
|
||||
<MudText Typo="Typo.h4">@_userInfo.TotalRightLegMembers</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">اعضای شاخه راست</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4" Elevation="0">
|
||||
<MudStack Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Layers" Color="Color.Info" Size="Size.Large" />
|
||||
<MudText Typo="Typo.h4">@_userInfo.MaxNetworkDepth</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">حداکثر عمق شبکه</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4" Elevation="0">
|
||||
<MudStack Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Large" />
|
||||
<MudText Typo="Typo.h4">@_userInfo.ActiveMembersInNetwork</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">اعضای فعال (پکیج خریداری)</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4" Elevation="0">
|
||||
<MudStack Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Cancel" Color="Color.Error" Size="Size.Large" />
|
||||
<MudText Typo="Typo.h4">@_userInfo.InactiveMembersInNetwork</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">اعضای غیرفعال</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
|
||||
<!-- آمار مالی -->
|
||||
<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>
|
||||
<MudText Typo="Typo.body1" Color="Color.Primary">
|
||||
<strong>@_userInfo.TotalEarnedCommission.ToString("N0") ریال</strong>
|
||||
</MudText>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>کمیسیون پرداخت شده:</strong></td>
|
||||
<td>
|
||||
<MudText Typo="Typo.body1" Color="Color.Success">
|
||||
@_userInfo.TotalPaidCommission.ToString("N0") ریال
|
||||
</MudText>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>کمیسیون در انتظار:</strong></td>
|
||||
<td>
|
||||
<MudText Typo="Typo.body1" Color="Color.Warning">
|
||||
@_userInfo.PendingCommission.ToString("N0") ریال
|
||||
</MudText>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>تعداد بالانس کسب شده:</strong></td>
|
||||
<td>
|
||||
<MudChip T="string" Color="Color.Info" Size="Size.Small">
|
||||
@_userInfo.TotalBalancesEarned بالانس
|
||||
</MudChip>
|
||||
</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>
|
||||
<tr>
|
||||
<td><strong>پکیج طلایی:</strong></td>
|
||||
<td>
|
||||
@if (_userInfo.HasPurchasedGoldenPackage)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Success" Size="Size.Small" Icon="@Icons.Material.Filled.CheckCircle">
|
||||
خریداری شده
|
||||
</MudChip>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||
روش: @GetPackagePurchaseMethodText(_userInfo.PackagePurchaseMethod)
|
||||
</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChip T="string" Color="Color.Default" Size="Size.Small">خریداری نشده</MudChip>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>اعتبار دایا:</strong></td>
|
||||
<td>
|
||||
@if (_userInfo.HasReceivedDayaCredit)
|
||||
{
|
||||
<MudStack Spacing="1">
|
||||
<MudChip T="string" Color="Color.Success" Size="Size.Small" Icon="@Icons.Material.Filled.CheckCircle">
|
||||
دریافت شده
|
||||
</MudChip>
|
||||
@if (_userInfo.DayaCreditReceivedAt != null)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||
تاریخ: @PersianDateTime.ConvertToPersianDate(_userInfo.DayaCreditReceivedAt.ToDateTime().ToLocalTime())
|
||||
</MudText>
|
||||
}
|
||||
</MudStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChip T="string" Color="Color.Default" Size="Size.Small">دریافت نشده</MudChip>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
|
||||
<!-- عملیات -->
|
||||
<MudItem xs="12">
|
||||
<MudCard>
|
||||
<MudCardHeader>
|
||||
@@ -155,9 +364,15 @@
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Info"
|
||||
StartIcon="@Icons.Material.Filled.History"
|
||||
OnClick="ViewHistory">
|
||||
تاریخچه تغییرات
|
||||
StartIcon="@Icons.Material.Filled.Money"
|
||||
OnClick="@(() => NavigationManager.NavigateTo($"/commission/payouts?userId={UserId}"))">
|
||||
Payout های کاربر
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Secondary"
|
||||
StartIcon="@Icons.Material.Filled.Refresh"
|
||||
OnClick="LoadUserInfo">
|
||||
بروزرسانی
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
@@ -177,6 +392,7 @@
|
||||
[Parameter] public long UserId { get; set; }
|
||||
[Inject] public NetworkMembershipContract.NetworkMembershipContractClient NetworkContract { get; set; }
|
||||
[Inject] public NavigationManager NavigationManager { get; set; }
|
||||
[Inject] public BackOffice.Services.IPersianDateTimeService PersianDateTime { get; set; }
|
||||
|
||||
private GetUserNetworkResponse _userInfo;
|
||||
private bool _isLoading = true;
|
||||
@@ -217,10 +433,15 @@
|
||||
NavigationManager.NavigateTo($"/network/user-info/{parentIdValue.Value}");
|
||||
}
|
||||
|
||||
private async Task ViewHistory()
|
||||
private string GetPackagePurchaseMethodText(int method)
|
||||
{
|
||||
// Future enhancement: Show historical changes (parent changes, status updates, etc.)
|
||||
// Requires: Historical tracking table in CMS database
|
||||
Snackbar.Add("این قابلیت به زودی اضافه خواهد شد", Severity.Info);
|
||||
return method switch
|
||||
{
|
||||
0 => "خریداری نشده",
|
||||
1 => "خرید با پول",
|
||||
2 => "خرید با بالانس",
|
||||
3 => "هدیه سیستم",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@page "/system/configuration"
|
||||
@attribute [Authorize(Roles = "Administrator")]
|
||||
|
||||
@using Foursat.BackOffice.BFF.Configuration.Protobuf
|
||||
@using Foursat.BackOffice.BFF.Configuration.Protos
|
||||
@using MudBlazor
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
|
||||
@@ -31,11 +31,11 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>آخرین اجرا:</strong></td>
|
||||
<td>@_lastRunTime.ToString("yyyy/MM/dd HH:mm:ss")</td>
|
||||
<td>@PersianDateTime.ConvertToPersianDateTime(_lastRunTime)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>اجرای بعدی:</strong></td>
|
||||
<td>@_nextRunTime.ToString("yyyy/MM/dd HH:mm:ss")</td>
|
||||
<td>@PersianDateTime.ConvertToPersianDateTime(_nextRunTime)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>تعداد اجراهای موفق:</strong></td>
|
||||
@@ -143,8 +143,8 @@
|
||||
<MudTh>پیام خطا</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="زمان">@context.ExecutionTime.ToString("yyyy/MM/dd HH:mm:ss")</MudTd>
|
||||
<MudTd DataLabel="هفته">@context.WeekNumber</MudTd>
|
||||
<MudTd DataLabel="زمان">@PersianDateTime.ConvertToPersianDateTime(context.ExecutionTime)</MudTd>
|
||||
<MudTd DataLabel="هفته">@PersianDateTime.ConvertWeekNumberToPersian(context.WeekNumber)</MudTd>
|
||||
<MudTd DataLabel="وضعیت">
|
||||
<MudChip T="string"
|
||||
Color="@(context.Status == "موفق" ? Color.Success : Color.Error)"
|
||||
@@ -176,7 +176,8 @@
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
[Inject] public BackOffice.BFF.Commission.Protobuf.CommissionContract.CommissionContractClient CommissionClient { get; set; }
|
||||
[Inject] public Foursat.BackOffice.BFF.Commission.Protos.CommissionContract.CommissionContractClient CommissionClient { get; set; }
|
||||
[Inject] public BackOffice.Services.IPersianDateTimeService PersianDateTime { get; set; }
|
||||
|
||||
private WorkerStatus _workerStatus = WorkerStatus.Running;
|
||||
private DateTime _lastRunTime = DateTime.Now.AddHours(-2);
|
||||
@@ -191,7 +192,7 @@
|
||||
{
|
||||
try
|
||||
{
|
||||
var statusResponse = await CommissionClient.GetWorkerStatusAsync(new BackOffice.BFF.Commission.Protobuf.GetWorkerStatusRequest());
|
||||
var statusResponse = await CommissionClient.GetWorkerStatusAsync(new Foursat.BackOffice.BFF.Commission.Protos.GetWorkerStatusRequest());
|
||||
_workerStatus = statusResponse.IsRunning ? WorkerStatus.Running : WorkerStatus.Paused;
|
||||
_lastRunTime = statusResponse.LastRunAt?.ToDateTime() ?? DateTime.MinValue;
|
||||
_nextRunTime = statusResponse.NextScheduledRun?.ToDateTime() ?? DateTime.MinValue;
|
||||
@@ -217,7 +218,7 @@
|
||||
|
||||
var confirmed = await DialogService.ShowMessageBox(
|
||||
"اجرای دستی محاسبات",
|
||||
$"آیا از اجرای محاسبات برای هفته {_manualWeekNumber} اطمینان دارید؟",
|
||||
$"آیا از اجرای محاسبات برای هفته {PersianDateTime.ConvertWeekNumberToPersian(_manualWeekNumber)} اطمینان دارید؟",
|
||||
yesText: "بله، اجرا شود", cancelText: "لغو");
|
||||
|
||||
if (confirmed == true)
|
||||
@@ -225,13 +226,14 @@
|
||||
_isProcessing = true;
|
||||
try
|
||||
{
|
||||
var request = new BackOffice.BFF.Commission.Protobuf.TriggerWeeklyCalculationRequest
|
||||
var request = new Foursat.BackOffice.BFF.Commission.Protos.TriggerWeeklyCalculationRequest
|
||||
{
|
||||
WeekNumber = _manualWeekNumber
|
||||
};
|
||||
await CommissionClient.TriggerWeeklyCalculationAsync(request);
|
||||
|
||||
Snackbar.Add($"محاسبات هفته {_manualWeekNumber} با موفقیت آغاز شد", Severity.Success);
|
||||
var persianWeek = PersianDateTime.ConvertWeekNumberToPersian(_manualWeekNumber);
|
||||
Snackbar.Add($"محاسبات هفته {persianWeek} با موفقیت آغاز شد", Severity.Success);
|
||||
_manualWeekNumber = "";
|
||||
await RefreshLog();
|
||||
}
|
||||
@@ -271,7 +273,7 @@
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new BackOffice.BFF.Commission.Protobuf.GetWorkerExecutionLogsRequest
|
||||
var request = new Foursat.BackOffice.BFF.Commission.Protos.GetWorkerExecutionLogsRequest
|
||||
{
|
||||
PageIndex = 1,
|
||||
PageSize = 20
|
||||
|
||||
@@ -29,27 +29,27 @@ public class AuthorizationService : IAuthorizationService
|
||||
if (cachedPermissions == null || cachedPermissions.Count == 0)
|
||||
{
|
||||
// فعلاً بر اساس Role ساده تصمیم میگیریم تا زمانی که BFF Permission API آماده شود
|
||||
var role = await GetUserRoleAsync();
|
||||
if (string.IsNullOrWhiteSpace(role))
|
||||
var roles = await GetUserRolesAsync();
|
||||
if (roles == null || roles.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// SuperAdmin: همه دسترسیها
|
||||
if (string.Equals(role, "Administrator", StringComparison.OrdinalIgnoreCase))
|
||||
if (roles.Any(r => string.Equals(r, "Administrator", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Admin: اجازه دسترسی به بیشتر صفحات مدیریتی
|
||||
if (string.Equals(role, "Admin", StringComparison.OrdinalIgnoreCase))
|
||||
if (roles.Any(r => string.Equals(r, "Admin", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
// فعلاً همه permissionهای UI را برای Admin آزاد میکنیم
|
||||
return true;
|
||||
}
|
||||
|
||||
// Inspector: فقط view
|
||||
if (string.Equals(role, "Inspector", StringComparison.OrdinalIgnoreCase))
|
||||
if (roles.Any(r => string.Equals(r, "Inspector", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return permission.EndsWith(".view", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
@@ -61,6 +61,12 @@ public class AuthorizationService : IAuthorizationService
|
||||
}
|
||||
|
||||
public async Task<string?> GetUserRoleAsync()
|
||||
{
|
||||
var roles = await GetUserRolesAsync();
|
||||
return roles?.FirstOrDefault();
|
||||
}
|
||||
|
||||
public async Task<List<string>?> GetUserRolesAsync()
|
||||
{
|
||||
var authState = await _authenticationStateProvider.GetAuthenticationStateAsync();
|
||||
var user = authState.User;
|
||||
@@ -70,8 +76,8 @@ public class AuthorizationService : IAuthorizationService
|
||||
return null;
|
||||
}
|
||||
|
||||
var roleClaim = user.FindFirst(ClaimTypes.Role) ?? user.FindFirst("role");
|
||||
return roleClaim?.Value;
|
||||
var roleClaims = user.FindAll(ClaimTypes.Role).Select(c => c.Value).ToList();
|
||||
return roleClaims.Count > 0 ? roleClaims : null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
161
src/BackOffice/Services/PersianDateTimeService.cs
Normal file
161
src/BackOffice/Services/PersianDateTimeService.cs
Normal file
@@ -0,0 +1,161 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace BackOffice.Services;
|
||||
|
||||
/// <summary>
|
||||
/// سرویس تبدیل و نمایش تاریخ شمسی
|
||||
/// </summary>
|
||||
public interface IPersianDateTimeService
|
||||
{
|
||||
/// <summary>
|
||||
/// دریافت شماره هفته فعلی به فرمت شمسی
|
||||
/// فرمت: "1404-W23"
|
||||
/// </summary>
|
||||
string GetCurrentWeekNumber();
|
||||
|
||||
/// <summary>
|
||||
/// تبدیل شماره هفته میلادی به شمسی
|
||||
/// ورودی: "2025-W48" (میلادی)
|
||||
/// خروجی: "1404-W23" (شمسی)
|
||||
/// </summary>
|
||||
string ConvertWeekNumberToPersian(string gregorianWeekNumber);
|
||||
|
||||
/// <summary>
|
||||
/// تبدیل تاریخ میلادی به شمسی با فرمت کامل
|
||||
/// خروجی: "1404/09/21"
|
||||
/// </summary>
|
||||
string ConvertToPersianDate(DateTime dateTime);
|
||||
|
||||
/// <summary>
|
||||
/// تبدیل تاریخ میلادی به شمسی با فرمت کامل + ساعت
|
||||
/// خروجی: "1404/09/21 - 14:30"
|
||||
/// </summary>
|
||||
string ConvertToPersianDateTime(DateTime dateTime);
|
||||
|
||||
/// <summary>
|
||||
/// نمایش بازه هفته به شمسی
|
||||
/// خروجی: "شنبه 1404/09/15 تا جمعه 1404/09/21"
|
||||
/// </summary>
|
||||
string GetWeekRangeDisplay(string gregorianWeekNumber);
|
||||
}
|
||||
|
||||
public class PersianDateTimeService : IPersianDateTimeService
|
||||
{
|
||||
private readonly PersianCalendar _persianCalendar = new();
|
||||
private static readonly string[] PersianDayNames =
|
||||
{ "یکشنبه", "دوشنبه", "سهشنبه", "چهارشنبه", "پنجشنبه", "جمعه", "شنبه" };
|
||||
|
||||
public string GetCurrentWeekNumber()
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
return GetPersianWeekNumber(now);
|
||||
}
|
||||
|
||||
public string ConvertWeekNumberToPersian(string gregorianWeekNumber)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Parse: "2025-W48"
|
||||
var parts = gregorianWeekNumber.Split('-');
|
||||
if (parts.Length != 2 || !parts[1].StartsWith("W"))
|
||||
return gregorianWeekNumber;
|
||||
|
||||
var year = int.Parse(parts[0]);
|
||||
var week = int.Parse(parts[1].Replace("W", ""));
|
||||
|
||||
// محاسبه تاریخ میلادی این هفته
|
||||
var jan1 = new DateTime(year, 1, 1);
|
||||
var jan1DayOfWeek = (int)jan1.DayOfWeek;
|
||||
var daysToFirstSaturday = jan1DayOfWeek == 6 ? 0 : (6 - jan1DayOfWeek + 7) % 7;
|
||||
var firstSaturday = jan1.AddDays(daysToFirstSaturday);
|
||||
var weekStart = firstSaturday.AddDays((week - 1) * 7);
|
||||
|
||||
// تبدیل به شمسی
|
||||
return GetPersianWeekNumber(weekStart);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return gregorianWeekNumber;
|
||||
}
|
||||
}
|
||||
|
||||
public string ConvertToPersianDate(DateTime dateTime)
|
||||
{
|
||||
var year = _persianCalendar.GetYear(dateTime);
|
||||
var month = _persianCalendar.GetMonth(dateTime);
|
||||
var day = _persianCalendar.GetDayOfMonth(dateTime);
|
||||
|
||||
return $"{year:0000}/{month:00}/{day:00}";
|
||||
}
|
||||
|
||||
public string ConvertToPersianDateTime(DateTime dateTime)
|
||||
{
|
||||
var persianDate = ConvertToPersianDate(dateTime);
|
||||
var time = dateTime.ToString("HH:mm");
|
||||
|
||||
return $"{persianDate} - {time}";
|
||||
}
|
||||
|
||||
public string GetWeekRangeDisplay(string gregorianWeekNumber)
|
||||
{
|
||||
try
|
||||
{
|
||||
var (startDate, endDate) = ParseGregorianWeekNumber(gregorianWeekNumber);
|
||||
|
||||
var startPersian = ConvertToPersianDate(startDate);
|
||||
var endPersian = ConvertToPersianDate(endDate);
|
||||
|
||||
var startDay = PersianDayNames[(int)startDate.DayOfWeek];
|
||||
var endDay = PersianDayNames[(int)endDate.DayOfWeek];
|
||||
|
||||
return $"{startDay} {startPersian} تا {endDay} {endPersian}";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return gregorianWeekNumber;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// محاسبه شماره هفته شمسی (شنبه محور)
|
||||
/// </summary>
|
||||
private string GetPersianWeekNumber(DateTime dateTime)
|
||||
{
|
||||
var persianYear = _persianCalendar.GetYear(dateTime);
|
||||
var dayOfYear = _persianCalendar.GetDayOfYear(dateTime);
|
||||
|
||||
// محاسبه اولین روز سال شمسی
|
||||
var firstDayOfYear = _persianCalendar.ToDateTime(persianYear, 1, 1, 0, 0, 0, 0);
|
||||
var firstDayOfWeek = (int)firstDayOfYear.DayOfWeek;
|
||||
|
||||
// محاسبه تعداد روزها تا اولین شنبه
|
||||
// شنبه = 6, یکشنبه = 0, دوشنبه = 1, ...
|
||||
var daysToFirstSaturday = firstDayOfWeek == 6 ? 0 : (6 - firstDayOfWeek + 7) % 7;
|
||||
|
||||
// محاسبه شماره هفته
|
||||
var adjustedDayOfYear = dayOfYear + daysToFirstSaturday - 1;
|
||||
var weekNumber = (adjustedDayOfYear / 7) + 1;
|
||||
|
||||
return $"{persianYear}-W{weekNumber:D2}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// تبدیل شماره هفته میلادی به بازه تاریخی
|
||||
/// </summary>
|
||||
private (DateTime startDate, DateTime endDate) ParseGregorianWeekNumber(string weekNumber)
|
||||
{
|
||||
var parts = weekNumber.Split('-');
|
||||
var year = int.Parse(parts[0]);
|
||||
var week = int.Parse(parts[1].Replace("W", ""));
|
||||
|
||||
var jan1 = new DateTime(year, 1, 1);
|
||||
var jan1DayOfWeek = (int)jan1.DayOfWeek;
|
||||
var daysToFirstSaturday = jan1DayOfWeek == 6 ? 0 : (6 - jan1DayOfWeek + 7) % 7;
|
||||
var firstSaturday = jan1.AddDays(daysToFirstSaturday);
|
||||
|
||||
var weekStart = firstSaturday.AddDays((week - 1) * 7);
|
||||
var weekEnd = weekStart.AddDays(6);
|
||||
|
||||
return (weekStart, weekEnd);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
// "GwUrl": "https://bogw.kbs1.ir",
|
||||
"GwUrl": "https://backoffice-bff.foursat.afrino.co",
|
||||
// "GwUrl": "https://localhost:6468",
|
||||
"Authentication": {
|
||||
//"Authority": "https://localhost:5001",
|
||||
"Authority": "https://ids.afrino.co/",
|
||||
|
||||
@@ -34,8 +34,10 @@
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
||||
<script src="/js/d3.v7.min.js"></script>
|
||||
<script src="/js/main.js"></script>
|
||||
<script src="/js/quill.js"></script>
|
||||
<script src="/js/network-tree.js"></script>
|
||||
<script src="_content/Tizzani.MudBlazor.HtmlEditor/quill-blot-formatter.min.js"></script> <!-- optional; for image resize -->
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
<script>navigator.serviceWorker.register('service-worker.js');</script>
|
||||
|
||||
2
src/BackOffice/wwwroot/js/d3.v7.min.js
vendored
Normal file
2
src/BackOffice/wwwroot/js/d3.v7.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
210
src/BackOffice/wwwroot/js/network-tree.js
Normal file
210
src/BackOffice/wwwroot/js/network-tree.js
Normal file
@@ -0,0 +1,210 @@
|
||||
// Network Tree Visualization using D3.js
|
||||
window.NetworkTreeViewer = {
|
||||
instance: null,
|
||||
dotNetRef: null,
|
||||
|
||||
setDotNetReference: function(dotNetReference) {
|
||||
this.dotNetRef = dotNetReference;
|
||||
},
|
||||
|
||||
initialize: function(elementId, data) {
|
||||
const container = document.getElementById(elementId);
|
||||
if (!container) {
|
||||
console.error('Container not found:', elementId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear previous content
|
||||
container.innerHTML = '';
|
||||
|
||||
// Set dimensions - responsive to container
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const width = containerRect.width || 1200;
|
||||
const height = Math.max(containerRect.height, 800);
|
||||
|
||||
// Create SVG with viewBox for responsiveness
|
||||
const svg = d3.select(`#${elementId}`)
|
||||
.append('svg')
|
||||
.attr('width', '100%')
|
||||
.attr('height', '100%')
|
||||
.attr('viewBox', `0 0 ${width} ${height}`)
|
||||
.attr('preserveAspectRatio', 'xMidYMid meet')
|
||||
.style('background-color', '#fafafa')
|
||||
.call(d3.zoom()
|
||||
.scaleExtent([0.1, 3])
|
||||
.on('zoom', (event) => {
|
||||
g.attr('transform', event.transform);
|
||||
}));
|
||||
|
||||
const g = svg.append('g')
|
||||
.attr('transform', 'translate(40,0)');
|
||||
|
||||
// Create tree layout
|
||||
const tree = d3.tree()
|
||||
.size([height - 100, width - 300])
|
||||
.separation((a, b) => a.parent === b.parent ? 1.5 : 2);
|
||||
|
||||
// Convert flat data to hierarchy
|
||||
const root = this.buildHierarchy(data);
|
||||
|
||||
if (!root) {
|
||||
container.innerHTML = '<div style="padding: 20px; text-align: center; color: #666;">درخت خالی است</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate tree layout
|
||||
const treeData = tree(root);
|
||||
|
||||
// Add zoom reset button
|
||||
const resetButton = svg.append('g')
|
||||
.attr('transform', 'translate(20, 20)')
|
||||
.style('cursor', 'pointer')
|
||||
.on('click', () => {
|
||||
svg.transition()
|
||||
.duration(750)
|
||||
.call(d3.zoom().transform, d3.zoomIdentity);
|
||||
});
|
||||
|
||||
resetButton.append('rect')
|
||||
.attr('width', 80)
|
||||
.attr('height', 30)
|
||||
.attr('rx', 5)
|
||||
.style('fill', '#2196F3')
|
||||
.style('opacity', 0.8);
|
||||
|
||||
resetButton.append('text')
|
||||
.attr('x', 40)
|
||||
.attr('y', 20)
|
||||
.attr('text-anchor', 'middle')
|
||||
.style('fill', 'white')
|
||||
.style('font-size', '12px')
|
||||
.text('بازنشانی');
|
||||
|
||||
// Create links (edges)
|
||||
const link = g.selectAll('.link')
|
||||
.data(treeData.links())
|
||||
.enter().append('path')
|
||||
.attr('class', 'link')
|
||||
.attr('d', d3.linkHorizontal()
|
||||
.x(d => d.y)
|
||||
.y(d => d.x))
|
||||
.style('fill', 'none')
|
||||
.style('stroke', '#ccc')
|
||||
.style('stroke-width', 2);
|
||||
|
||||
// Create nodes
|
||||
const node = g.selectAll('.node')
|
||||
.data(treeData.descendants())
|
||||
.enter().append('g')
|
||||
.attr('class', 'node')
|
||||
.attr('transform', d => `translate(${d.y},${d.x})`);
|
||||
|
||||
// Add circles for nodes
|
||||
node.append('circle')
|
||||
.attr('r', 8)
|
||||
.style('fill', d => d.data.isActive ? '#4caf50' : '#f44336')
|
||||
.style('stroke', '#fff')
|
||||
.style('stroke-width', 2)
|
||||
.style('cursor', 'pointer')
|
||||
.on('click', (event, d) => {
|
||||
if (window.NetworkTreeViewer.dotNetRef) {
|
||||
window.NetworkTreeViewer.dotNetRef.invokeMethodAsync('OnNodeClicked', d.data.userId);
|
||||
}
|
||||
});
|
||||
|
||||
// Add user info labels
|
||||
node.append('text')
|
||||
.attr('dy', -15)
|
||||
.attr('text-anchor', 'middle')
|
||||
.style('font-size', '12px')
|
||||
.style('font-weight', 'bold')
|
||||
.style('fill', '#333')
|
||||
.text(d => d.data.userName || `User ${d.data.userId}`);
|
||||
|
||||
// Add level info
|
||||
node.append('text')
|
||||
.attr('dy', 20)
|
||||
.attr('text-anchor', 'middle')
|
||||
.style('font-size', '10px')
|
||||
.style('fill', '#666')
|
||||
.text(d => `سطح ${d.data.level}`);
|
||||
|
||||
// Add leg indicator (Left/Right)
|
||||
node.append('text')
|
||||
.attr('dy', 32)
|
||||
.attr('text-anchor', 'middle')
|
||||
.style('font-size', '9px')
|
||||
.style('fill', d => d.data.networkLeg === 0 ? '#4caf50' : '#ff9800')
|
||||
.text(d => d.data.networkLeg === 0 ? 'چپ' : 'راست');
|
||||
|
||||
// Add legend
|
||||
const legend = svg.append('g')
|
||||
.attr('transform', `translate(${width - 150}, 20)`);
|
||||
|
||||
legend.append('circle')
|
||||
.attr('cx', 0)
|
||||
.attr('cy', 0)
|
||||
.attr('r', 6)
|
||||
.style('fill', '#4caf50');
|
||||
|
||||
legend.append('text')
|
||||
.attr('x', 12)
|
||||
.attr('y', 4)
|
||||
.style('font-size', '12px')
|
||||
.text('فعال');
|
||||
|
||||
legend.append('circle')
|
||||
.attr('cx', 0)
|
||||
.attr('cy', 25)
|
||||
.attr('r', 6)
|
||||
.style('fill', '#f44336');
|
||||
|
||||
legend.append('text')
|
||||
.attr('x', 12)
|
||||
.attr('y', 29)
|
||||
.style('font-size', '12px')
|
||||
.text('غیرفعال');
|
||||
},
|
||||
|
||||
buildHierarchy: function(nodes) {
|
||||
if (!nodes || nodes.length === 0) return null;
|
||||
|
||||
// Find root (parent with no parent or lowest level)
|
||||
const root = nodes.find(n => !n.parentId || n.parentId === 0) || nodes[0];
|
||||
|
||||
const nodeMap = new Map();
|
||||
nodes.forEach(node => {
|
||||
nodeMap.set(node.userId, {
|
||||
userId: node.userId,
|
||||
userName: node.userName,
|
||||
parentId: node.parentId,
|
||||
level: node.networkLevel,
|
||||
networkLeg: node.networkLeg,
|
||||
isActive: node.isActive,
|
||||
children: []
|
||||
});
|
||||
});
|
||||
|
||||
// Build parent-child relationships
|
||||
nodes.forEach(node => {
|
||||
if (node.parentId && node.parentId !== node.userId) {
|
||||
const parent = nodeMap.get(node.parentId);
|
||||
const child = nodeMap.get(node.userId);
|
||||
if (parent && child) {
|
||||
parent.children.push(child);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Return root as D3 hierarchy
|
||||
const rootNode = nodeMap.get(root.userId);
|
||||
return d3.hierarchy(rootNode);
|
||||
},
|
||||
|
||||
destroy: function(elementId) {
|
||||
const container = document.getElementById(elementId);
|
||||
if (container) {
|
||||
container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user