feat: Add network tree visualization and Persian date service
All checks were successful
Build and Deploy / build (push) Successful in 2m39s

This commit is contained in:
masoodafar-web
2025-12-12 07:54:05 +03:30
parent 82068bf8f8
commit 2fc7733c84
32 changed files with 1071 additions and 140 deletions

View File

@@ -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" />

View File

@@ -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

View 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)"/>

View 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
}
}
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -1,4 +1,4 @@
@using Foursat.BackOffice.BFF.ClubMembership.Protobuf
@using Foursat.BackOffice.BFF.ClubMembership.Protos
<MudDialog>
<DialogContent>

View File

@@ -1,4 +1,4 @@
@using Foursat.BackOffice.BFF.ClubMembership.Protobuf
@using Foursat.BackOffice.BFF.ClubMembership.Protos
<MudDialog>
<DialogContent>

View File

@@ -1,4 +1,4 @@
@using Foursat.BackOffice.BFF.ClubMembership.Protobuf
@using Foursat.BackOffice.BFF.ClubMembership.Protos
<MudDialog>
<DialogContent>

View File

@@ -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>

View File

@@ -1,4 +1,4 @@
@using BackOffice.BFF.Commission.Protobuf
@using Foursat.BackOffice.BFF.Commission.Protos
<MudDialog>
<DialogContent>

View File

@@ -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>
@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>

View File

@@ -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,7 +108,14 @@ 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
@@ -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}";
}
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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">

View File

@@ -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

View File

@@ -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>

View File

@@ -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;

View File

@@ -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}";
}
}

View File

@@ -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)

View File

@@ -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>
@if (context.Item.IsActive!=null)
{
<MudChip T="string"
Color="@(context.Item.IsActive ? Color.Success : Color.Error)"
Color="@((bool)context.Item.IsActive ? Color.Success : Color.Error)"
Size="Size.Small">
@(context.Item.IsActive ? "فعال" : "غیرفعال")
@((bool)context.Item.IsActive ? "فعال" : "غیرفعال")
</MudChip>
}
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.JoinedAt" Title="تاریخ عضویت">
<CellTemplate>
@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();
}
}

View File

@@ -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>

View File

@@ -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>
@@ -72,12 +98,15 @@
<tr>
<td><strong>والد:</strong></td>
<td>
<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)
{
<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)
{
<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 => "هدیه سیستم",
_ => "نامشخص"
};
}
}

View File

@@ -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">

View File

@@ -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

View File

@@ -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;
}
}

View 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);
}
}

View File

@@ -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/",

View File

@@ -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>

File diff suppressed because one or more lines are too long

View 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 = '';
}
}
};