feat(club): update club reports and navigation with new customer package insights
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m14s

- Updated ReportLabels.cs to refine purchase method labels for clarity.
- Enhanced ClubHub.razor by adding a new tab for customer package reports and adjusting tab indices.
- Modified ClubPackagePurchasesReport.razor to reflect updated purchase method options.
- Updated NavMenu.razor to include a link to the new customer packages report.
- Bumped Foursat.CMSMicroservice.Protobuf version to 0.0.208 for compatibility with recent changes.
This commit is contained in:
masoodafar-web
2026-08-08 21:59:30 +03:30
parent e9f8328404
commit c3ee46ff62
7 changed files with 419 additions and 8 deletions
+1 -1
View File
@@ -143,7 +143,7 @@
<ProjectReference Include="../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj" /> <ProjectReference Include="../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="!Exists('../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj')"> <ItemGroup Condition="!Exists('../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj')">
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.207" /> <PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.208" />
</ItemGroup> </ItemGroup>
<!-- ============================================================ --> <!-- ============================================================ -->
@@ -42,8 +42,9 @@ public static class ReportLabels
public static string GetPurchaseMethodLabel(int method) => method switch public static string GetPurchaseMethodLabel(int method) => method switch
{ {
1 => "وام دایا", 1 => "دایا",
2 => "خرید مستقیم", 2 => "درگاه",
3 => "دستی",
_ => "نامشخص" _ => "نامشخص"
}; };
+8 -3
View File
@@ -1,6 +1,7 @@
@page "/club" @page "/club"
@page "/club/members" @page "/club/members"
@page "/club/purchases" @page "/club/purchases"
@page "/club/customer-packages"
@page "/club/cycles" @page "/club/cycles"
@page "/club/history" @page "/club/history"
@page "/club/statistics" @page "/club/statistics"
@@ -18,6 +19,9 @@
<MudTabPanel Text="گزارش خرید پکیج" Icon="@Icons.Material.Filled.ShoppingBag"> <MudTabPanel Text="گزارش خرید پکیج" Icon="@Icons.Material.Filled.ShoppingBag">
<ClubPackagePurchasesReport /> <ClubPackagePurchasesReport />
</MudTabPanel> </MudTabPanel>
<MudTabPanel Text="گزارش مشتریان پکیج" Icon="@Icons.Material.Filled.PeopleAlt">
<CustomerPackagePurchasesReport />
</MudTabPanel>
<MudTabPanel Text="گزارش دوره‌ها" Icon="@Icons.Material.Filled.Loop"> <MudTabPanel Text="گزارش دوره‌ها" Icon="@Icons.Material.Filled.Loop">
<ClubCyclesReport /> <ClubCyclesReport />
</MudTabPanel> </MudTabPanel>
@@ -39,9 +43,10 @@
_activeTab = uri switch _activeTab = uri switch
{ {
"club/purchases" => 1, "club/purchases" => 1,
"club/cycles" => 2, "club/customer-packages" => 2,
"club/history" => 3, "club/cycles" => 3,
"club/statistics" => 4, "club/history" => 4,
"club/statistics" => 5,
_ => 0 _ => 0
}; };
} }
@@ -17,8 +17,9 @@
<MudItem xs="12" sm="6" md="3"> <MudItem xs="12" sm="6" md="3">
<MudSelect T="int?" Clearable="true" Label="روش خرید" Variant="Variant.Outlined" <MudSelect T="int?" Clearable="true" Label="روش خرید" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_purchaseMethod"> Margin="Margin.Dense" @bind-Value="_purchaseMethod">
<MudSelectItem T="int?" Value="1">وام دایا</MudSelectItem> <MudSelectItem T="int?" Value="1">دایا</MudSelectItem>
<MudSelectItem T="int?" Value="2">خرید مستقیم</MudSelectItem> <MudSelectItem T="int?" Value="3">دستی</MudSelectItem>
<MudSelectItem T="int?" Value="2">درگاه</MudSelectItem>
</MudSelect> </MudSelect>
</MudItem> </MudItem>
<MudItem xs="12" sm="6" md="4"> <MudItem xs="12" sm="6" md="4">
@@ -0,0 +1,105 @@
@using CMSMicroservice.Protobuf.Protos.UserPackagePurchase
@using Google.Protobuf.WellKnownTypes
@using DateTimeConverterCL
@inject UserPackagePurchaseContract.UserPackagePurchaseContractClient PurchaseClient
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">
جزئیات خرید پکیج — @UserName
</MudText>
</TitleContent>
<DialogContent>
@if (_loading)
{
<MudProgressLinear Indeterminate="true" Color="Color.Primary" Class="my-4" />
}
else if (_items.Count == 0)
{
<MudAlert Severity="Severity.Info" Dense="true">خریدی برای این کاربر یافت نشد.</MudAlert>
}
else
{
<MudTable Items="_items" Dense="true" Hover="true" Breakpoint="Breakpoint.Sm">
<HeaderContent>
<MudTh>شناسه</MudTh>
<MudTh>پکیج</MudTh>
<MudTh>روش</MudTh>
<MudTh>مبلغ (ریال)</MudTh>
<MudTh>تاریخ خرید</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="شناسه">@context.Id</MudTd>
<MudTd DataLabel="پکیج">@context.PackageName</MudTd>
<MudTd DataLabel="روش">
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined"
Color="@GetMethodColor(context.PurchaseMethod)">
@ReportLabels.GetPurchaseMethodLabel(context.PurchaseMethod)
</MudChip>
</MudTd>
<MudTd DataLabel="مبلغ">@context.Amount.ToString("N0")</MudTd>
<MudTd DataLabel="تاریخ">
@(context.PurchasedAt != null
? context.PurchasedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()
: "-")
</MudTd>
</RowTemplate>
</MudTable>
}
</DialogContent>
<DialogActions>
<MudButton OnClick="Close" Variant="Variant.Text">بستن</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!;
[Parameter] public long UserId { get; set; }
[Parameter] public string UserName { get; set; } = string.Empty;
private bool _loading = true;
private List<UserPackagePurchaseModel> _items = new();
protected override async Task OnInitializedAsync()
{
try
{
var result = await PurchaseClient.GetAllUserPackagePurchaseByFilterAsync(
new GetAllUserPackagePurchaseByFilterRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = 1,
PageSize = 500
},
Filter = new GetAllUserPackagePurchaseByFilterFilter
{
UserId = UserId
}
});
_items = result?.Models?
.OrderBy(x => x.PurchasedAt?.ToDateTime() ?? DateTime.MinValue)
.ToList() ?? new();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری جزئیات: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
private static Color GetMethodColor(int method) => method switch
{
1 => Color.Info, // دایا
2 => Color.Primary, // درگاه
3 => Color.Secondary, // دستی
_ => Color.Default
};
private void Close() => MudDialog.Close();
}
@@ -0,0 +1,294 @@
@using System.Text
@using BackOffice.Common.BaseComponents
@using BackOffice.Common.Utilities
@using BackOffice.Pages.AutoComplete
@using BackOffice.Pages.Club.Reports.Components
@using CMSMicroservice.Protobuf.Protos.UserPackagePurchase
@using Google.Protobuf.WellKnownTypes
@using DateTimeConverterCL
@using Microsoft.JSInterop
@inject UserPackagePurchaseContract.UserPackagePurchaseContractClient PurchaseClient
@inject IDialogService DialogService
@inject IJSRuntime JsRuntime
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3">
گزارش تجمیعی مشتریان — اولین/آخرین پکیج، تفکیک دایا / دستی / درگاه. خروجی Excel مطابق فیلتر فعال است.
</MudAlert>
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
<Filters>
<MudItem xs="12" sm="6" md="3">
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_userId" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="int?" Clearable="true" Label="روش خرید" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_purchaseMethod">
<MudSelectItem T="int?" Value="1">دایا</MudSelectItem>
<MudSelectItem T="int?" Value="3">دستی</MudSelectItem>
<MudSelectItem T="int?" Value="2">درگاه</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه خرید" />
</MudItem>
</Filters>
<Content>
<MudStack Row="true" Justify="Justify.FlexEnd" Class="mb-3">
<MudButton Variant="Variant.Outlined" Color="Color.Success"
StartIcon="@Icons.Material.Filled.FileDownload"
OnClick="ExportToExcel" Disabled="_exporting">
خروجی Excel
</MudButton>
</MudStack>
<MudDataGrid @ref="_grid" T="CustomerPackagePurchaseRollupModel" ServerData="LoadData" ReadOnly="true"
Hover="true" Dense="true" Height="calc(100vh - 420px)">
<Columns>
<TemplateColumn Title="مشتری">
<CellTemplate>
<MudStack Spacing="0">
<MudText Typo="Typo.body2">@context.Item.UserName</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">@context.Item.UserMobile</MudText>
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="اولین پکیج">
<CellTemplate>
<MudStack Spacing="0">
<MudText Typo="Typo.body2">@context.Item.FirstPackageName</MudText>
<MudText Typo="Typo.caption">@context.Item.FirstAmount.ToString("N0") ریال</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">
@(context.Item.FirstPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-")
</MudText>
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="آخرین پکیج">
<CellTemplate>
<MudStack Spacing="0">
<MudText Typo="Typo.body2">@context.Item.LastPackageName</MudText>
<MudText Typo="Typo.caption">@context.Item.LastAmount.ToString("N0") ریال</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">
@(context.Item.LastPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-")
</MudText>
<MudChip T="string" Size="Size.Small" Class="mt-1" Variant="Variant.Filled"
Color="@GetMethodColor(context.Item.LastPurchaseMethod)">
@ReportLabels.GetPurchaseMethodLabel(context.Item.LastPurchaseMethod)
</MudChip>
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="تعداد / جمع">
<CellTemplate>
<MudStack Spacing="0">
<MudText>@context.Item.PurchaseCount خرید</MudText>
<MudText Typo="Typo.caption">@context.Item.TotalAmount.ToString("N0") ریال</MudText>
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="دایا">
<CellTemplate>
<MudText Typo="Typo.caption">@context.Item.DayaCount عدد</MudText>
<MudText Typo="Typo.caption">@context.Item.DayaAmount.ToString("N0")</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="دستی">
<CellTemplate>
<MudText Typo="Typo.caption">@context.Item.ManualCount عدد</MudText>
<MudText Typo="Typo.caption">@context.Item.ManualAmount.ToString("N0")</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="درگاه">
<CellTemplate>
<MudText Typo="Typo.caption">@context.Item.GatewayCount عدد</MudText>
<MudText Typo="Typo.caption">@context.Item.GatewayAmount.ToString("N0")</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="عملیات">
<CellTemplate>
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
OnClick="() => OpenDetails(context.Item)">
جزئیات
</MudButton>
</CellTemplate>
</TemplateColumn>
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="CustomerPackagePurchaseRollupModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد در صفحه" />
</PagerContent>
</MudDataGrid>
</Content>
</BasePageComponent>
@code {
private BasePageComponent? _basePage;
private MudDataGrid<CustomerPackagePurchaseRollupModel>? _grid;
private long? _userId;
private int? _purchaseMethod;
private DateTime? _fromDate, _toDate;
private bool _exporting;
private GetCustomerPackagePurchaseRollupFilter BuildFilter()
{
var filter = new GetCustomerPackagePurchaseRollupFilter();
if (_userId is > 0)
filter.UserId = _userId;
if (_purchaseMethod.HasValue)
filter.PurchaseMethod = _purchaseMethod.Value;
if (_fromDate.HasValue)
filter.PurchasedFrom = Timestamp.FromDateTime(DateTime.SpecifyKind(_fromDate.Value.Date, DateTimeKind.Utc));
if (_toDate.HasValue)
filter.PurchasedTo = Timestamp.FromDateTime(DateTime.SpecifyKind(_toDate.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Utc));
return filter;
}
private async Task<GridData<CustomerPackagePurchaseRollupModel>> LoadData(GridState<CustomerPackagePurchaseRollupModel> state)
{
try
{
var result = await PurchaseClient.GetCustomerPackagePurchaseRollupAsync(
new GetCustomerPackagePurchaseRollupRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = state.Page + 1,
PageSize = state.PageSize
},
Filter = BuildFilter()
});
return new GridData<CustomerPackagePurchaseRollupModel>
{
Items = result?.Models?.ToList() ?? new(),
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
};
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
return new GridData<CustomerPackagePurchaseRollupModel>();
}
}
private async Task OnFilterSubmit()
{
if (_grid != null) await _grid.ReloadServerData();
}
private async Task OnFilterCleared()
{
_userId = null;
_purchaseMethod = null;
_fromDate = _toDate = null;
if (_grid != null) await _grid.ReloadServerData();
}
private async Task OpenDetails(CustomerPackagePurchaseRollupModel row)
{
var parameters = new DialogParameters
{
{ nameof(CustomerPackagePurchaseDetailsDialog.UserId), row.UserId },
{ nameof(CustomerPackagePurchaseDetailsDialog.UserName), row.UserName }
};
var options = new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true, CloseButton = true };
await DialogService.ShowAsync<CustomerPackagePurchaseDetailsDialog>("جزئیات خرید پکیج", parameters, options);
}
private async Task ExportToExcel()
{
_exporting = true;
try
{
var filter = BuildFilter();
var all = new List<CustomerPackagePurchaseRollupModel>();
const int pageSize = 500;
var page = 1;
long totalCount;
do
{
var result = await PurchaseClient.GetCustomerPackagePurchaseRollupAsync(
new GetCustomerPackagePurchaseRollupRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = page,
PageSize = pageSize
},
Filter = filter
});
var batch = result?.Models?.ToList() ?? new();
all.AddRange(batch);
totalCount = result?.MetaData?.TotalCount ?? 0;
page++;
} while (all.Count < totalCount && page <= 50);
if (all.Count == 0)
{
Snackbar.Add("داده‌ای برای خروجی با فیلتر فعلی وجود ندارد.", Severity.Info);
return;
}
var headers = new[]
{
"شناسه کاربر", "نام مشتری", "موبایل",
"اولین پکیج", "مبلغ اولین", "تاریخ اولین",
"آخرین پکیج", "مبلغ آخرین", "تاریخ آخرین", "روش آخرین",
"تعداد خرید", "جمع مبلغ",
"تعداد دایا", "مبلغ دایا",
"تعداد دستی", "مبلغ دستی",
"تعداد درگاه", "مبلغ درگاه"
};
var rows = all.Select(o => new object?[]
{
o.UserId,
o.UserName,
o.UserMobile,
o.FirstPackageName,
o.FirstAmount,
o.FirstPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-",
o.LastPackageName,
o.LastAmount,
o.LastPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-",
ReportLabels.GetPurchaseMethodLabel(o.LastPurchaseMethod),
o.PurchaseCount,
o.TotalAmount,
o.DayaCount,
o.DayaAmount,
o.ManualCount,
o.ManualAmount,
o.GatewayCount,
o.GatewayAmount
});
var filename = $"customer-packages-{DateTime.Now:yyyyMMddHHmmss}.xls";
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
CsvExportHelper.ToExcelXmlBase64("مشتریان پکیج", headers, rows));
Snackbar.Add($"خروجی Excel آماده شد ({all.Count} مشتری).", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
}
finally
{
_exporting = false;
}
}
private static Color GetMethodColor(int method) => method switch
{
1 => Color.Info,
2 => Color.Primary,
3 => Color.Secondary,
_ => Color.Default
};
}
+5
View File
@@ -77,6 +77,11 @@
Icon="@Icons.Material.Filled.Groups"> Icon="@Icons.Material.Filled.Groups">
اعضا و گزارش‌ها اعضا و گزارش‌ها
</MudNavLink> </MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/club/customer-packages"
Icon="@Icons.Material.Filled.PeopleAlt">
گزارش مشتریان پکیج
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix" <MudNavLink Match="NavLinkMatch.Prefix"
Href="/club/features" Href="/club/features"
Icon="@Icons.Material.Filled.Star"> Icon="@Icons.Material.Filled.Star">