feat(backoffice): Add App Version management UI
Build and Deploy to Production / build-and-deploy (push) Successful in 5m47s

- Add IAppVersionService interface and implementation
- Add AppVersions.razor page for managing app versions
- Add AppVersionEditDialog component for editing versions
- Register AppVersion gRPC client and service in DI
- Update Foursat.BackOffice.BFF.Configuration.Protobuf to 1.0.20
This commit is contained in:
masoodafar-web
2025-12-26 06:07:02 +03:30
parent 332fe0112c
commit 6b140c3bd8
6 changed files with 528 additions and 1 deletions
+1 -1
View File
@@ -116,7 +116,7 @@
<PackageReference Include="Foursat.BackOffice.BFF.ClubMembership.Protobuf" Version="0.0.7"/>
<PackageReference Include="Foursat.BackOffice.BFF.Commission.Protobuf" Version="0.0.13"/>
<PackageReference Include="Foursat.BackOffice.BFF.Common.Protobuf" Version="0.0.3"/>
<PackageReference Include="Foursat.BackOffice.BFF.Configuration.Protobuf" Version="1.0.7"/>
<PackageReference Include="Foursat.BackOffice.BFF.Configuration.Protobuf" Version="1.0.20"/>
<PackageReference Include="Foursat.BackOffice.BFF.DiscountCategory.Protobuf" Version="0.0.3"/>
<PackageReference Include="Foursat.BackOffice.BFF.DiscountOrder.Protobuf" Version="0.0.3"/>
<PackageReference Include="Foursat.BackOffice.BFF.DiscountProduct.Protobuf" Version="0.0.3"/>
@@ -12,6 +12,7 @@ using Foursat.BackOffice.BFF.ClubMembership.Protos;
using Foursat.BackOffice.BFF.Configuration.Protos;
using Foursat.BackOffice.BFF.NetworkMembership.Protos;
using Foursat.BackOffice.BFF.Health.Protobuf;
using BackOffice.BFF.Configuration.Protobuf.Protos.AppVersion;
// TODO: Create these proto projects - temporarily disabled
// using BackOffice.BFF.DiscountProduct.Protobuf.Protos.DiscountProduct;
@@ -68,6 +69,7 @@ public static class ConfigureServices
// Application Services
services.AddScoped<BackOffice.Services.Authorization.IAuthorizationService, BackOffice.Services.Authorization.AuthorizationService>();
services.AddScoped<BackOffice.Services.AppVersion.IAppVersionService, BackOffice.Services.AppVersion.AppVersionService>();
// TODO: Re-enable when proto projects are created
// services.AddScoped<IDiscountProductService, DiscountProductService>();
// services.AddScoped<IDiscountCategoryService, DiscountCategoryService>();
@@ -116,6 +118,7 @@ public static class ConfigureServices
services.AddTransient(sp => new ClubMembershipContract.ClubMembershipContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new ConfigurationContract.ConfigurationContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new HealthContract.HealthContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new AppVersionContract.AppVersionContractClient(sp.GetRequiredService<CallInvoker>()));
// TODO: Re-enable when proto projects are created
// Discount Shop Services
@@ -0,0 +1,310 @@
@page "/settings/app-versions"
@attribute [Authorize]
@using BackOffice.Services.AppVersion
@using BackOffice.Pages.Settings.Components
@inject IAppVersionService AppVersionService
@inject IDialogService DialogService
@inject ISnackbar Snackbar
<PageTitle>مدیریت نسخه اپلیکیشن‌ها</PageTitle>
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
<MudText Typo="Typo.h4" GutterBottom="true">
<MudIcon Icon="@Icons.Material.Filled.PhoneAndroid" Class="ml-2" />
مدیریت نسخه اپلیکیشن‌ها
</MudText>
<MudText Typo="Typo.body1" Color="Color.Secondary" Class="mb-4">
مدیریت نسخه‌های اپلیکیشن‌های موبایل و کنترل به‌روزرسانی اجباری
</MudText>
<MudPaper Class="pa-4">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-4">
<MudSwitch Value="_includeInactive"
Color="Color.Primary"
Label="نمایش نسخه‌های غیرفعال"
T="bool"
ValueChanged="@(async (bool val) => { _includeInactive = val; await LoadVersions(); })" />
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Refresh"
OnClick="@LoadVersions">
بارگذاری مجدد
</MudButton>
</MudStack>
@if (_loading)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mb-4" />
}
<MudDataGrid T="AppVersionDto"
Items="@_versions"
Loading="@_loading"
Hover="true"
Dense="true">
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<TemplateColumn Title="اپلیکیشن">
<CellTemplate>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@GetAppIcon(context.Item.AppName)"
Color="@GetAppColor(context.Item.AppName)"
Size="Size.Small" />
<MudText>@context.Item.AppNameDisplay</MudText>
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="نسخه فعلی">
<CellTemplate>
<MudChip T="string" Color="Color.Primary" Size="Size.Small" Variant="Variant.Outlined">
v@context.Item.CurrentVersion
</MudChip>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="حداقل نسخه">
<CellTemplate>
@if (!string.IsNullOrEmpty(context.Item.MinRequiredVersion))
{
<MudChip T="string" Color="Color.Warning" Size="Size.Small" Variant="Variant.Outlined">
v@context.Item.MinRequiredVersion
</MudChip>
}
else
{
<MudText Typo="Typo.body2" Color="Color.Default">-</MudText>
}
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="پاکسازی کش">
<CellTemplate>
@if (context.Item.RequiresFullCacheClear)
{
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Warning" Size="Size.Small" />
}
else
{
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" />
}
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="وضعیت">
<CellTemplate>
@if (context.Item.IsActive)
{
<MudChip T="string" Color="Color.Success" Size="Size.Small">فعال</MudChip>
}
else
{
<MudChip T="string" Color="Color.Default" Size="Size.Small">غیرفعال</MudChip>
}
</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.LastModified" Title="آخرین تغییر" Format="yyyy/MM/dd HH:mm" />
<TemplateColumn Title="عملیات" Sortable="false">
<CellTemplate>
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Color="Color.Primary"
Size="Size.Small"
OnClick="@(() => OpenEditDialog(context.Item))" />
<MudIconButton Icon="@Icons.Material.Filled.Visibility"
Color="Color.Info"
Size="Size.Small"
OnClick="@(() => OpenDetailsDialog(context.Item))" />
</CellTemplate>
</TemplateColumn>
</Columns>
</MudDataGrid>
</MudPaper>
@* Info Cards *@
<MudGrid Class="mt-4">
@foreach (var version in _versions.Where(v => v.IsActive))
{
<MudItem xs="12" md="6">
<MudCard Elevation="2">
<MudCardHeader>
<CardHeaderAvatar>
<MudAvatar Color="@GetAppColor(version.AppName)">
<MudIcon Icon="@GetAppIcon(version.AppName)" />
</MudAvatar>
</CardHeaderAvatar>
<CardHeaderContent>
<MudText Typo="Typo.h6">@version.AppNameDisplay</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">@version.AppName</MudText>
</CardHeaderContent>
<CardHeaderActions>
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Color="Color.Primary"
OnClick="@(() => OpenEditDialog(version))" />
</CardHeaderActions>
</MudCardHeader>
<MudCardContent>
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">نسخه فعلی:</MudText>
<MudChip T="string" Color="Color.Primary" Size="Size.Small">v@version.CurrentVersion</MudChip>
</MudStack>
<MudStack Row="true" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.body2">حداقل نسخه:</MudText>
@if (!string.IsNullOrEmpty(version.MinRequiredVersion))
{
<MudChip T="string" Color="Color.Warning" Size="Size.Small">v@version.MinRequiredVersion</MudChip>
}
else
{
<MudText Typo="Typo.body2" Color="Color.Default">تنظیم نشده</MudText>
}
</MudStack>
@if (version.RequiresFullCacheClear)
{
<MudAlert Severity="Severity.Warning" Dense="true" Class="mt-2">
نیاز به پاکسازی کامل کش دارد
</MudAlert>
}
@if (!string.IsNullOrEmpty(version.UpdateMessage))
{
<MudDivider Class="my-2" />
<MudText Typo="Typo.caption" Color="Color.Secondary">پیام به‌روزرسانی:</MudText>
<MudText Typo="Typo.body2">@version.UpdateMessage</MudText>
}
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
}
</MudGrid>
</MudContainer>
@code {
private List<AppVersionDto> _versions = new();
private bool _loading = false;
private bool _includeInactive = false;
protected override async Task OnInitializedAsync()
{
await LoadVersions();
}
private async Task LoadVersions()
{
_loading = true;
try
{
_versions = await AppVersionService.GetAllAsync(_includeInactive);
Snackbar.Add("اطلاعات نسخه‌ها بارگذاری شد", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
private async Task OpenEditDialog(AppVersionDto version)
{
var parameters = new DialogParameters<AppVersionEditDialog>
{
{ x => x.Model, new UpdateAppVersionDto
{
AppName = version.AppName,
CurrentVersion = version.CurrentVersion,
MinRequiredVersion = version.MinRequiredVersion,
RequiresFullCacheClear = version.RequiresFullCacheClear,
UpdateMessage = version.UpdateMessage,
ReleaseNotes = version.ReleaseNotes
}
}
};
var options = new DialogOptions
{
CloseButton = true,
MaxWidth = MaxWidth.Medium,
FullWidth = true
};
var dialog = await DialogService.ShowAsync<AppVersionEditDialog>(
$"ویرایش نسخه {version.AppNameDisplay}",
parameters,
options);
var result = await dialog.Result;
if (result is { Canceled: false, Data: UpdateAppVersionDto dto })
{
try
{
await AppVersionService.UpdateAsync(dto);
Snackbar.Add("نسخه با موفقیت به‌روزرسانی شد", Severity.Success);
await LoadVersions();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در به‌روزرسانی: {ex.Message}", Severity.Error);
}
}
}
private void OpenDetailsDialog(AppVersionDto version)
{
var parameters = new DialogParameters
{
{ "ContentText", BuildDetailsContent(version) },
{ "ButtonText", "بستن" },
{ "Color", Color.Primary }
};
DialogService.Show<MudMessageBox>($"جزئیات {version.AppNameDisplay}", parameters);
}
private string BuildDetailsContent(AppVersionDto version)
{
var lines = new List<string>
{
$"نام اپلیکیشن: {version.AppName}",
$"نسخه فعلی: {version.CurrentVersion}",
$"حداقل نسخه: {version.MinRequiredVersion ?? "تنظیم نشده"}",
$"نیاز به پاکسازی کش: {(version.RequiresFullCacheClear ? "بله" : "خیر")}",
$"وضعیت: {(version.IsActive ? "فعال" : "غیرفعال")}",
"",
"پیام به‌روزرسانی:",
version.UpdateMessage ?? "ندارد",
"",
"یادداشت‌های انتشار:",
version.ReleaseNotes ?? "ندارد",
"",
$"تاریخ ایجاد: {version.Created?.ToString("yyyy/MM/dd HH:mm") ?? "-"}",
$"آخرین تغییر: {version.LastModified?.ToString("yyyy/MM/dd HH:mm") ?? "-"}"
};
return string.Join(Environment.NewLine, lines);
}
private string GetAppIcon(string appName) => appName switch
{
"FoursatMarketApp" => Icons.Material.Filled.ShoppingCart,
"FoursatClubApp" => Icons.Material.Filled.Groups,
_ => Icons.Material.Filled.PhoneAndroid
};
private Color GetAppColor(string appName) => appName switch
{
"FoursatMarketApp" => Color.Primary,
"FoursatClubApp" => Color.Secondary,
_ => Color.Default
};
}
@@ -0,0 +1,83 @@
@using BackOffice.Services.AppVersion
@inject ISnackbar Snackbar
<MudDialog>
<DialogContent>
<MudForm @ref="_form" @bind-IsValid="_isValid">
<MudStack Spacing="3">
<MudTextField @bind-Value="Model.AppName"
Label="نام اپلیکیشن"
Variant="Variant.Outlined"
ReadOnly="true"
Disabled="true" />
<MudTextField @bind-Value="Model.CurrentVersion"
Label="نسخه فعلی"
Variant="Variant.Outlined"
Required="true"
RequiredError="نسخه فعلی اجباری است"
HelperText="مثال: 1.2.0" />
<MudTextField @bind-Value="Model.MinRequiredVersion"
Label="حداقل نسخه مورد نیاز"
Variant="Variant.Outlined"
HelperText="کاربران با نسخه‌های پایین‌تر مجبور به آپدیت می‌شوند" />
<MudSwitch @bind-Value="Model.RequiresFullCacheClear"
Color="Color.Warning"
Label="نیاز به پاکسازی کامل کش" />
<MudTextField @bind-Value="Model.UpdateMessage"
Label="پیام به‌روزرسانی"
Variant="Variant.Outlined"
Lines="2"
HelperText="پیامی که به کاربر نمایش داده می‌شود" />
<MudTextField @bind-Value="Model.ReleaseNotes"
Label="یادداشت‌های انتشار"
Variant="Variant.Outlined"
Lines="4"
HelperText="توضیحات تغییرات این نسخه" />
<MudDivider Class="my-2" />
<MudTextField @bind-Value="Model.UpdateReason"
Label="دلیل تغییر (برای لاگ)"
Variant="Variant.Outlined"
Required="true"
RequiredError="دلیل تغییر اجباری است"
Lines="2"
HelperText="این متن در لاگ ذخیره می‌شود" />
</MudStack>
</MudForm>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel" Color="Color.Default" Variant="Variant.Text">
انصراف
</MudButton>
<MudButton OnClick="Submit" Color="Color.Primary" Variant="Variant.Filled" Disabled="@(!_isValid)">
ذخیره
</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter]
private IMudDialogInstance MudDialog { get; set; } = null!;
[Parameter]
public UpdateAppVersionDto Model { get; set; } = new();
private MudForm? _form;
private bool _isValid;
private void Cancel() => MudDialog.Cancel();
private void Submit()
{
if (_isValid)
{
MudDialog.Close(DialogResult.Ok(Model));
}
}
}
@@ -0,0 +1,90 @@
using BackOffice.BFF.Configuration.Protobuf.Protos.AppVersion;
using Google.Protobuf.WellKnownTypes;
namespace BackOffice.Services.AppVersion;
public class AppVersionService : IAppVersionService
{
private readonly AppVersionContract.AppVersionContractClient _client;
public AppVersionService(AppVersionContract.AppVersionContractClient client)
{
_client = client;
}
public async Task<List<AppVersionDto>> GetAllAsync(bool includeInactive = false)
{
var request = new GetAllAppVersionsRequest
{
IncludeInactive = includeInactive
};
var response = await _client.GetAllAppVersionsAsync(request);
return response.Items.Select(item => new AppVersionDto
{
Id = item.Id,
AppName = item.AppName,
CurrentVersion = item.CurrentVersion,
MinRequiredVersion = item.MinRequiredVersion,
RequiresFullCacheClear = item.RequiresFullCacheClear,
UpdateMessage = item.UpdateMessage,
ReleaseNotes = item.ReleaseNotes,
IsActive = item.IsActive,
Created = item.Created?.ToDateTime(),
LastModified = item.LastModified?.ToDateTime()
}).ToList();
}
public async Task<AppVersionDto?> GetByNameAsync(string appName)
{
var request = new GetAppVersionRequest
{
AppName = appName
};
var response = await _client.GetAppVersionAsync(request);
if (!response.Found || response.Item == null)
return null;
var item = response.Item;
return new AppVersionDto
{
Id = item.Id,
AppName = item.AppName,
CurrentVersion = item.CurrentVersion,
MinRequiredVersion = item.MinRequiredVersion,
RequiresFullCacheClear = item.RequiresFullCacheClear,
UpdateMessage = item.UpdateMessage,
ReleaseNotes = item.ReleaseNotes,
IsActive = item.IsActive,
Created = item.Created?.ToDateTime(),
LastModified = item.LastModified?.ToDateTime()
};
}
public async Task UpdateAsync(UpdateAppVersionDto dto)
{
var request = new UpdateAppVersionRequest
{
AppName = dto.AppName,
CurrentVersion = dto.CurrentVersion,
RequiresFullCacheClear = dto.RequiresFullCacheClear
};
if (!string.IsNullOrWhiteSpace(dto.MinRequiredVersion))
request.MinRequiredVersion = dto.MinRequiredVersion;
if (!string.IsNullOrWhiteSpace(dto.UpdateMessage))
request.UpdateMessage = dto.UpdateMessage;
if (!string.IsNullOrWhiteSpace(dto.ReleaseNotes))
request.ReleaseNotes = dto.ReleaseNotes;
if (!string.IsNullOrWhiteSpace(dto.UpdateReason))
request.UpdateReason = dto.UpdateReason;
await _client.UpdateAppVersionAsync(request);
}
}
@@ -0,0 +1,41 @@
namespace BackOffice.Services.AppVersion;
public interface IAppVersionService
{
Task<List<AppVersionDto>> GetAllAsync(bool includeInactive = false);
Task<AppVersionDto?> GetByNameAsync(string appName);
Task UpdateAsync(UpdateAppVersionDto dto);
}
public class AppVersionDto
{
public long Id { get; set; }
public string AppName { get; set; } = string.Empty;
public string AppNameDisplay => GetDisplayName(AppName);
public string CurrentVersion { get; set; } = string.Empty;
public string MinRequiredVersion { get; set; } = string.Empty;
public bool RequiresFullCacheClear { get; set; }
public string UpdateMessage { get; set; } = string.Empty;
public string ReleaseNotes { get; set; } = string.Empty;
public bool IsActive { get; set; }
public DateTime? Created { get; set; }
public DateTime? LastModified { get; set; }
private static string GetDisplayName(string appName) => appName switch
{
"FoursatMarketApp" => "اپ مارکت فورست",
"FoursatClubApp" => "اپ باشگاه فورست",
_ => appName
};
}
public class UpdateAppVersionDto
{
public string AppName { get; set; } = string.Empty;
public string CurrentVersion { get; set; } = string.Empty;
public string? MinRequiredVersion { get; set; }
public bool RequiresFullCacheClear { get; set; }
public string? UpdateMessage { get; set; }
public string? ReleaseNotes { get; set; }
public string? UpdateReason { get; set; }
}