Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Failing after 6s
Build and Deploy to Production / build-and-deploy (push) Failing after 6s
Features merged: - Discount shop management (CRUD, inventory, bulk edit) - Blog and content management improvements - Docker build fixes (NuGet package reference) - UI improvements across multiple components Production workflow and Dockerfile preserved.
This commit is contained in:
@@ -1,131 +0,0 @@
|
||||
@page "/payment/membership"
|
||||
@using BackOffice.BFF.ManualPayment.Protobuf
|
||||
@using BackOffice.Main.Pages.AutoComplete
|
||||
@attribute [Authorize(Roles = "Admin,SuperAdmin")]
|
||||
|
||||
<PageTitle>پرداخت دستی عضویت</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
|
||||
<MudCard>
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
<MudText Typo="Typo.h5">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Payment" Class="ml-2" />
|
||||
پرداخت دستی عضویت
|
||||
</MudText>
|
||||
</CardHeaderContent>
|
||||
</MudCardHeader>
|
||||
|
||||
<MudCardContent>
|
||||
<MudGrid>
|
||||
<!-- انتخاب کاربر -->
|
||||
<MudItem xs="12" md="6">
|
||||
<UserAutoComplete
|
||||
Label="انتخاب کاربر *"
|
||||
@bind-SelectedUserId="_userId"
|
||||
HelperText="جستجو با نام، موبایل یا شناسه" />
|
||||
</MudItem>
|
||||
|
||||
<!-- مبلغ -->
|
||||
<MudItem xs="12" md="6">
|
||||
<MudNumericField
|
||||
@bind-Value="_amount"
|
||||
Label="مبلغ (ریال) *"
|
||||
Min="0"
|
||||
Step="10000"
|
||||
Format="N0"
|
||||
Variant="Variant.Outlined"
|
||||
HelperText="مبلغ پرداختی به ریال" />
|
||||
</MudItem>
|
||||
|
||||
<!-- شماره مرجع -->
|
||||
<MudItem xs="12" md="6">
|
||||
<MudTextField
|
||||
@bind-Value="_referenceNumber"
|
||||
Label="شماره مرجع *"
|
||||
Variant="Variant.Outlined"
|
||||
MaxLength="50"
|
||||
HelperText="شماره مرجع تراکنش یا فیش" />
|
||||
</MudItem>
|
||||
|
||||
<!-- توضیحات -->
|
||||
<MudItem xs="12" md="6">
|
||||
<MudTextField
|
||||
@bind-Value="_description"
|
||||
Label="توضیحات"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="3"
|
||||
MaxLength="500"
|
||||
HelperText="توضیحات اختیاری" />
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
@if (_showResult)
|
||||
{
|
||||
<MudAlert Severity="Severity.Success" Class="mt-4" ShowCloseIcon CloseIconClicked="@(() => _showResult = false)">
|
||||
<MudText>@_resultMessage</MudText>
|
||||
<MudText Typo="Typo.caption">شماره تراکنش: @_transactionId</MudText>
|
||||
<MudText Typo="Typo.caption">شماره سفارش: @_orderId</MudText>
|
||||
<MudText Typo="Typo.caption">موجودی جدید کیف پول: @_newWalletBalance.ToString("N0") ریال</MudText>
|
||||
</MudAlert>
|
||||
}
|
||||
</MudCardContent>
|
||||
|
||||
<MudCardActions>
|
||||
<MudButton
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Check"
|
||||
OnClick="ProcessPayment"
|
||||
Disabled="@_isProcessing">
|
||||
@if (_isProcessing)
|
||||
{
|
||||
<MudProgressCircular Class="ml-2" Size="Size.Small" Indeterminate="true" />
|
||||
<MudText>در حال ثبت...</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText>ثبت پرداخت</MudText>
|
||||
}
|
||||
</MudButton>
|
||||
|
||||
<MudButton
|
||||
Variant="Variant.Outlined"
|
||||
Color="Color.Default"
|
||||
StartIcon="@Icons.Material.Filled.Clear"
|
||||
OnClick="ResetForm"
|
||||
Disabled="@_isProcessing">
|
||||
پاک کردن فرم
|
||||
</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
|
||||
<!-- راهنما -->
|
||||
<MudCard Class="mt-4">
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
<MudText Typo="Typo.h6">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Info" Class="ml-2" />
|
||||
راهنما
|
||||
</MudText>
|
||||
</CardHeaderContent>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudText Typo="Typo.body2" Class="mb-2">این صفحه برای ثبت پرداختهای دستی عضویت استفاده میشود. پس از ثبت:</MudText>
|
||||
<MudList Dense="true">
|
||||
<MudListItem Icon="@Icons.Material.Filled.AccountBalance">
|
||||
<MudText Typo="Typo.body2">مبلغ به کیف پول کاربر (Balance و DiscountBalance) اضافه میشود</MudText>
|
||||
</MudListItem>
|
||||
<MudListItem Icon="@Icons.Material.Filled.History">
|
||||
<MudText Typo="Typo.body2">لاگ تغییرات کیف پول ثبت میشود</MudText>
|
||||
</MudListItem>
|
||||
<MudListItem Icon="@Icons.Material.Filled.Receipt">
|
||||
<MudText Typo="Typo.body2">تراکنش با وضعیت موفق ثبت میشود</MudText>
|
||||
</MudListItem>
|
||||
<MudListItem Icon="@Icons.Material.Filled.ShoppingCart">
|
||||
<MudText Typo="Typo.body2">سفارش خرید پکیج عضویت ثبت میشود</MudText>
|
||||
</MudListItem>
|
||||
</MudList>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudContainer>
|
||||
@@ -1,103 +0,0 @@
|
||||
using BackOffice.BFF.ManualPayment.Protobuf;
|
||||
using BackOffice.Main.Components;
|
||||
using Grpc.Core;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace BackOffice.Main.Pages.Payment;
|
||||
|
||||
public partial class ManualMembershipPayment
|
||||
{
|
||||
[Inject] private ManualPaymentContract.ManualPaymentContractClient ManualPaymentClient { get; set; } = default!;
|
||||
[Inject] private ISnackbar Snackbar { get; set; } = default!;
|
||||
|
||||
private long? _userId;
|
||||
private long _amount = 0;
|
||||
private string _referenceNumber = string.Empty;
|
||||
private string? _description;
|
||||
|
||||
private bool _isProcessing = false;
|
||||
private bool _showResult = false;
|
||||
private string _resultMessage = string.Empty;
|
||||
private long _transactionId = 0;
|
||||
private long _orderId = 0;
|
||||
private long _newWalletBalance = 0;
|
||||
|
||||
private async Task ProcessPayment()
|
||||
{
|
||||
// Validation
|
||||
if (!_userId.HasValue || _userId.Value <= 0)
|
||||
{
|
||||
Snackbar.Add("لطفا کاربر را انتخاب کنید", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_amount <= 0)
|
||||
{
|
||||
Snackbar.Add("لطفا مبلغ معتبری وارد کنید", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_referenceNumber))
|
||||
{
|
||||
Snackbar.Add("لطفا شماره مرجع را وارد کنید", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_isProcessing = true;
|
||||
_showResult = false;
|
||||
|
||||
var request = new ProcessManualMembershipPaymentRequest
|
||||
{
|
||||
UserId = _userId.Value,
|
||||
Amount = _amount,
|
||||
ReferenceNumber = _referenceNumber
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_description))
|
||||
{
|
||||
request.Description = _description;
|
||||
}
|
||||
|
||||
var response = await ManualPaymentClient.ProcessManualMembershipPaymentAsync(request);
|
||||
|
||||
_resultMessage = response.Message;
|
||||
_transactionId = response.TransactionId;
|
||||
_orderId = response.OrderId;
|
||||
_newWalletBalance = response.NewWalletBalance;
|
||||
_showResult = true;
|
||||
|
||||
Snackbar.Add("پرداخت دستی با موفقیت ثبت شد", Severity.Success);
|
||||
|
||||
// Reset form
|
||||
await Task.Delay(2000);
|
||||
ResetForm();
|
||||
}
|
||||
catch (RpcException ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ثبت پرداخت: {ex.Status.Detail}", Severity.Error);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطای غیرمنتظره: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetForm()
|
||||
{
|
||||
_userId = null;
|
||||
_amount = 0;
|
||||
_referenceNumber = string.Empty;
|
||||
_description = null;
|
||||
_showResult = false;
|
||||
_resultMessage = string.Empty;
|
||||
_transactionId = 0;
|
||||
_orderId = 0;
|
||||
_newWalletBalance = 0;
|
||||
}
|
||||
}
|
||||
@@ -109,71 +109,144 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
|
||||
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0"/>
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.Category.Protobuf" Version="0.0.5"/>
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.ClubMembership.Protobuf" Version="0.0.7"/>
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.Commission.Protobuf" Version="0.0.15"/>
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.Common.Protobuf" Version="0.0.3"/>
|
||||
<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"/>
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.DiscountShoppingCart.Protobuf" Version="0.0.3"/>
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.Health.Protobuf" Version="1.0.5"/>
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.ManualPayment.Protobuf" Version="0.0.5"/>
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.NetworkMembership.Protobuf" Version="0.0.11"/>
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.Otp.Protobuf" Version="0.0.113"/>
|
||||
<PackageReference Include="FourSat.BackOffice.BFF.Package.Protobuf" Version="0.0.113"/>
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.Products.Protobuf" Version="0.0.11"/>
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.ProductTag.Protobuf" Version="0.0.3"/>
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.PublicMessage.Protobuf" Version="0.0.5"/>
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.Role.Protobuf" Version="0.0.113"/>
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.Tag.Protobuf" Version="0.0.4"/>
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.User.Protobuf" Version="0.0.114"/>
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.UserAddress.Protobuf" Version="0.0.113"/>
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.UserOrder.Protobuf" Version="0.0.116"/>
|
||||
<PackageReference Include="Foursat.BackOffice.BFF.UserRole.Protobuf" Version="0.0.113"/>
|
||||
|
||||
<!-- Proto Projects - Using ProjectReference for latest changes -->
|
||||
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Category.Protobuf/BackOffice.BFF.Category.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.ClubMembership.Protobuf/BackOffice.BFF.ClubMembership.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Commission.Protobuf/BackOffice.BFF.Commission.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Common.Protobuf/BackOffice.BFF.Common.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Configuration.Protobuf/BackOffice.BFF.Configuration.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.DiscountCategory.Protobuf/BackOffice.BFF.DiscountCategory.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.DiscountOrder.Protobuf/BackOffice.BFF.DiscountOrder.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.DiscountProduct.Protobuf/BackOffice.BFF.DiscountProduct.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.DiscountShoppingCart.Protobuf/BackOffice.BFF.DiscountShoppingCart.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Health.Protobuf/BackOffice.BFF.Health.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.ManualPayment.Protobuf/BackOffice.BFF.ManualPayment.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.NetworkMembership.Protobuf/BackOffice.BFF.NetworkMembership.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Otp.Protobuf/BackOffice.BFF.Otp.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Package.Protobuf/BackOffice.BFF.Package.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Products.Protobuf/BackOffice.BFF.Products.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.ProductTag.Protobuf/BackOffice.BFF.ProductTag.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.PublicMessage.Protobuf/BackOffice.BFF.PublicMessage.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Role.Protobuf/BackOffice.BFF.Role.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Tag.Protobuf/BackOffice.BFF.Tag.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.User.Protobuf/BackOffice.BFF.User.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.UserAddress.Protobuf/BackOffice.BFF.UserAddress.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.UserOrder.Protobuf/BackOffice.BFF.UserOrder.Protobuf.csproj" />-->
|
||||
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.UserRole.Protobuf/BackOffice.BFF.UserRole.Protobuf.csproj" />-->
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.1"/>
|
||||
<PackageReference Include="DateTimeConverterCL" Version="1.0.0"/>
|
||||
<PackageReference Include="Grpc.Core" Version="2.46.6"/>
|
||||
<PackageReference Include="Grpc.Net.Client" Version="2.71.0"/>
|
||||
<PackageReference Include="Grpc.Net.Client.Web" Version="2.71.0"/>
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.1"/>
|
||||
<PackageReference Include="Mapster" Version="7.4.0"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.11"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.11"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.11" PrivateAssets="all"/>
|
||||
|
||||
<PackageReference Include="MudBlazor" Version="8.14.0"/>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4"/>
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.8.0"/>
|
||||
<PackageReference Include="Tizzani.MudBlazor.HtmlEditor" Version="2.3.0"/>
|
||||
|
||||
<!-- Proto dependencies (needed when using DLL references) -->
|
||||
<PackageReference Include="Google.Protobuf" Version="3.28.3"/>
|
||||
<PackageReference Include="Google.Api.CommonProtos" Version="2.10.0"/>
|
||||
<PackageReference Include="FluentValidation" Version="11.2.2"/>
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.2.2"/>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Proto References: Direct reference to CMS Protobuf project -->
|
||||
<!-- This enables direct communication with CMS (no BFF needed) -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<!-- CMS Protobuf NuGet package (used for both local dev and Docker builds) -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.178" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- OLD BFF Proto References (DEPRECATED - kept for reference) -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<!-- CI/CD Mode: Use pre-built DLLs from libs/ folder -->
|
||||
<ItemGroup Condition="false AND Exists('../../libs/BackOffice.BFF.Common.Protobuf.dll')">
|
||||
<Reference Include="BackOffice.BFF.Common.Protobuf">
|
||||
<HintPath>../../libs/BackOffice.BFF.Common.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="BackOffice.BFF.Category.Protobuf">
|
||||
<HintPath>../../libs/BackOffice.BFF.Category.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="BackOffice.BFF.ClubMembership.Protobuf">
|
||||
<HintPath>../../libs/BackOffice.BFF.ClubMembership.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="BackOffice.BFF.Commission.Protobuf">
|
||||
<HintPath>../../libs/BackOffice.BFF.Commission.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="BackOffice.BFF.Configuration.Protobuf">
|
||||
<HintPath>../../libs/BackOffice.BFF.Configuration.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="BackOffice.BFF.DiscountCategory.Protobuf">
|
||||
<HintPath>../../libs/BackOffice.BFF.DiscountCategory.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="BackOffice.BFF.DiscountOrder.Protobuf">
|
||||
<HintPath>../../libs/BackOffice.BFF.DiscountOrder.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="BackOffice.BFF.DiscountProduct.Protobuf">
|
||||
<HintPath>../../libs/BackOffice.BFF.DiscountProduct.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="BackOffice.BFF.DiscountShoppingCart.Protobuf">
|
||||
<HintPath>../../libs/BackOffice.BFF.DiscountShoppingCart.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="BackOffice.BFF.Health.Protobuf">
|
||||
<HintPath>../../libs/BackOffice.BFF.Health.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="BackOffice.BFF.Inventory.Protobuf">
|
||||
<HintPath>../../libs/BackOffice.BFF.Inventory.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="BackOffice.BFF.ManualPayment.Protobuf">
|
||||
<HintPath>../../libs/BackOffice.BFF.ManualPayment.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="BackOffice.BFF.NetworkMembership.Protobuf">
|
||||
<HintPath>../../libs/BackOffice.BFF.NetworkMembership.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="BackOffice.BFF.Otp.Protobuf">
|
||||
<HintPath>../../libs/BackOffice.BFF.Otp.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="BackOffice.BFF.Package.Protobuf">
|
||||
<HintPath>../../libs/BackOffice.BFF.Package.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="BackOffice.BFF.Products.Protobuf">
|
||||
<HintPath>../../libs/BackOffice.BFF.Products.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="BackOffice.BFF.ProductTag.Protobuf">
|
||||
<HintPath>../../libs/BackOffice.BFF.ProductTag.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="BackOffice.BFF.PublicMessage.Protobuf">
|
||||
<HintPath>../../libs/BackOffice.BFF.PublicMessage.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="BackOffice.BFF.Role.Protobuf">
|
||||
<HintPath>../../libs/BackOffice.BFF.Role.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="BackOffice.BFF.Tag.Protobuf">
|
||||
<HintPath>../../libs/BackOffice.BFF.Tag.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="BackOffice.BFF.User.Protobuf">
|
||||
<HintPath>../../libs/BackOffice.BFF.User.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="BackOffice.BFF.UserAddress.Protobuf">
|
||||
<HintPath>../../libs/BackOffice.BFF.UserAddress.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="BackOffice.BFF.UserOrder.Protobuf">
|
||||
<HintPath>../../libs/BackOffice.BFF.UserOrder.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="BackOffice.BFF.UserRole.Protobuf">
|
||||
<HintPath>../../libs/BackOffice.BFF.UserRole.Protobuf.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Dev Mode: OLD BFF ProjectReference (DEPRECATED) -->
|
||||
<ItemGroup Condition="false AND !Exists('../../libs/BackOffice.BFF.Common.Protobuf.dll')">
|
||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Common.Protobuf/BackOffice.BFF.Common.Protobuf.csproj" />
|
||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Category.Protobuf/BackOffice.BFF.Category.Protobuf.csproj" />
|
||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.ClubMembership.Protobuf/BackOffice.BFF.ClubMembership.Protobuf.csproj" />
|
||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Commission.Protobuf/BackOffice.BFF.Commission.Protobuf.csproj" />
|
||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Configuration.Protobuf/BackOffice.BFF.Configuration.Protobuf.csproj" />
|
||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.DiscountCategory.Protobuf/BackOffice.BFF.DiscountCategory.Protobuf.csproj" />
|
||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.DiscountOrder.Protobuf/BackOffice.BFF.DiscountOrder.Protobuf.csproj" />
|
||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.DiscountProduct.Protobuf/BackOffice.BFF.DiscountProduct.Protobuf.csproj" />
|
||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.DiscountShoppingCart.Protobuf/BackOffice.BFF.DiscountShoppingCart.Protobuf.csproj" />
|
||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Health.Protobuf/BackOffice.BFF.Health.Protobuf.csproj" />
|
||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Inventory.Protobuf/BackOffice.BFF.Inventory.Protobuf.csproj" />
|
||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.ManualPayment.Protobuf/BackOffice.BFF.ManualPayment.Protobuf.csproj" />
|
||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.NetworkMembership.Protobuf/BackOffice.BFF.NetworkMembership.Protobuf.csproj" />
|
||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Otp.Protobuf/BackOffice.BFF.Otp.Protobuf.csproj" />
|
||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Package.Protobuf/BackOffice.BFF.Package.Protobuf.csproj" />
|
||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Products.Protobuf/BackOffice.BFF.Products.Protobuf.csproj" />
|
||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.ProductTag.Protobuf/BackOffice.BFF.ProductTag.Protobuf.csproj" />
|
||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.PublicMessage.Protobuf/BackOffice.BFF.PublicMessage.Protobuf.csproj" />
|
||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Role.Protobuf/BackOffice.BFF.Role.Protobuf.csproj" />
|
||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Tag.Protobuf/BackOffice.BFF.Tag.Protobuf.csproj" />
|
||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.User.Protobuf/BackOffice.BFF.User.Protobuf.csproj" />
|
||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.UserAddress.Protobuf/BackOffice.BFF.UserAddress.Protobuf.csproj" />
|
||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.UserOrder.Protobuf/BackOffice.BFF.UserOrder.Protobuf.csproj" />
|
||||
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.UserRole.Protobuf/BackOffice.BFF.UserRole.Protobuf.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,34 +1,54 @@
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Class="pa-2 pr-4 rounded" Style="height:85vh;">
|
||||
<MudItem xs="2">
|
||||
<MudPaper Height="85vh" Style="overflow:hidden; position:relative;" Outlined="true" Class="rounded-lg">
|
||||
<MudStack Class="pa-5" Style="height:100%;">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.subtitle2">فیلتر ها</MudText>
|
||||
<MudSpacer />
|
||||
@if (IsFiltered)
|
||||
{
|
||||
<MudButton Color="Color.Error" Variant="Variant.Text" Size="Size.Small" OnClick="@(async () => await OnClearFilterClick.InvokeAsync())">حذف فیلتر</MudButton>
|
||||
}
|
||||
</MudStack>
|
||||
<MudItem Style="height:100%;">
|
||||
<MudStack StretchItems="StretchItems.None" Class="filter-item" Style="height:100%;">
|
||||
<MudStack>
|
||||
@Filters
|
||||
</MudStack>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Size="Size.Small" FullWidth="true" Class="mt-auto" OnClick="@(async () => await OnSubmitClick.InvokeAsync())">جستجو</MudButton>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
@* ── Responsive base page: filter above grid, collapsible ── *@
|
||||
<MudStack Spacing="0" Class="base-page">
|
||||
|
||||
@* ── Header row: page title + filter toggle ── *@
|
||||
@if (Filters != null)
|
||||
{
|
||||
<MudPaper Outlined="true" Class="rounded-lg mb-3 base-page__filter-panel">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Class="pa-3 cursor-pointer"
|
||||
@onclick="ToggleFilterPanel">
|
||||
<MudIcon Icon="@Icons.Material.Filled.FilterList" Size="Size.Small"
|
||||
Color="@(IsFiltered ? Color.Primary : Color.Default)" />
|
||||
<MudText Typo="Typo.subtitle2" Class="mx-1">فیلترها</MudText>
|
||||
@if (IsFiltered)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Primary" Size="Size.Small" Variant="Variant.Text">فعال</MudChip>
|
||||
}
|
||||
<MudSpacer />
|
||||
<MudIcon Icon="@(_filterOpen ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)"
|
||||
Size="Size.Small" />
|
||||
</MudStack>
|
||||
|
||||
@if (_filterOpen)
|
||||
{
|
||||
<MudDivider />
|
||||
<div class="pa-3">
|
||||
<MudGrid Spacing="2">
|
||||
@Filters
|
||||
</MudGrid>
|
||||
<MudStack Row="true" Justify="Justify.FlexEnd" Class="mt-3" Spacing="2">
|
||||
@if (IsFiltered)
|
||||
{
|
||||
<MudButton Color="Color.Error" Variant="Variant.Text" Size="Size.Small"
|
||||
OnClick="@(async () => await OnClearFilterClick.InvokeAsync())">
|
||||
پاک کردن فیلتر
|
||||
</MudButton>
|
||||
}
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.Search"
|
||||
OnClick="@(async () => await OnSubmitClick.InvokeAsync())">
|
||||
جستجو
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</div>
|
||||
}
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
</MudItem>
|
||||
<MudItem xs="10">
|
||||
<MudPaper Height="85vh" Style="overflow:hidden; position:relative;" Outlined="true" Class="rounded-lg">
|
||||
@* ── Content area (DataGrid or custom layout) ── *@
|
||||
<MudPaper Outlined="true" Class="rounded-lg base-page__content" Style="overflow:hidden; position:relative;">
|
||||
@Content
|
||||
</MudPaper>
|
||||
|
||||
@Content
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudStack>
|
||||
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace BackOffice.Common.BaseComponents;
|
||||
|
||||
public partial class BasePageComponent
|
||||
{
|
||||
[Parameter] public RenderFragment Filters { get; set; } = default!;
|
||||
[Parameter] public RenderFragment? Filters { get; set; }
|
||||
[Parameter] public RenderFragment Content { get; set; } = default!;
|
||||
[Parameter] public bool IsFiltered { get; set; }
|
||||
[Parameter] public EventCallback OnSubmitClick { get; set; }
|
||||
[Parameter] public EventCallback OnClearFilterClick { get; set; }
|
||||
|
||||
/// <summary>Whether the filter panel is expanded.</summary>
|
||||
private bool _filterOpen;
|
||||
|
||||
private void ToggleFilterPanel() => _filterOpen = !_filterOpen;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/* ── BasePageComponent scoped styles ── */
|
||||
|
||||
.base-page {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.base-page__filter-panel {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.base-page__content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
@@ -7,6 +7,6 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudImage Src="@src" Alt="@Alt" Elevation="@Elevation" Class="@Class" Fluid="@Fluid" Height="@Height" ObjectFit="@ObjectFit" ObjectPosition="@ObjectPosition" Style="@(Style + ";cursor:pointer;")" Tag="@Tag" Width="@Width" @onclick="async()=>await OnClick.InvokeAsync()" />
|
||||
<MudImage Src="@src" Alt="@Alt" Elevation="@Elevation" Class="@Class" Fluid="@Fluid" Height="@Height" ObjectFit="@ObjectFit" ObjectPosition="@ObjectPosition" Style="@Style" Tag="@Tag" Width="@Width" @onclick="async()=>await OnClick.InvokeAsync()" />
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Common.BaseComponents
|
||||
@@ -13,13 +14,24 @@ namespace BackOffice.Common.BaseComponents
|
||||
{
|
||||
private string src = string.Empty;
|
||||
[Parameter] public EventCallback OnClick { get; set; }
|
||||
[Inject] private IConfiguration Configuration { get; set; } = default!;
|
||||
|
||||
private string FileBaseUrl => Configuration["GwUrl"]?.TrimEnd('/') ?? "";
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
await base.OnParametersSetAsync();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Src))
|
||||
{
|
||||
src = Src.Contains("data:") ? Src : "https://dl.afrino.co" + Src;
|
||||
if (Src.Contains("data:") || Src.StartsWith("http"))
|
||||
src = Src;
|
||||
else
|
||||
src = FileBaseUrl + "/uploads/" + Src.TrimStart('/');
|
||||
}
|
||||
else
|
||||
{
|
||||
src = string.Empty;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,32 +1,43 @@
|
||||
using BackOffice.BFF.Otp.Protobuf.Protos.Otp;
|
||||
using BackOffice.BFF.Package.Protobuf.Protos.Package;
|
||||
using BackOffice.BFF.Role.Protobuf.Protos.Role;
|
||||
using BackOffice.BFF.Products.Protobuf.Protos.Products;
|
||||
using BackOffice.BFF.User.Protobuf.Protos.User;
|
||||
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 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;
|
||||
using BackOffice.BFF.Configuration.Protobuf.Protos.AppVersion;
|
||||
using CMSMicroservice.Protobuf.Protos.OtpToken;
|
||||
using CMSMicroservice.Protobuf.Protos.Package;
|
||||
using CMSMicroservice.Protobuf.Protos.Role;
|
||||
using CMSMicroservice.Protobuf.Protos.Products;
|
||||
using CMSMicroservice.Protobuf.Protos.User;
|
||||
using CMSMicroservice.Protobuf.Protos.UserAddress;
|
||||
using CMSMicroservice.Protobuf.Protos.UserOrder;
|
||||
using CMSMicroservice.Protobuf.Protos.UserRole;
|
||||
using CMSMicroservice.Protobuf.Protos.Category;
|
||||
using CMSMicroservice.Protobuf.Protos.Commission;
|
||||
using CMSMicroservice.Protobuf.Protos.ClubMembership;
|
||||
using CMSMicroservice.Protobuf.Protos.Configuration;
|
||||
using CMSMicroservice.Protobuf.Protos.NetworkMembership;
|
||||
using CMSMicroservice.Protobuf.Protos.Health;
|
||||
using CMSMicroservice.Protobuf.Protos.AppVersion;
|
||||
|
||||
// TODO: Create these proto projects - temporarily disabled
|
||||
// using BackOffice.BFF.DiscountProduct.Protobuf.Protos.DiscountProduct;
|
||||
// using BackOffice.BFF.DiscountCategory.Protobuf.Protos.DiscountCategory;
|
||||
// using BackOffice.BFF.DiscountOrder.Protobuf.Protos.DiscountOrder;
|
||||
// using BackOffice.BFF.Tag.Protobuf.Protos.Tag;
|
||||
// using BackOffice.BFF.ProductTag.Protobuf.Protos.ProductTag;
|
||||
// using BackOffice.BFF.PublicMessage.Protobuf.Protos.PublicMessage;
|
||||
// Discount Shop Proto Clients
|
||||
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
|
||||
using CMSMicroservice.Protobuf.Protos.DiscountCategory;
|
||||
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
|
||||
using CMSMicroservice.Protobuf.Protos.Tag;
|
||||
using CMSMicroservice.Protobuf.Protos.ProductTag;
|
||||
using CMSMicroservice.Protobuf.Protos.ManualPayment;
|
||||
using CMSMicroservice.Protobuf.Protos;
|
||||
using CMSMicroservice.Protobuf.Protos.Inventory;
|
||||
using CMSMicroservice.Protobuf.Protos.BlogPost;
|
||||
using CMSMicroservice.Protobuf.Protos.BlogCategory;
|
||||
using CMSMicroservice.Protobuf.Protos.BlogPostImage;
|
||||
using CMSMicroservice.Protobuf.Protos.SitePage;
|
||||
using CMSMicroservice.Protobuf.Protos.UserContract;
|
||||
using CMSMicroservice.Protobuf.Protos.UserWallet;
|
||||
using CMSMicroservice.Protobuf.Protos.UserWalletChangeLog;
|
||||
using BackOffice.Common.Utilities;
|
||||
// using BackOffice.Services.DiscountProduct;
|
||||
// using BackOffice.Services.DiscountCategory;
|
||||
// using BackOffice.Services.DiscountOrder;
|
||||
// using BackOffice.Services.PublicMessage;
|
||||
// using BackOffice.Services.Tag;
|
||||
using BackOffice.Services.DiscountProduct;
|
||||
using BackOffice.Services.DiscountCategory;
|
||||
using BackOffice.Services.DiscountOrder;
|
||||
using BackOffice.Services.PublicMessage;
|
||||
using BackOffice.Services.Tag;
|
||||
using BackOffice.Services.Blog;
|
||||
using BackOffice.Services.Content;
|
||||
using Blazored.LocalStorage;
|
||||
using Grpc.Core;
|
||||
using Grpc.Core.Interceptors;
|
||||
@@ -37,9 +48,6 @@ using MudBlazor.Services;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using Foursat.BackOffice.BFF.Health.Protobuf;
|
||||
using Foursat.BackOffice.BFF.NetworkMembership.Protos;
|
||||
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@@ -70,12 +78,19 @@ 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>();
|
||||
// services.AddScoped<IDiscountOrderService, DiscountOrderService>();
|
||||
// services.AddScoped<IPublicMessageService, PublicMessageService>();
|
||||
// services.AddScoped<ITagService, TagService>();
|
||||
|
||||
// Discount Shop Services
|
||||
services.AddScoped<IDiscountProductService, DiscountProductService>();
|
||||
services.AddScoped<IDiscountCategoryService, DiscountCategoryService>();
|
||||
services.AddScoped<IDiscountOrderService, DiscountOrderService>();
|
||||
services.AddScoped<IPublicMessageService, PublicMessageService>();
|
||||
services.AddScoped<ITagService, TagService>();
|
||||
|
||||
// Blog & Content Management Services
|
||||
services.AddScoped<IBlogPostService, BlogPostService>();
|
||||
services.AddScoped<IBlogCategoryService, BlogCategoryService>();
|
||||
services.AddScoped<IBlogPostImageService, BlogPostImageService>();
|
||||
services.AddScoped<ISitePageService, SitePageService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
@@ -102,7 +117,7 @@ public static class ConfigureServices
|
||||
return CreateAuthenticatedChannel(baseUri, httpClient, sp);
|
||||
});
|
||||
|
||||
services.AddTransient(sp => new OtpContract.OtpContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new OtpTokenContract.OtpTokenContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new PackageContract.PackageContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new ProductsContract.ProductsContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new RoleContract.RoleContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
@@ -120,18 +135,36 @@ public static class ConfigureServices
|
||||
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
|
||||
// services.AddTransient(sp => new DiscountProductsContract.DiscountProductsContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
// services.AddTransient(sp => new DiscountCategoriesContract.DiscountCategoriesContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
// services.AddTransient(sp => new DiscountOrdersContract.DiscountOrdersContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new DiscountProductContract.DiscountProductContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new DiscountCategoryContract.DiscountCategoryContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new DiscountOrderContract.DiscountOrderContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
|
||||
// Public Message Service
|
||||
// services.AddTransient(sp => new PublicMessagesContract.PublicMessagesContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new PublicMessageContract.PublicMessageContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
|
||||
// Tag Management Services
|
||||
// services.AddTransient(sp => new TagContract.TagContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
// services.AddTransient(sp => new ProductTagContract.ProductTagContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new TagContract.TagContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new ProductTagContract.ProductTagContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
|
||||
// Manual Payment Service
|
||||
services.AddTransient(sp => new ManualPaymentContract.ManualPaymentContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
|
||||
// Inventory Management Service
|
||||
services.AddTransient(sp => new InventoryContract.InventoryContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
|
||||
// Blog & Content Management Services
|
||||
services.AddTransient(sp => new BlogPostContract.BlogPostContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new BlogCategoryContract.BlogCategoryContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new BlogPostImageContract.BlogPostImageContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new SitePageContract.SitePageContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
|
||||
// User Contracts Service
|
||||
services.AddTransient(sp => new UserContractContract.UserContractContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
|
||||
// User Wallet Services
|
||||
services.AddTransient(sp => new UserWalletContract.UserWalletContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
services.AddTransient(sp => new UserWalletChangeLogContract.UserWalletChangeLogContractClient(sp.GetRequiredService<CallInvoker>()));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ public static class RouteConstance
|
||||
public const string UserPage = "/UserPage/";
|
||||
public const string UserOrder = "/UserOrderPage/";
|
||||
public const string Orders = "/OrdersPage/";
|
||||
public const string OrdersSalesReports = "/OrdersSalesReportsPage/";
|
||||
public const string UserRole = "/UserRolePage/";
|
||||
public const string UserAddress = "/UserAddressPage/";
|
||||
public const string Products = "/ProductsPage/";
|
||||
@@ -17,4 +18,11 @@ public static class RouteConstance
|
||||
public const string ProductCategories = "/ProductCategoriesPage/";
|
||||
public const string ProductsBulkEdit = "/ProductsBulkEditPage/";
|
||||
public const string CategoryProducts = "/CategoryProductsPage/";
|
||||
|
||||
// Inventory Management
|
||||
public const string Inventory = "/InventoryPage/";
|
||||
public const string InventoryLowStock = "/InventoryLowStockPage/";
|
||||
public const string InventoryWarehouses = "/InventoryWarehousesPage/";
|
||||
public const string InventoryAddStock = "/InventoryAddStockPage/";
|
||||
public const string InventoryMovements = "/InventoryMovementsPage/";
|
||||
}
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||
<add key="FourSat" value="https://git.afrino.co/api/packages/FourSat/nuget/index.json" />
|
||||
<add key="Afrino" value="https://git.afrino.co/api/packages/Afrino/nuget/index.json" />
|
||||
<clear />
|
||||
<!-- Nexus (hosts FourSat packages + proxies nuget.org) -->
|
||||
<add key="Nexus" value="http://194.5.195.53:32081/repository/nuget-all/index.json" allowInsecureConnections="true" />
|
||||
</packageSources>
|
||||
<packageSourceCredentials>
|
||||
<FourSat>
|
||||
<add key="Username" value="masoud" />
|
||||
<Nexus>
|
||||
<add key="Username" value="admin" />
|
||||
<add key="ClearTextPassword" value="87zH26nbqT" />
|
||||
</FourSat>
|
||||
<Afrino>
|
||||
<add key="Username" value="systemuser" />
|
||||
<add key="ClearTextPassword" value="sZSA7PTiv3pUSQZ" />
|
||||
</Afrino>
|
||||
</Nexus>
|
||||
</packageSourceCredentials>
|
||||
</configuration>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@using BackOffice.BFF.Category.Protobuf.Protos.Category
|
||||
@using CMSMicroservice.Protobuf.Protos.Category
|
||||
|
||||
<MudAutocomplete T="GetAllCategoryByFilterResponseModel"
|
||||
Label="@Label"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using BackOffice.BFF.Category.Protobuf.Protos.Category;
|
||||
using CMSMicroservice.Protobuf.Protos;
|
||||
using CMSMicroservice.Protobuf.Protos.Category;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace BackOffice.Pages.AutoComplete;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@using BackOffice.BFF.Category.Protobuf.Protos.Category
|
||||
@using CMSMicroservice.Protobuf.Protos.Category
|
||||
|
||||
<MudAutocomplete T="GetAllCategoryByFilterResponseModel"
|
||||
Label="@Label"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using BackOffice.BFF.Category.Protobuf.Protos.Category;
|
||||
using CMSMicroservice.Protobuf.Protos;
|
||||
using CMSMicroservice.Protobuf.Protos.Category;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace BackOffice.Pages.AutoComplete;
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
@using BackOffice.BFF.Category.Protobuf.Protos.Category
|
||||
@using CMSMicroservice.Protobuf.Protos.Category
|
||||
|
||||
<MudSelect T="long"
|
||||
@ref="_selectRef"
|
||||
Label="@Label"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Disabled="@Disabled"
|
||||
MultiSelection="true"
|
||||
MultiSelectionTextFunc="@GetSelectedText"
|
||||
SelectedValues="_internalSelectedIds"
|
||||
SelectedValuesChanged="@(_ => OnSelectedValuesChangedAsync())">
|
||||
SelectedValuesChanged="OnSelectedValuesChangedAsync">
|
||||
@foreach (var item in _items)
|
||||
{
|
||||
<MudSelectItem T="long" Value="@item.Id">
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BackOffice.BFF.Category.Protobuf.Protos.Category;
|
||||
using CMSMicroservice.Protobuf.Protos;
|
||||
using CMSMicroservice.Protobuf.Protos.Category;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace BackOffice.Pages.AutoComplete;
|
||||
@@ -61,11 +62,9 @@ public partial class CategoryMultiSelectCombo
|
||||
: new HashSet<long>();
|
||||
}
|
||||
|
||||
private async Task OnSelectedValuesChangedAsync()
|
||||
private async Task OnSelectedValuesChangedAsync(IEnumerable<long> values)
|
||||
{
|
||||
var current = _selectRef?.SelectedValues ?? System.Array.Empty<long>();
|
||||
|
||||
_internalSelectedIds = current.Where(id => id > 0).Distinct().ToHashSet();
|
||||
_internalSelectedIds = values.Where(id => id > 0).Distinct().ToHashSet();
|
||||
SelectedIds = _internalSelectedIds.ToList();
|
||||
|
||||
if (SelectedIdsChanged.HasDelegate)
|
||||
@@ -73,5 +72,27 @@ public partial class CategoryMultiSelectCombo
|
||||
await SelectedIdsChanged.InvokeAsync(SelectedIds);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetSelectedText(List<string> selectedValues)
|
||||
{
|
||||
if (selectedValues == null || selectedValues.Count == 0)
|
||||
return string.Empty;
|
||||
|
||||
var titles = new List<string>();
|
||||
foreach (var val in selectedValues)
|
||||
{
|
||||
if (long.TryParse(val, out var id))
|
||||
{
|
||||
var item = _items.FirstOrDefault(x => x.Id == id);
|
||||
titles.Add(item != null ? item.Title : val);
|
||||
}
|
||||
else
|
||||
{
|
||||
titles.Add(val);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Join("، ", titles);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
@using BackOffice.Services.DiscountCategory
|
||||
|
||||
<MudSelect @ref="_selectRef"
|
||||
T="long"
|
||||
MultiSelection="true"
|
||||
MultiSelectionTextFunc="@GetSelectedText"
|
||||
SelectedValues="_internalSelectedIds"
|
||||
SelectedValuesChanged="OnSelectedValuesChangedAsync"
|
||||
Label="@Label"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Disabled="@Disabled">
|
||||
@foreach (var item in _items)
|
||||
{
|
||||
<MudSelectItem T="long" Value="@item.Id">@item.Title</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
@@ -0,0 +1,119 @@
|
||||
using BackOffice.Services.DiscountCategory;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.AutoComplete;
|
||||
|
||||
public partial class DiscountCategoryMultiSelectCombo
|
||||
{
|
||||
[Inject] public IDiscountCategoryService CategoryService { get; set; } = default!;
|
||||
|
||||
[Parameter] public List<long> SelectedIds { get; set; } = new();
|
||||
[Parameter] public EventCallback<List<long>> SelectedIdsChanged { get; set; }
|
||||
[Parameter] public string? Label { get; set; } = "انتخاب دستهبندیها";
|
||||
[Parameter] public bool Disabled { get; set; }
|
||||
|
||||
private List<DiscountCategoryDto> _items = new();
|
||||
private HashSet<long> _internalSelectedIds = new();
|
||||
private MudSelect<long>? _selectRef;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
await LoadCategoriesAsync();
|
||||
SyncInternalFromParameter();
|
||||
}
|
||||
|
||||
protected override Task OnParametersSetAsync()
|
||||
{
|
||||
SyncInternalFromParameter();
|
||||
return base.OnParametersSetAsync();
|
||||
}
|
||||
|
||||
private async Task LoadCategoriesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var tree = await CategoryService.GetCategoriesAsync(isActive: true);
|
||||
_items = FlattenCategories(tree ?? new List<DiscountCategoryDto>());
|
||||
|
||||
// Log first few items
|
||||
if (_items.Count > 0)
|
||||
{
|
||||
foreach (var item in _items.Take(5))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
}
|
||||
_items = new List<DiscountCategoryDto>();
|
||||
}
|
||||
}
|
||||
|
||||
private List<DiscountCategoryDto> FlattenCategories(List<DiscountCategoryDto> categories, string prefix = "")
|
||||
{
|
||||
var result = new List<DiscountCategoryDto>();
|
||||
foreach (var category in categories)
|
||||
{
|
||||
var catCopy = new DiscountCategoryDto
|
||||
{
|
||||
Id = category.Id,
|
||||
Title = prefix + category.Title,
|
||||
Name = category.Name,
|
||||
ParentCategoryId = category.ParentCategoryId,
|
||||
IsActive = category.IsActive
|
||||
};
|
||||
result.Add(catCopy);
|
||||
|
||||
if (category.Children?.Any() == true)
|
||||
{
|
||||
result.AddRange(FlattenCategories(category.Children.ToList(), prefix + " "));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void SyncInternalFromParameter()
|
||||
{
|
||||
_internalSelectedIds = SelectedIds != null
|
||||
? SelectedIds.Where(id => id > 0).Distinct().ToHashSet()
|
||||
: new HashSet<long>();
|
||||
}
|
||||
|
||||
private async Task OnSelectedValuesChangedAsync(IEnumerable<long> values)
|
||||
{
|
||||
_internalSelectedIds = values.Where(id => id > 0).Distinct().ToHashSet();
|
||||
SelectedIds = _internalSelectedIds.ToList();
|
||||
|
||||
if (SelectedIdsChanged.HasDelegate)
|
||||
{
|
||||
await SelectedIdsChanged.InvokeAsync(SelectedIds);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetSelectedText(List<string> selectedValues)
|
||||
{
|
||||
if (selectedValues == null || selectedValues.Count == 0)
|
||||
return string.Empty;
|
||||
|
||||
var titles = new List<string>();
|
||||
foreach (var val in selectedValues)
|
||||
{
|
||||
if (long.TryParse(val, out var id))
|
||||
{
|
||||
var item = _items.FirstOrDefault(x => x.Id == id);
|
||||
titles.Add(item != null ? item.Title : val);
|
||||
}
|
||||
else
|
||||
{
|
||||
titles.Add(val);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Join("، ", titles);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
@using BackOffice.BFF.Products.Protobuf.Protos.Products
|
||||
@using CMSMicroservice.Protobuf.Protos.Products
|
||||
|
||||
<MudAutocomplete T="GetAllProductsByFilterResponseModel"
|
||||
Label="@Label"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using BackOffice.BFF.Products.Protobuf.Protos.Products;
|
||||
using BackOffice.BFF.Protobuf.Common;
|
||||
using CMSMicroservice.Protobuf.Protos.Products;
|
||||
using CMSMicroservice.Protobuf.Protos;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace BackOffice.Pages.AutoComplete;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@using BackOffice.BFF.Role.Protobuf.Protos.Role
|
||||
@using CMSMicroservice.Protobuf.Protos.Role
|
||||
|
||||
<MudAutocomplete T="GetAllRoleByFilterResponseModel"
|
||||
Label="@Label"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using BackOffice.BFF.Role.Protobuf.Protos.Role;
|
||||
using CMSMicroservice.Protobuf.Protos.Role;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@using BackOffice.BFF.User.Protobuf.Protos.User
|
||||
@using CMSMicroservice.Protobuf.Protos.User
|
||||
|
||||
<MudAutocomplete T="GetAllUserByFilterResponseModel"
|
||||
Label="@Label"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using BackOffice.BFF.User.Protobuf.Protos.User;
|
||||
using CMSMicroservice.Protobuf.Protos.User;
|
||||
using Grpc.Core;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@using Foursat.BackOffice.BFF.Commission.Protos
|
||||
@using CMSMicroservice.Protobuf.Protos.Commission
|
||||
|
||||
<MudAutocomplete T="WeekInfo"
|
||||
Label="@Label"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using BackOffice.Services;
|
||||
using Foursat.BackOffice.BFF.Commission.Protos;
|
||||
using CMSMicroservice.Protobuf.Protos.Commission;
|
||||
using Grpc.Core;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
@* Merged into /blog *@
|
||||
@attribute [Authorize(Roles = "Administrator")]
|
||||
|
||||
@using BackOffice.Services.Blog
|
||||
|
||||
@inject IBlogCategoryService BlogCategoryService
|
||||
|
||||
<div>
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.h5">مدیریت دستهبندیهای بلاگ</MudText>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="CreateCategory">
|
||||
افزودن دستهبندی
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
<MudDataGrid T="BlogCategoryItemDto"
|
||||
@ref="_grid"
|
||||
ServerData="LoadServerData"
|
||||
Hover="true"
|
||||
Elevation="1"
|
||||
Dense="true"
|
||||
Height="calc(100vh - 240px)">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
<PropertyColumn Property="x => x.Title" Title="عنوان" />
|
||||
<PropertyColumn Property="x => x.Slug" Title="اسلاگ" />
|
||||
<PropertyColumn Property="x => x.IconName" Title="آیکون" />
|
||||
<PropertyColumn Property="x => x.PostCount" Title="تعداد پست" />
|
||||
<PropertyColumn Property="x => x.IsActive" Title="وضعیت">
|
||||
<CellTemplate>
|
||||
<MudChip Color="@(context.Item.IsActive ? Color.Success : Color.Error)"
|
||||
Size="Size.Small">
|
||||
@(context.Item.IsActive ? "فعال" : "غیرفعال")
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<PropertyColumn Property="x => x.SortOrder" Title="ترتیب" />
|
||||
<TemplateColumn Title="عملیات" Sortable="false">
|
||||
<CellTemplate>
|
||||
<MudStack Row="true" Spacing="1">
|
||||
<MudTooltip Text="ویرایش">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Size="Size.Small"
|
||||
Color="Color.Primary"
|
||||
OnClick="@(() => EditCategory(context.Item))" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="حذف">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Size="Size.Small"
|
||||
Color="Color.Error"
|
||||
OnClick="@(() => DeleteCategory(context.Item))" />
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="BlogCategoryItemDto" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudStack>
|
||||
</div>
|
||||
@@ -0,0 +1,102 @@
|
||||
using BackOffice.Services.Blog;
|
||||
using BackOffice.Pages.Blog.Components;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Blog;
|
||||
|
||||
public partial class BlogCategoryManagementPage
|
||||
{
|
||||
private MudDataGrid<BlogCategoryItemDto> _grid = default!;
|
||||
|
||||
private async Task<GridData<BlogCategoryItemDto>> LoadServerData(GridState<BlogCategoryItemDto> state)
|
||||
{
|
||||
var filter = new BlogCategoryFilterDto
|
||||
{
|
||||
PageNumber = state.Page + 1,
|
||||
PageSize = state.PageSize
|
||||
};
|
||||
|
||||
var result = await BlogCategoryService.GetAllAsync(filter);
|
||||
|
||||
return new GridData<BlogCategoryItemDto>
|
||||
{
|
||||
Items = result.Items,
|
||||
TotalItems = result.TotalCount
|
||||
};
|
||||
}
|
||||
|
||||
private async Task ReloadAsync()
|
||||
{
|
||||
if (_grid != null)
|
||||
{
|
||||
await _grid.ReloadServerData();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CreateCategory()
|
||||
{
|
||||
var parameters = new DialogParameters<BlogCategoryEditDialog>
|
||||
{
|
||||
{ nameof(BlogCategoryEditDialog.Model), new BlogCategoryEditDto() },
|
||||
{ nameof(BlogCategoryEditDialog.IsEditMode), false }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<BlogCategoryEditDialog>("ایجاد دستهبندی جدید", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true });
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
Snackbar.Add("دستهبندی با موفقیت ایجاد شد", Severity.Success);
|
||||
await ReloadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EditCategory(BlogCategoryItemDto item)
|
||||
{
|
||||
var dto = new BlogCategoryEditDto
|
||||
{
|
||||
Title = item.Title,
|
||||
Slug = item.Slug,
|
||||
Description = item.Description,
|
||||
IconName = item.IconName,
|
||||
SortOrder = item.SortOrder,
|
||||
IsActive = item.IsActive
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters<BlogCategoryEditDialog>
|
||||
{
|
||||
{ nameof(BlogCategoryEditDialog.Model), dto },
|
||||
{ nameof(BlogCategoryEditDialog.CategoryId), item.Id },
|
||||
{ nameof(BlogCategoryEditDialog.IsEditMode), true }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<BlogCategoryEditDialog>("ویرایش دستهبندی", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true });
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
Snackbar.Add("دستهبندی با موفقیت بهروزرسانی شد", Severity.Success);
|
||||
await ReloadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteCategory(BlogCategoryItemDto item)
|
||||
{
|
||||
var confirmed = await DialogService.ShowMessageBox(
|
||||
"حذف دستهبندی",
|
||||
$"آیا از حذف دستهبندی «{item.Title}» مطمئن هستید؟",
|
||||
yesText: "حذف",
|
||||
cancelText: "لغو");
|
||||
|
||||
if (confirmed == true)
|
||||
{
|
||||
await BlogCategoryService.DeleteAsync(item.Id);
|
||||
Snackbar.Add("دستهبندی حذف شد", Severity.Success);
|
||||
await ReloadAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
@page "/blog"
|
||||
@page "/blog/posts"
|
||||
@page "/blog/categories"
|
||||
@attribute [Authorize(Roles = "Administrator")]
|
||||
|
||||
<div>
|
||||
<MudText Typo="Typo.h5" Class="mb-3">مدیریت بلاگ</MudText>
|
||||
|
||||
<MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true"
|
||||
PanelClass="pt-4" @bind-ActivePanelIndex="_activeTab">
|
||||
<MudTabPanel Text="پستها" Icon="@Icons.Material.Filled.PostAdd">
|
||||
<BlogPostManagementPage />
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="دستهبندیها" Icon="@Icons.Material.Filled.Category">
|
||||
<BlogCategoryManagementPage />
|
||||
</MudTabPanel>
|
||||
</MudTabs>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private int _activeTab;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
var uri = Navigation.ToBaseRelativePath(Navigation.Uri).ToLowerInvariant().TrimEnd('/');
|
||||
_activeTab = uri switch
|
||||
{
|
||||
"blog/categories" => 1,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
@* Merged into /blog *@
|
||||
@attribute [Authorize(Roles = "Administrator")]
|
||||
|
||||
@using BackOffice.Services.Blog
|
||||
|
||||
@inject IBlogPostService BlogPostService
|
||||
@inject IBlogCategoryService BlogCategoryService
|
||||
|
||||
<div>
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.h5">مدیریت پستهای بلاگ</MudText>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="CreatePost">
|
||||
افزودن پست
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
||||
<MudTextField @bind-Value="_search"
|
||||
Label="جستجو بر اساس عنوان"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||
Immediate="true"
|
||||
OnBlur="ReloadAsync"
|
||||
OnKeyDown="OnSearchKeyDown" />
|
||||
|
||||
<MudSelect @bind-Value="_statusFilter"
|
||||
Label="وضعیت"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Style="width: 160px;">
|
||||
<MudSelectItem T="int?" Value="@( (int?)null )">همه</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="0">پیشنویس</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="1">منتشر شده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="2">آرشیو شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
|
||||
<MudSelect @bind-Value="_categoryFilter"
|
||||
Label="دستهبندی"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Style="width: 200px;">
|
||||
<MudSelectItem T="long?" Value="@( (long?)null )">همه</MudSelectItem>
|
||||
@foreach (var cat in _categories)
|
||||
{
|
||||
<MudSelectItem T="long?" Value="@( (long?)cat.Id )">@cat.Title</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Primary"
|
||||
OnClick="ReloadAsync">
|
||||
اعمال فیلتر
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
<MudDataGrid T="BlogPostListItemDto"
|
||||
@ref="_grid"
|
||||
ServerData="LoadServerData"
|
||||
Hover="true"
|
||||
Elevation="1"
|
||||
Dense="true"
|
||||
Height="calc(100vh - 240px)">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
<PropertyColumn Property="x => x.Title" Title="عنوان" />
|
||||
<PropertyColumn Property="x => x.Slug" Title="اسلاگ" />
|
||||
<PropertyColumn Property="x => x.StatusName" Title="وضعیت">
|
||||
<CellTemplate>
|
||||
<MudChip Color="@GetStatusColor(context.Item.Status)"
|
||||
Size="Size.Small">
|
||||
@context.Item.StatusName
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<PropertyColumn Property="x => x.IsFeatured" Title="ویژه">
|
||||
<CellTemplate>
|
||||
@if (context.Item.IsFeatured)
|
||||
{
|
||||
<MudIcon Icon="@Icons.Material.Filled.Star" Color="Color.Warning" Size="Size.Small" />
|
||||
}
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<PropertyColumn Property="x => x.ViewCount" Title="بازدید" />
|
||||
<TemplateColumn Title="دستهبندیها" Sortable="false">
|
||||
<CellTemplate>
|
||||
<MudStack Row="true" Spacing="1">
|
||||
@foreach (var cat in context.Item.Categories)
|
||||
{
|
||||
<MudChip Size="Size.Small" Color="Color.Info">@cat.Title</MudChip>
|
||||
}
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="تاریخ ایجاد">
|
||||
<CellTemplate>
|
||||
@context.Item.Created.MiladiToJalali()
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="عملیات" Sortable="false">
|
||||
<CellTemplate>
|
||||
<MudStack Row="true" Spacing="1">
|
||||
@if (context.Item.Status == 0)
|
||||
{
|
||||
<MudTooltip Text="انتشار">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Publish"
|
||||
Size="Size.Small"
|
||||
Color="Color.Success"
|
||||
OnClick="@(() => PublishPost(context.Item))" />
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (context.Item.Status == 1)
|
||||
{
|
||||
<MudTooltip Text="آرشیو">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Archive"
|
||||
Size="Size.Small"
|
||||
Color="Color.Warning"
|
||||
OnClick="@(() => ArchivePost(context.Item))" />
|
||||
</MudTooltip>
|
||||
}
|
||||
<MudTooltip Text="ویرایش">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Size="Size.Small"
|
||||
Color="Color.Primary"
|
||||
OnClick="@(() => EditPost(context.Item))" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="حذف">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Size="Size.Small"
|
||||
Color="Color.Error"
|
||||
OnClick="@(() => DeletePost(context.Item))" />
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="BlogPostListItemDto" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudStack>
|
||||
</div>
|
||||
@@ -0,0 +1,189 @@
|
||||
using BackOffice.Services.Blog;
|
||||
using BackOffice.Pages.Blog.Components;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Blog;
|
||||
|
||||
public partial class BlogPostManagementPage
|
||||
{
|
||||
private MudDataGrid<BlogPostListItemDto> _grid = default!;
|
||||
private string? _search;
|
||||
private int? _statusFilter;
|
||||
private long? _categoryFilter;
|
||||
private List<BlogCategoryItemDto> _categories = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_categories = await BlogCategoryService.GetActiveAsync();
|
||||
}
|
||||
|
||||
private async Task<GridData<BlogPostListItemDto>> LoadServerData(GridState<BlogPostListItemDto> state)
|
||||
{
|
||||
var filter = new BlogPostFilterDto
|
||||
{
|
||||
PageNumber = state.Page + 1,
|
||||
PageSize = state.PageSize,
|
||||
SearchTerm = _search,
|
||||
Status = _statusFilter,
|
||||
CategoryId = _categoryFilter
|
||||
};
|
||||
|
||||
var result = await BlogPostService.GetAllAsync(filter);
|
||||
|
||||
return new GridData<BlogPostListItemDto>
|
||||
{
|
||||
Items = result.Items,
|
||||
TotalItems = result.TotalCount
|
||||
};
|
||||
}
|
||||
|
||||
private async Task ReloadAsync()
|
||||
{
|
||||
if (_grid != null)
|
||||
{
|
||||
await _grid.ReloadServerData();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSearchKeyDown(KeyboardEventArgs args)
|
||||
{
|
||||
if (args.Key == "Enter")
|
||||
{
|
||||
_ = ReloadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private static Color GetStatusColor(int status) => status switch
|
||||
{
|
||||
0 => Color.Default, // Draft
|
||||
1 => Color.Success, // Published
|
||||
2 => Color.Warning, // Archived
|
||||
_ => Color.Default
|
||||
};
|
||||
|
||||
private async Task CreatePost()
|
||||
{
|
||||
var parameters = new DialogParameters<BlogPostEditDialog>
|
||||
{
|
||||
{ nameof(BlogPostEditDialog.Model), new BlogPostEditDto() },
|
||||
{ nameof(BlogPostEditDialog.IsEditMode), false },
|
||||
{ nameof(BlogPostEditDialog.Categories), _categories }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<BlogPostEditDialog>("ایجاد پست جدید", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Large, FullWidth = true });
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
Snackbar.Add("پست با موفقیت ایجاد شد", Severity.Success);
|
||||
await ReloadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EditPost(BlogPostListItemDto item)
|
||||
{
|
||||
var details = await BlogPostService.GetByIdAsync(item.Id);
|
||||
if (details == null)
|
||||
{
|
||||
Snackbar.Add("خطا در دریافت اطلاعات پست", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var dto = new BlogPostEditDto
|
||||
{
|
||||
Title = details.Title,
|
||||
Slug = details.Slug,
|
||||
Summary = details.Summary,
|
||||
HtmlContent = details.HtmlContent,
|
||||
FeaturedImagePath = details.FeaturedImagePath,
|
||||
FeaturedImageThumbnailPath = details.FeaturedImageThumbnailPath,
|
||||
CategoryIds = details.Categories.Select(c => c.Id).ToList(),
|
||||
TagIds = details.Tags.Select(t => t.Id).ToList(),
|
||||
IsFeatured = details.IsFeatured,
|
||||
SortOrder = details.SortOrder
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters<BlogPostEditDialog>
|
||||
{
|
||||
{ nameof(BlogPostEditDialog.Model), dto },
|
||||
{ nameof(BlogPostEditDialog.PostId), item.Id },
|
||||
{ nameof(BlogPostEditDialog.IsEditMode), true },
|
||||
{ nameof(BlogPostEditDialog.Categories), _categories }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<BlogPostEditDialog>("ویرایش پست", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Large, FullWidth = true });
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
Snackbar.Add("پست با موفقیت بهروزرسانی شد", Severity.Success);
|
||||
await ReloadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PublishPost(BlogPostListItemDto item)
|
||||
{
|
||||
var confirmed = await DialogService.ShowMessageBox(
|
||||
"انتشار پست",
|
||||
$"آیا از انتشار پست «{item.Title}» مطمئن هستید؟",
|
||||
yesText: "انتشار",
|
||||
cancelText: "لغو");
|
||||
|
||||
if (confirmed == true)
|
||||
{
|
||||
var result = await BlogPostService.PublishAsync(item.Id);
|
||||
if (result.Success)
|
||||
{
|
||||
Snackbar.Add("پست با موفقیت منتشر شد", Severity.Success);
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add(result.Message, Severity.Error);
|
||||
}
|
||||
await ReloadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ArchivePost(BlogPostListItemDto item)
|
||||
{
|
||||
var confirmed = await DialogService.ShowMessageBox(
|
||||
"آرشیو پست",
|
||||
$"آیا از آرشیو کردن پست «{item.Title}» مطمئن هستید؟",
|
||||
yesText: "آرشیو",
|
||||
cancelText: "لغو");
|
||||
|
||||
if (confirmed == true)
|
||||
{
|
||||
var result = await BlogPostService.ArchiveAsync(item.Id);
|
||||
if (result.Success)
|
||||
{
|
||||
Snackbar.Add("پست آرشیو شد", Severity.Success);
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add(result.Message, Severity.Error);
|
||||
}
|
||||
await ReloadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeletePost(BlogPostListItemDto item)
|
||||
{
|
||||
var confirmed = await DialogService.ShowMessageBox(
|
||||
"حذف پست",
|
||||
$"آیا از حذف پست «{item.Title}» مطمئن هستید؟",
|
||||
yesText: "حذف",
|
||||
cancelText: "لغو");
|
||||
|
||||
if (confirmed == true)
|
||||
{
|
||||
await BlogPostService.DeleteAsync(item.Id);
|
||||
Snackbar.Add("پست حذف شد", Severity.Success);
|
||||
await ReloadAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
@using BackOffice.Services.Blog
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<EditForm Model="@Model" OnValidSubmit="HandleValidSubmit">
|
||||
<MudStack Spacing="2">
|
||||
<MudTextField @bind-Value="Model.Title"
|
||||
Label="عنوان"
|
||||
Required="true"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudTextField @bind-Value="Model.Slug"
|
||||
Label="اسلاگ (URL)"
|
||||
Required="true"
|
||||
Variant="Variant.Outlined"
|
||||
HelperText="مثال: tech-news" />
|
||||
|
||||
<MudTextField @bind-Value="Model.Description"
|
||||
Label="توضیحات"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="3" />
|
||||
|
||||
<MudTextField @bind-Value="Model.IconName"
|
||||
Label="نام آیکون"
|
||||
Variant="Variant.Outlined"
|
||||
HelperText="مثال: category" />
|
||||
|
||||
<MudNumericField T="int"
|
||||
@bind-Value="Model.SortOrder"
|
||||
Label="ترتیب نمایش"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudSwitch T="bool" @bind-Value="Model.IsActive" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.body2">فعال</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</EditForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="SubmitAsync">
|
||||
@(_isEditMode ? "ذخیره تغییرات" : "ایجاد دستهبندی")
|
||||
</MudButton>
|
||||
<MudButton Color="Color.Default" Variant="Variant.Text" OnClick="Cancel">
|
||||
انصراف
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,41 @@
|
||||
using BackOffice.Services.Blog;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Blog.Components;
|
||||
|
||||
public partial class BlogCategoryEditDialog
|
||||
{
|
||||
[CascadingParameter] IMudDialogInstance DialogInstance { get; set; } = default!;
|
||||
[Inject] public IBlogCategoryService BlogCategoryService { get; set; } = default!;
|
||||
|
||||
[Parameter] public BlogCategoryEditDto Model { get; set; } = new();
|
||||
[Parameter] public long? CategoryId { get; set; }
|
||||
[Parameter] public bool IsEditMode { get; set; }
|
||||
|
||||
private bool _isEditMode => IsEditMode && CategoryId.HasValue;
|
||||
|
||||
private async Task HandleValidSubmit()
|
||||
{
|
||||
await SubmitAsync();
|
||||
}
|
||||
|
||||
private async Task SubmitAsync()
|
||||
{
|
||||
if (_isEditMode && CategoryId.HasValue)
|
||||
{
|
||||
await BlogCategoryService.UpdateAsync(CategoryId.Value, Model);
|
||||
}
|
||||
else
|
||||
{
|
||||
await BlogCategoryService.CreateAsync(Model);
|
||||
}
|
||||
|
||||
DialogInstance.Close(DialogResult.Ok(true));
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
DialogInstance.Cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
@using BackOffice.Services.Blog
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Tizzani.MudBlazor.HtmlEditor
|
||||
@using BackOffice.Common.BaseComponents
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudForm @ref="_form" @bind-IsValid="@_isValid">
|
||||
<MudStack Spacing="2">
|
||||
<MudTextField @bind-Value="Model.Title"
|
||||
Label="عنوان پست"
|
||||
Required="true"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudTextField @bind-Value="Model.Summary"
|
||||
Label="خلاصه"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="3" />
|
||||
|
||||
<MudText Typo="Typo.subtitle2">محتوای HTML</MudText>
|
||||
<MudHtmlEditor @bind-Html="Model.HtmlContent" MinHeight="300px">
|
||||
<MudHtmlToolbarOptions InsertImage="false" />
|
||||
</MudHtmlEditor>
|
||||
|
||||
<MudText Typo="Typo.subtitle2">تصویر شاخص</MudText>
|
||||
<MudStack Justify="Justify.Center" AlignItems="AlignItems.Center">
|
||||
@if (!string.IsNullOrWhiteSpace(_imagePreview))
|
||||
{
|
||||
<Image Src="@_imagePreview" Width="280" Height="180" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Model.FeaturedImagePath))
|
||||
{
|
||||
<Image Src="@Model.FeaturedImagePath" Width="280" Height="180" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper Class="d-flex align-center justify-center" Style="width:280px;height:180px;">
|
||||
<MudText Typo="Typo.caption">تصویری انتخاب نشده است</MudText>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
<MudFileUpload T="IBrowserFile" Accept="image/*" FilesChanged="OnImageSelected">
|
||||
<ActivatorContent>
|
||||
<MudButton HtmlTag="label"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
ButtonType="ButtonType.Button"
|
||||
StartIcon="@Icons.Material.Filled.Image">
|
||||
انتخاب تصویر شاخص
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
<SelectedTemplate>
|
||||
@if (context != null)
|
||||
{
|
||||
<MudText Class="mt-1" Typo="Typo.caption">@context.Name</MudText>
|
||||
}
|
||||
</SelectedTemplate>
|
||||
</MudFileUpload>
|
||||
</MudStack>
|
||||
|
||||
<MudSelect T="long"
|
||||
Label="دستهبندیها"
|
||||
MultiSelection="true"
|
||||
@bind-SelectedValues="_selectedCategoryIds"
|
||||
Variant="Variant.Outlined"
|
||||
MultiSelectionTextFunc="@(new Func<List<string>, string>(GetCategorySelectionText))">
|
||||
@foreach (var cat in Categories)
|
||||
{
|
||||
<MudSelectItem T="long" Value="@cat.Id">@cat.Title</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudSwitch T="bool" @bind-Value="Model.IsFeatured" Color="Color.Warning" />
|
||||
<MudText Typo="Typo.body2">پست ویژه</MudText>
|
||||
</MudStack>
|
||||
|
||||
<MudNumericField T="int"
|
||||
@bind-Value="Model.SortOrder"
|
||||
Label="ترتیب نمایش"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="SubmitAsync" Disabled="@(!_isValid || _loading)">
|
||||
@(_isEditMode ? "ذخیره تغییرات" : "ایجاد پست")
|
||||
</MudButton>
|
||||
<MudButton Color="Color.Default" Variant="Variant.Text" OnClick="Cancel">
|
||||
انصراف
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,115 @@
|
||||
using BackOffice.Services.Blog;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Blog.Components;
|
||||
|
||||
public partial class BlogPostEditDialog
|
||||
{
|
||||
[CascadingParameter] IMudDialogInstance DialogInstance { get; set; } = default!;
|
||||
[Inject] public IBlogPostService BlogPostService { get; set; } = default!;
|
||||
|
||||
[Parameter] public BlogPostEditDto Model { get; set; } = new();
|
||||
[Parameter] public long? PostId { get; set; }
|
||||
[Parameter] public bool IsEditMode { get; set; }
|
||||
[Parameter] public List<BlogCategoryItemDto> Categories { get; set; } = new();
|
||||
|
||||
private bool _isEditMode => IsEditMode && PostId.HasValue;
|
||||
private MudForm? _form;
|
||||
private bool _isValid = true;
|
||||
private bool _loading;
|
||||
|
||||
private IBrowserFile? _imageFile;
|
||||
private byte[]? _imageBuffer;
|
||||
private string? _imagePreview;
|
||||
private readonly long _maxAllowedSize = (1024 * 1024) * 5;
|
||||
|
||||
private IEnumerable<long> _selectedCategoryIds
|
||||
{
|
||||
get => Model.CategoryIds;
|
||||
set => Model.CategoryIds = value.ToList();
|
||||
}
|
||||
|
||||
private string GetCategorySelectionText(List<string> selectedValues)
|
||||
{
|
||||
if (selectedValues == null || !selectedValues.Any())
|
||||
return "انتخاب دستهبندی";
|
||||
|
||||
var names = new List<string>();
|
||||
foreach (var val in selectedValues)
|
||||
{
|
||||
if (long.TryParse(val, out var id))
|
||||
{
|
||||
var cat = Categories.FirstOrDefault(c => c.Id == id);
|
||||
if (cat != null) names.Add(cat.Title);
|
||||
}
|
||||
}
|
||||
|
||||
return names.Any() ? string.Join("، ", names) : "انتخاب دستهبندی";
|
||||
}
|
||||
|
||||
private async Task OnImageSelected(IBrowserFile? file)
|
||||
{
|
||||
_imageFile = file;
|
||||
if (file != null)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await file.OpenReadStream(_maxAllowedSize).CopyToAsync(ms);
|
||||
_imageBuffer = ms.ToArray();
|
||||
_imagePreview = $"data:{file.ContentType};base64," + Convert.ToBase64String(_imageBuffer);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleValidSubmit()
|
||||
{
|
||||
await SubmitAsync();
|
||||
}
|
||||
|
||||
private async Task SubmitAsync()
|
||||
{
|
||||
if (_form != null)
|
||||
{
|
||||
await _form.Validate();
|
||||
if (!_isValid) return;
|
||||
}
|
||||
|
||||
_loading = true;
|
||||
|
||||
// Handle image upload
|
||||
if (_imageFile != null && _imageBuffer != null)
|
||||
{
|
||||
Model.ImageFile = _imageBuffer;
|
||||
Model.ImageMime = _imageFile.ContentType;
|
||||
Model.ImageFileName = Path.GetFileNameWithoutExtension(_imageFile.Name);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_isEditMode && PostId.HasValue)
|
||||
{
|
||||
await BlogPostService.UpdateAsync(PostId.Value, Model);
|
||||
}
|
||||
else
|
||||
{
|
||||
await BlogPostService.CreateAsync(Model);
|
||||
}
|
||||
|
||||
DialogInstance.Close(DialogResult.Ok(true));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
DialogInstance.Cancel();
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,31 @@
|
||||
@attribute [Route(RouteConstance.Category)]
|
||||
|
||||
@using BackOffice.BFF.Category.Protobuf.Protos.Category
|
||||
@using CMSMicroservice.Protobuf.Protos.Category
|
||||
@using BackOffice.Common.BaseComponents
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
@using DataModel = BackOffice.BFF.Category.Protobuf.Protos.Category.GetAllCategoryByFilterResponseModel
|
||||
@using DataModel = CMSMicroservice.Protobuf.Protos.Category.GetAllCategoryByFilterResponseModel
|
||||
|
||||
<BasePageComponent @ref="_basePage" OnClearFilterClick="OnFilterCleared" OnSubmitClick="OnFilterSubmit">
|
||||
<Filters>
|
||||
<MudTextField T="string" Clearable="true" Label="عنوان" @bind-Value="@_request.Filter.Title" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
<CategoryAutoComplete Value="_parentFilterId"
|
||||
ValueChanged="OnParentFilterChanged"
|
||||
Label="فیلتر بر اساس دستهبندی والد" />
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField T="string" Clearable="true" Label="عنوان" @bind-Value="@_request.Filter.Title" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<CategoryAutoComplete Value="_parentFilterId"
|
||||
ValueChanged="OnParentFilterChanged"
|
||||
Label="فیلتر بر اساس دستهبندی والد" />
|
||||
</MudItem>
|
||||
</Filters>
|
||||
<Content>
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="3">
|
||||
<MudPaper Class="pa-2" Style="height:72vh; overflow:auto;">
|
||||
<MudPaper Class="pa-2" Style="height:calc(100vh - 240px); overflow:auto;">
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2">درخت دستهبندیها</MudText>
|
||||
<MudList T="string" Dense="true">
|
||||
<MudListItem T="string"
|
||||
OnClick="@(() => OnTreeNodeSelected(null))"
|
||||
Selected="@(_parentFilterId == null)"
|
||||
Style="cursor:pointer;">
|
||||
>
|
||||
<MudText Typo="Typo.body2">همه دستهبندیها</MudText>
|
||||
</MudListItem>
|
||||
@foreach (var node in _treeNodes)
|
||||
@@ -29,7 +33,7 @@
|
||||
<MudListItem T="string"
|
||||
OnClick="@(() => OnTreeNodeSelected(node.Id))"
|
||||
Selected="@(_parentFilterId == node.Id)"
|
||||
Style="cursor:pointer;">
|
||||
>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
<div style="width:@(node.Level * 16)px;"></div>
|
||||
<MudText Typo="Typo.body2">@node.Title</MudText>
|
||||
@@ -44,13 +48,14 @@
|
||||
ServerData="@(new Func<GridState<DataModel>, Task<GridData<DataModel>>>(ServerReload))"
|
||||
Hover="true"
|
||||
@ref="_gridData"
|
||||
Height="72vh">
|
||||
Height="calc(100vh - 240px)">
|
||||
<ColGroup>
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
<col style="width: 80px;" />
|
||||
</ColGroup>
|
||||
<ToolBarContent>
|
||||
@@ -62,7 +67,7 @@
|
||||
Size="Size.Large"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="CreateNew"
|
||||
Style="cursor:pointer;">
|
||||
>
|
||||
افزودن
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
@@ -85,6 +90,8 @@
|
||||
<MudText Typo="Typo.body2">@parentTitle</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<PropertyColumn Property="x => x.SortOrder" Title="ترتیب" />
|
||||
<PropertyColumn Property="x => x.ProductCount" Title="تعداد محصولات" />
|
||||
<TemplateColumn Title="فعال؟">
|
||||
<CellTemplate>
|
||||
<MudChip Color="@(context.Item.IsActive ? Color.Success : Color.Error)"
|
||||
@@ -102,15 +109,16 @@
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => Update(context.Item))"
|
||||
Style="cursor:pointer;" />
|
||||
/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="حذف">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.DeleteOutline"
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
Disabled="@HasChildren(context.Item.Id)"
|
||||
OnClick="@(() => OnDelete(context.Item))"
|
||||
Style="cursor:pointer;" />
|
||||
/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="مدیریت محصولات این دسته (درگ و دراپ)">
|
||||
@@ -118,17 +126,29 @@
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => OpenCategoryProducts(context.Item))"
|
||||
Style="cursor:pointer;" />
|
||||
/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="افزودن زیردسته">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.CreateNewFolder"
|
||||
Size="Size.Small"
|
||||
Color="Color.Primary"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => CreateSubcategory(context.Item))"
|
||||
/>
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="DataModel"
|
||||
PageSizeOptions="@(new int[] { 30, 60, 90 })"
|
||||
PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد سطرهای صفحه" />
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudItem>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using System.Text.Json;
|
||||
using BackOffice.BFF.Category.Protobuf.Protos.Category;
|
||||
using CMSMicroservice.Protobuf.Protos;
|
||||
using CMSMicroservice.Protobuf.Protos.Category;
|
||||
using BackOffice.Common.BaseComponents;
|
||||
using BackOffice.Common.Utilities;
|
||||
using Mapster;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
using DataModel = BackOffice.BFF.Category.Protobuf.Protos.Category.GetAllCategoryByFilterResponseModel;
|
||||
using DataModel = CMSMicroservice.Protobuf.Protos.Category.GetAllCategoryByFilterResponseModel;
|
||||
|
||||
namespace BackOffice.Pages.Category;
|
||||
|
||||
@@ -151,12 +152,30 @@ public partial class CategoryMainPage
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateSubcategory(DataModel parent)
|
||||
{
|
||||
var dialog = await DialogService.ShowAsync<CreateOrUpdateCategoryDialog>(
|
||||
$"افزودن زیردسته برای «{parent.Title}»",
|
||||
new DialogParameters<CreateOrUpdateCategoryDialog>
|
||||
{
|
||||
{ x => x.Model, new CreateNewCategoryRequest { IsActive = true, ParentId = parent.Id } }
|
||||
},
|
||||
new DialogOptions { CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.Small });
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
await ReloadData();
|
||||
await LoadTreeAsync();
|
||||
Snackbar.Add("زیردسته با موفقیت ایجاد شد.", Severity.Success);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Update(DataModel model)
|
||||
{
|
||||
var getResponse = await CategoryContract.GetCategoryAsync(new GetCategoryRequest { Id = model.Id });
|
||||
|
||||
var editModel = getResponse.Adapt<UpdateCategoryRequest>();
|
||||
// Console.WriteLine(JsonSerializer.Serialize(editModel));
|
||||
var dialog = await DialogService.ShowAsync<CreateOrUpdateCategoryDialog>(
|
||||
"ویرایش دستهبندی",
|
||||
new DialogParameters<CreateOrUpdateCategoryDialog>
|
||||
@@ -179,7 +198,7 @@ public partial class CategoryMainPage
|
||||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small };
|
||||
bool? result = await DialogService.ShowMessageBox(
|
||||
"اخطار",
|
||||
"آیا از حذف این دستهبندی مطمئن هستید؟",
|
||||
$"آیا از حذف دستهبندی «{model.Title}» مطمئن هستید؟",
|
||||
yesText: "حذف",
|
||||
cancelText: "لغو",
|
||||
options: options);
|
||||
@@ -237,4 +256,9 @@ public partial class CategoryMainPage
|
||||
{
|
||||
Navigation.NavigateTo($"{RouteConstance.CategoryProducts}{model.Id}");
|
||||
}
|
||||
|
||||
private bool HasChildren(long categoryId)
|
||||
{
|
||||
return _treeNodes.Any(n => n.ParentId == categoryId);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
@attribute [Route(RouteConstance.CategoryProducts + "{CategoryId:long?}")]
|
||||
|
||||
@using BackOffice.BFF.Products.Protobuf.Protos.Products
|
||||
@using CMSMicroservice.Protobuf.Protos.Products
|
||||
@using BackOffice.Common.BaseComponents
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using BackOffice.BFF.Products.Protobuf.Protos.Products;
|
||||
using BackOffice.BFF.Category.Protobuf.Protos.Category;
|
||||
using CMSMicroservice.Protobuf.Protos.Products;
|
||||
using CMSMicroservice.Protobuf.Protos.Category;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using MudBlazor;
|
||||
|
||||
@@ -1,46 +1,75 @@
|
||||
@using BackOffice.BFF.Category.Protobuf.Protos.Category
|
||||
@using CMSMicroservice.Protobuf.Protos.Category
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6">
|
||||
<MudIcon Icon="@(IsEdit ? Icons.Material.Filled.Edit : Icons.Material.Filled.Add)" Class="ml-2" />
|
||||
@(IsEdit ? "ویرایش دستهبندی" : "ایجاد دستهبندی جدید")
|
||||
</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudStack Spacing="2">
|
||||
<MudTextField T="string"
|
||||
@bind-Value="Name"
|
||||
Label="نام لاتین"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
<MudForm @ref="_form" @bind-IsValid="@_isValid">
|
||||
<MudStack Spacing="2">
|
||||
<MudTextField T="string"
|
||||
@bind-Value="Name"
|
||||
Label="نام لاتین"
|
||||
Required="true"
|
||||
RequiredError="نام لاتین الزامی است"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
|
||||
<MudTextField T="string"
|
||||
@bind-Value="Title"
|
||||
Label="عنوان"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
<MudTextField T="string"
|
||||
@bind-Value="Title"
|
||||
Label="عنوان"
|
||||
Required="true"
|
||||
RequiredError="عنوان الزامی است"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
|
||||
<MudTextField T="string"
|
||||
@bind-Value="Description"
|
||||
Label="توضیحات"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Lines="3"
|
||||
TextArea="true" />
|
||||
<MudTextField T="string"
|
||||
@bind-Value="Description"
|
||||
Label="توضیحات"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Lines="3"
|
||||
TextArea="true" />
|
||||
|
||||
<CategoryAutoComplete Value="ParentId"
|
||||
ValueChanged="OnParentChanged"
|
||||
Label="دستهبندی والد (اختیاری)" />
|
||||
<MudTextField T="string"
|
||||
@bind-Value="ImagePath"
|
||||
Label="آدرس تصویر"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
|
||||
<MudNumericField T="int"
|
||||
@bind-Value="SortOrder"
|
||||
HideSpinButtons="true"
|
||||
Label="ترتیب نمایش"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
<CategoryAutoComplete Value="ParentId"
|
||||
ValueChanged="OnParentChanged"
|
||||
Label="دستهبندی والد (اختیاری)" />
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">برای دسته اصلی خالی بگذارید</MudText>
|
||||
|
||||
<MudCheckBox T="bool" @bind-Value="IsActive" Label="فعال" />
|
||||
</MudStack>
|
||||
<MudNumericField T="int"
|
||||
@bind-Value="SortOrder"
|
||||
HideSpinButtons="true"
|
||||
Label="ترتیب نمایش"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
HelperText="عدد کمتر = اولویت بالاتر" />
|
||||
|
||||
<MudSwitch T="bool" @bind-Value="IsActive" Label="فعال" Color="Color.Success" />
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel">لغو</MudButton>
|
||||
<MudButton Color="Color.Primary" OnClick="Save">ثبت</MudButton>
|
||||
<MudButton OnClick="Cancel" Variant="Variant.Text">لغو</MudButton>
|
||||
<MudButton Color="Color.Primary"
|
||||
Variant="Variant.Filled"
|
||||
OnClick="Save"
|
||||
Disabled="@(!_isValid || _isSaving)">
|
||||
@if (_isSaving)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="ml-2" />
|
||||
}
|
||||
@(IsEdit ? "ذخیره تغییرات" : "ایجاد دستهبندی")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using BackOffice.BFF.Category.Protobuf.Protos.Category;
|
||||
using CMSMicroservice.Protobuf.Protos.Category;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
@@ -14,9 +14,14 @@ public partial class CreateOrUpdateCategoryDialog
|
||||
|
||||
private bool IsEdit => EditModel != null;
|
||||
|
||||
private MudForm? _form;
|
||||
private bool _isValid;
|
||||
private bool _isSaving;
|
||||
|
||||
private string Name { get; set; } = string.Empty;
|
||||
private string Title { get; set; } = string.Empty;
|
||||
private string? Description { get; set; }
|
||||
private string? ImagePath { get; set; }
|
||||
private long? ParentId { get; set; }
|
||||
private bool IsActive { get; set; } = true;
|
||||
private int SortOrder { get; set; }
|
||||
@@ -28,9 +33,9 @@ public partial class CreateOrUpdateCategoryDialog
|
||||
Name = EditModel.Name;
|
||||
Title = EditModel.Title;
|
||||
Description = EditModel.Description;
|
||||
ImagePath = EditModel.ImagePath;
|
||||
ParentId = EditModel.ParentId;
|
||||
IsActive = EditModel.IsActive;
|
||||
Console.WriteLine(EditModel.IsActive);
|
||||
SortOrder = EditModel.SortOrder;
|
||||
}
|
||||
else if (Model != null)
|
||||
@@ -38,6 +43,7 @@ public partial class CreateOrUpdateCategoryDialog
|
||||
Name = Model.Name;
|
||||
Title = Model.Title;
|
||||
Description = Model.Description;
|
||||
ImagePath = Model.ImagePath;
|
||||
ParentId = Model.ParentId;
|
||||
IsActive = Model.IsActive;
|
||||
SortOrder = Model.SortOrder;
|
||||
@@ -46,37 +52,60 @@ public partial class CreateOrUpdateCategoryDialog
|
||||
|
||||
private async Task Save()
|
||||
{
|
||||
if (IsEdit && EditModel != null)
|
||||
if (_form != null)
|
||||
{
|
||||
var request = new UpdateCategoryRequest
|
||||
{
|
||||
Id = EditModel.Id,
|
||||
Name = Name,
|
||||
Title = Title,
|
||||
Description = Description,
|
||||
ParentId = ParentId,
|
||||
IsActive = IsActive,
|
||||
SortOrder = SortOrder
|
||||
};
|
||||
|
||||
await CategoryContract.UpdateCategoryAsync(request);
|
||||
}
|
||||
else
|
||||
{
|
||||
var request = new CreateNewCategoryRequest
|
||||
{
|
||||
Name = Name,
|
||||
Title = Title,
|
||||
Description = Description,
|
||||
ParentId = ParentId,
|
||||
IsActive = IsActive,
|
||||
SortOrder = SortOrder
|
||||
};
|
||||
|
||||
await CategoryContract.CreateNewCategoryAsync(request);
|
||||
await _form.Validate();
|
||||
if (!_isValid) return;
|
||||
}
|
||||
|
||||
MudDialog.Close(DialogResult.Ok(true));
|
||||
_isSaving = true;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
if (IsEdit && EditModel != null)
|
||||
{
|
||||
var request = new UpdateCategoryRequest
|
||||
{
|
||||
Id = EditModel.Id,
|
||||
Name = Name,
|
||||
Title = Title,
|
||||
Description = Description,
|
||||
ImagePath = ImagePath,
|
||||
ParentId = ParentId,
|
||||
IsActive = IsActive,
|
||||
SortOrder = SortOrder
|
||||
};
|
||||
|
||||
await CategoryContract.UpdateCategoryAsync(request);
|
||||
}
|
||||
else
|
||||
{
|
||||
var request = new CreateNewCategoryRequest
|
||||
{
|
||||
Name = Name,
|
||||
Title = Title,
|
||||
Description = Description,
|
||||
ImagePath = ImagePath,
|
||||
ParentId = ParentId,
|
||||
IsActive = IsActive,
|
||||
SortOrder = SortOrder
|
||||
};
|
||||
|
||||
await CategoryContract.CreateNewCategoryAsync(request);
|
||||
}
|
||||
|
||||
MudDialog.Close(DialogResult.Ok(true));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSaving = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private Task OnParentChanged(long? parentId)
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
@page "/club/features"
|
||||
@attribute [Authorize(Roles = "Administrator")]
|
||||
|
||||
@using CMSMicroservice.Protobuf.Protos.Configuration
|
||||
@using CMSMicroservice.Protobuf.Protos.ClubMembership
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
|
||||
@inject ConfigurationContract.ConfigurationContractClient ConfigClient
|
||||
@inject ClubMembershipContract.ClubMembershipContractClient ClubClient
|
||||
|
||||
<div>
|
||||
<MudText Typo="Typo.h5" Class="mb-3">فیچرهای باشگاه</MudText>
|
||||
|
||||
<MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true"
|
||||
PanelClass="pt-4" @bind-ActivePanelIndex="_activeTab">
|
||||
|
||||
@* ── تب اول: لیست فیچرها ── *@
|
||||
<MudTabPanel Text="فیچرهای تعریفشده" Icon="@Icons.Material.Filled.Star">
|
||||
@if (_loadingFeatures)
|
||||
{
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
|
||||
}
|
||||
else if (_features.Count == 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text">فیچری تعریف نشده است.</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudGrid Spacing="2">
|
||||
@foreach (var feature in _features)
|
||||
{
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
|
||||
<MudStack Spacing="1">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.subtitle1"><strong>@feature.Title</strong></MudText>
|
||||
<MudChip T="string" Size="Size.Small"
|
||||
Color="@(feature.IsEnabled ? Color.Success : Color.Error)">
|
||||
@(feature.IsEnabled ? "فعال" : "غیرفعال")
|
||||
</MudChip>
|
||||
</MudStack>
|
||||
@if (!string.IsNullOrWhiteSpace(feature.Description))
|
||||
{
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">@feature.Description</MudText>
|
||||
}
|
||||
<MudText Typo="Typo.caption">ترتیب: @feature.DisplayOrder</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
}
|
||||
</MudTabPanel>
|
||||
|
||||
@* ── تب دوم: فیچرهای کاربران ── *@
|
||||
<MudTabPanel Text="فیچرهای کاربران" Icon="@Icons.Material.Filled.PeopleAlt">
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center" Class="mb-3">
|
||||
<UserAutoComplete Label="جستجوی کاربر" @bind-SelectedUserId="_featureFilterUserId" />
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Search"
|
||||
OnClick="LoadUserFeatures">جستجو</MudButton>
|
||||
</MudStack>
|
||||
|
||||
@if (_loadingUserFeatures)
|
||||
{
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
|
||||
}
|
||||
else if (_featureFilterUserId == null || _featureFilterUserId == 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text">لطفاً یک کاربر انتخاب کنید.</MudAlert>
|
||||
}
|
||||
else if (_userFeatures.Count == 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Text">فیچری برای این کاربر یافت نشد.</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudDataGrid T="UserClubFeatureModel" Items="_userFeatures"
|
||||
Hover="true" Dense="true"
|
||||
Height="calc(100vh - 340px)">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
<PropertyColumn Property="x => x.FeatureTitle" Title="عنوان فیچر" />
|
||||
<TemplateColumn Title="توضیحات">
|
||||
<CellTemplate>
|
||||
<MudText Typo="Typo.body2">@(string.IsNullOrWhiteSpace(context.Item.FeatureDescription) ? "—" : context.Item.FeatureDescription)</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="وضعیت">
|
||||
<CellTemplate>
|
||||
<MudSwitch T="bool" Value="context.Item.IsActive"
|
||||
Color="Color.Success"
|
||||
ValueChanged="@(val => ToggleFeature(context.Item, val))" />
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="تاریخ اختصاص">
|
||||
<CellTemplate>
|
||||
@if (context.Item.GrantedAt != null)
|
||||
{
|
||||
@context.Item.GrantedAt.ToDateTime().ToLocalTime().MiladiToJalali()
|
||||
}
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<PropertyColumn Property="x => x.Notes" Title="یادداشت" />
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="UserClubFeatureModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
}
|
||||
</MudTabPanel>
|
||||
</MudTabs>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private int _activeTab;
|
||||
private bool _loadingFeatures;
|
||||
private bool _loadingUserFeatures;
|
||||
|
||||
// Features tab
|
||||
private List<ClubFeatureModel> _features = new();
|
||||
|
||||
// User features tab
|
||||
private long? _featureFilterUserId;
|
||||
private List<UserClubFeatureModel> _userFeatures = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadFeatures();
|
||||
}
|
||||
|
||||
private async Task LoadFeatures()
|
||||
{
|
||||
_loadingFeatures = true;
|
||||
try
|
||||
{
|
||||
var result = await ConfigClient.GetClubFeaturesAsync(new Google.Protobuf.WellKnownTypes.Empty());
|
||||
_features = result?.Features?.ToList() ?? new();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری فیچرها: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loadingFeatures = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadUserFeatures()
|
||||
{
|
||||
if (_featureFilterUserId == null || _featureFilterUserId == 0) return;
|
||||
|
||||
_loadingUserFeatures = true;
|
||||
try
|
||||
{
|
||||
var result = await ClubClient.GetUserClubFeaturesAsync(new GetUserClubFeaturesRequest
|
||||
{
|
||||
UserId = _featureFilterUserId.Value
|
||||
});
|
||||
_userFeatures = result?.Features?.ToList() ?? new();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loadingUserFeatures = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ToggleFeature(UserClubFeatureModel feature, bool isActive)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ClubClient.ToggleUserClubFeatureAsync(new ToggleUserClubFeatureRequest
|
||||
{
|
||||
UserId = feature.UserId,
|
||||
ClubFeatureId = feature.ClubFeatureId,
|
||||
IsActive = isActive
|
||||
});
|
||||
|
||||
feature.IsActive = isActive;
|
||||
Snackbar.Add($"فیچر «{feature.FeatureTitle}» {(isActive ? "فعال" : "غیرفعال")} شد", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
@page "/club"
|
||||
@page "/club/members"
|
||||
@page "/club/statistics"
|
||||
@attribute [Authorize]
|
||||
|
||||
<div>
|
||||
<MudText Typo="Typo.h5" Class="mb-3">باشگاه مشتریان</MudText>
|
||||
|
||||
<MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true"
|
||||
PanelClass="pt-4" @bind-ActivePanelIndex="_activeTab">
|
||||
<MudTabPanel Text="اعضای باشگاه" Icon="@Icons.Material.Filled.Groups">
|
||||
<ClubMembers />
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="آمار باشگاه" Icon="@Icons.Material.Filled.PieChart">
|
||||
<Statistics />
|
||||
</MudTabPanel>
|
||||
</MudTabs>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private int _activeTab;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
var uri = Navigation.ToBaseRelativePath(Navigation.Uri).ToLowerInvariant().TrimEnd('/');
|
||||
_activeTab = uri switch
|
||||
{
|
||||
"club/statistics" => 1,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
@page "/club/members"
|
||||
@* Merged into /club *@
|
||||
@attribute [Authorize]
|
||||
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
@using BackOffice.Pages.Club.Components
|
||||
@using Foursat.BackOffice.BFF.ClubMembership.Protos
|
||||
@using CMSMicroservice.Protobuf.Protos.ClubMembership
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<div>
|
||||
<MudText Typo="Typo.h4" Class="mb-4">مدیریت اعضای باشگاه</MudText>
|
||||
|
||||
<MudDataGrid T="ClubMembershipModel"
|
||||
ServerData="@(new Func<GridState<ClubMembershipModel>, Task<GridData<ClubMembershipModel>>>(ServerReload))"
|
||||
Hover="true"
|
||||
@ref="_gridData"
|
||||
Height="75vh">
|
||||
Height="calc(100vh - 240px)">
|
||||
<ToolBarContent>
|
||||
<MudText Typo="Typo.h6">لیست اعضا</MudText>
|
||||
<MudSpacer />
|
||||
@@ -32,6 +32,12 @@
|
||||
StartIcon="@Icons.Material.Filled.Add">
|
||||
فعالسازی عضو جدید
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Info"
|
||||
OnClick="ExportToExcel"
|
||||
StartIcon="@Icons.Material.Filled.FileDownload">
|
||||
خروجی Excel
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</ToolBarContent>
|
||||
<Columns>
|
||||
@@ -58,14 +64,14 @@
|
||||
|
||||
<PropertyColumn Property="x => x.ActivatedAt" Title="تاریخ فعالسازی">
|
||||
<CellTemplate>
|
||||
@context.Item.ActivatedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd")
|
||||
@(context.Item.ActivatedAt != null ? context.Item.ActivatedAt.ToDateTime().ToLocalTime().MiladiToJalali() : "—")
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.ExpiresAt" Title="تاریخ انقضا">
|
||||
<CellTemplate>
|
||||
<MudText Color="@(context.Item.IsExpired ? Color.Error : Color.Success)">
|
||||
@context.Item.ExpiresAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd")
|
||||
@(context.Item.ExpiresAt != null ? context.Item.ExpiresAt.ToDateTime().ToLocalTime().MiladiToJalali() : "—")
|
||||
</MudText>
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
@@ -102,11 +108,14 @@
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="ClubMembershipModel"
|
||||
PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد سطرهای صفحه" />
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudContainer>
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,17 @@ using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using BackOffice.Pages.Club.Components;
|
||||
using Foursat.BackOffice.BFF.ClubMembership.Protos;
|
||||
using CMSMicroservice.Protobuf.Protos.ClubMembership;
|
||||
using Microsoft.JSInterop;
|
||||
using System.Text;
|
||||
using DateTimeConverterCL;
|
||||
|
||||
namespace BackOffice.Pages.Club;
|
||||
|
||||
public partial class ClubMembers
|
||||
{
|
||||
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubContract { get; set; }
|
||||
[Inject] public IJSRuntime JsRuntime { get; set; }
|
||||
|
||||
private MudDataGrid<ClubMembershipModel> _gridData;
|
||||
private bool _filterIsActive = true;
|
||||
@@ -108,4 +112,61 @@ public partial class ClubMembers
|
||||
await ApplyFilter();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExportToExcel()
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new GetAllClubMembershipsRequest
|
||||
{
|
||||
PageIndex = 1,
|
||||
PageSize = 1000,
|
||||
IsActive = _filterIsActive
|
||||
};
|
||||
|
||||
var result = await ClubContract.GetAllClubMembershipsAsync(request);
|
||||
|
||||
if (result?.Models == null || !result.Models.Any())
|
||||
{
|
||||
Snackbar.Add("دادهای برای خروجی وجود ندارد.", Severity.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("شناسه,کاربر,نام کاربر,پکیج,کد فعالسازی,تاریخ فعالسازی,تاریخ انقضا,فعال,منقضی");
|
||||
|
||||
foreach (var m in result.Models)
|
||||
{
|
||||
sb.AppendLine(string.Join(",",
|
||||
m.Id,
|
||||
m.UserId,
|
||||
EscapeCsv(m.UserName),
|
||||
EscapeCsv(m.PackageName),
|
||||
EscapeCsv(m.ActivationCode),
|
||||
m.ActivatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime(),
|
||||
m.ExpiresAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime(),
|
||||
m.IsActive ? "فعال" : "غیرفعال",
|
||||
m.IsExpired ? "بله" : "خیر"));
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
var filename = $"club-members-{DateTime.Now:yyyyMMddHHmmss}.csv";
|
||||
|
||||
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
|
||||
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private static string EscapeCsv(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return "";
|
||||
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
|
||||
return $"\"{value.Replace("\"", "\"\"")}\"";
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@using Foursat.BackOffice.BFF.ClubMembership.Protos
|
||||
@using CMSMicroservice.Protobuf.Protos.ClubMembership
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@using Foursat.BackOffice.BFF.ClubMembership.Protos
|
||||
@using CMSMicroservice.Protobuf.Protos.ClubMembership
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@using Foursat.BackOffice.BFF.ClubMembership.Protos
|
||||
@using CMSMicroservice.Protobuf.Protos.ClubMembership
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
@@ -41,13 +41,13 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>تاریخ فعالسازی:</strong></td>
|
||||
<td>@Member.ActivatedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm")</td>
|
||||
<td>@Member.ActivatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>تاریخ انقضا:</strong></td>
|
||||
<td>
|
||||
<MudText Color="@(Member.IsExpired ? Color.Error : Color.Success)">
|
||||
@Member.ExpiresAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm")
|
||||
@Member.ExpiresAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()
|
||||
</MudText>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -82,10 +82,38 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>تاریخ ایجاد:</strong></td>
|
||||
<td>@Member.Created.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss")</td>
|
||||
<td>@Member.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
|
||||
@if (_historyLoading)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" Color="Color.Primary" Class="my-2" />
|
||||
}
|
||||
else if (_history.Any())
|
||||
{
|
||||
<MudTable T="ClubMembershipHistoryModel" Items="_history" Dense="true" Hover="true" Class="mt-2">
|
||||
<HeaderContent>
|
||||
<MudTh>عملیات</MudTh>
|
||||
<MudTh>تاریخ</MudTh>
|
||||
<MudTh>دلیل</MudTh>
|
||||
<MudTh>توسط</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="عملیات">
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Info">@context.Action</MudChip>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="تاریخ">@context.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</MudTd>
|
||||
<MudTd DataLabel="دلیل">@(string.IsNullOrEmpty(context.Reason) ? "-" : context.Reason)</MudTd>
|
||||
<MudTd DataLabel="توسط">@(string.IsNullOrEmpty(context.PerformedBy) ? "-" : context.PerformedBy)</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="mt-2">تاریخچهای یافت نشد.</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
</MudStack>
|
||||
}
|
||||
@@ -98,6 +126,39 @@
|
||||
@code {
|
||||
[CascadingParameter] IMudDialogInstance MudDialog { get; set; }
|
||||
[Parameter] public ClubMembershipModel Member { get; set; }
|
||||
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubClient { get; set; }
|
||||
|
||||
private List<ClubMembershipHistoryModel> _history = new();
|
||||
private bool _historyLoading;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadHistory();
|
||||
}
|
||||
|
||||
private async Task LoadHistory()
|
||||
{
|
||||
_historyLoading = true;
|
||||
try
|
||||
{
|
||||
var request = new GetClubMembershipHistoryRequest
|
||||
{
|
||||
UserId = Member.UserId,
|
||||
PageIndex = 0,
|
||||
PageSize = 50
|
||||
};
|
||||
var response = await ClubClient.GetClubMembershipHistoryAsync(request);
|
||||
_history = response.Models?.ToList() ?? new();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_history = new();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_historyLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Close() => MudDialog.Close();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
@page "/club/statistics"
|
||||
@* Merged into /club *@
|
||||
|
||||
@using MudBlazor
|
||||
@using Foursat.BackOffice.BFF.ClubMembership.Protos
|
||||
@using CMSMicroservice.Protobuf.Protos.ClubMembership
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<div>
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">آمار باشگاه</MudText>
|
||||
<MudText Typo="Typo.body1" Color="Color.Secondary" Class="mb-4">
|
||||
نمای کلی از وضعیت عضویتهای باشگاه
|
||||
@@ -139,7 +139,7 @@
|
||||
<MudTh>عملیات</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="تاریخ">@context.ActivatedAt.ToString("yyyy/MM/dd")</MudTd>
|
||||
<MudTd DataLabel="تاریخ">@context.ActivatedAt.MiladiToJalali()</MudTd>
|
||||
<MudTd DataLabel="شناسه کاربر">@context.UserId</MudTd>
|
||||
<MudTd DataLabel="نام کاربر">@context.UserName</MudTd>
|
||||
<MudTd DataLabel="پکیج">
|
||||
@@ -166,7 +166,7 @@
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
}
|
||||
</MudContainer>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubClient { get; set; }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@using Foursat.BackOffice.BFF.Commission.Protos
|
||||
@using CMSMicroservice.Protobuf.Protos.Commission
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
@@ -23,12 +23,12 @@
|
||||
</MudPaper>
|
||||
|
||||
<MudPaper Class="pa-3" Elevation="0">
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary">جزئیات Payout</MudText>
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary">جزئیات پرداخت</MudText>
|
||||
<MudDivider Class="my-2" />
|
||||
<MudSimpleTable Dense="true">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>شناسه Payout:</strong></td>
|
||||
<td><strong>شناسه پرداخت:</strong></td>
|
||||
<td>@Payout.Id</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -89,11 +89,12 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>تاریخ ایجاد:</strong></td>
|
||||
<td>@Payout.Created.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss")</td>
|
||||
<td>@Payout.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>آخرین ویرایش:</strong></td>
|
||||
<td>@Payout.LastModified.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss")</td>
|
||||
@* CMS UserCommissionPayoutModel does not have LastModified - using Created as fallback *@
|
||||
<td>@Payout.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudStack Spacing="3">
|
||||
<MudAlert Severity="Severity.Warning" Dense="true">
|
||||
درخواست برداشت @Amount.ToString("N0") ریال برای @UserName رد خواهد شد.
|
||||
</MudAlert>
|
||||
<MudTextField @bind-Value="_reason"
|
||||
Label="دلیل رد درخواست"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="3"
|
||||
Required="true"
|
||||
RequiredError="لطفاً دلیل رد درخواست را وارد کنید" />
|
||||
</MudStack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel">انصراف</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Error" OnClick="Submit"
|
||||
Disabled="@string.IsNullOrWhiteSpace(_reason)">
|
||||
رد درخواست
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
[Parameter] public string UserName { get; set; } = "";
|
||||
[Parameter] public long Amount { get; set; }
|
||||
|
||||
private string _reason = "رد توسط مدیر";
|
||||
|
||||
private void Cancel() => MudDialog.Cancel();
|
||||
private void Submit() => MudDialog.Close(DialogResult.Ok(_reason));
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudStack Spacing="3">
|
||||
<MudPaper Class="pa-3" Elevation="0" Outlined="true">
|
||||
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="mb-2">اطلاعات کاربر</MudText>
|
||||
<MudSimpleTable Dense="true">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>نام کاربر:</strong></td>
|
||||
<td>@Request.UserName</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>شناسه کاربر:</strong></td>
|
||||
<td>@Request.UserId</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
</MudPaper>
|
||||
|
||||
<MudPaper Class="pa-3" Elevation="0" Outlined="true">
|
||||
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="mb-2">اطلاعات درخواست</MudText>
|
||||
<MudSimpleTable Dense="true">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>شناسه درخواست:</strong></td>
|
||||
<td>@Request.Id</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>مبلغ:</strong></td>
|
||||
<td><MudText Color="Color.Primary"><strong>@Request.Amount.ToString("N0") ریال</strong></MudText></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>روش برداشت:</strong></td>
|
||||
<td>
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Info">@MethodText</MudChip>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>وضعیت:</strong></td>
|
||||
<td>
|
||||
<MudChip T="string" Size="Size.Small" Color="@GetStatusColor()">@StatusText</MudChip>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>تاریخ درخواست:</strong></td>
|
||||
<td>@Request.RequestedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td>
|
||||
</tr>
|
||||
@if (Request.ProcessedDate.HasValue)
|
||||
{
|
||||
<tr>
|
||||
<td><strong>تاریخ پردازش:</strong></td>
|
||||
<td>@Request.ProcessedDate.Value.MiladiToJalaliWithTime()</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
</MudPaper>
|
||||
|
||||
<MudPaper Class="pa-3" Elevation="0" Outlined="true">
|
||||
<MudText Typo="Typo.subtitle1" Color="Color.Primary" Class="mb-2">اطلاعات بانکی</MudText>
|
||||
<MudSimpleTable Dense="true">
|
||||
<tbody>
|
||||
@if (!string.IsNullOrWhiteSpace(Request.IbanNumber))
|
||||
{
|
||||
<tr>
|
||||
<td><strong>شماره شبا:</strong></td>
|
||||
<td>@Request.IbanNumber</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(Request.BankReferenceId))
|
||||
{
|
||||
<tr>
|
||||
<td><strong>شماره مرجع بانک:</strong></td>
|
||||
<td>@Request.BankReferenceId</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(Request.BankTrackingCode))
|
||||
{
|
||||
<tr>
|
||||
<td><strong>کد پیگیری بانکی:</strong></td>
|
||||
<td>@Request.BankTrackingCode</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(Request.PaymentFailureReason))
|
||||
{
|
||||
<tr>
|
||||
<td><strong>دلیل خطای پرداخت:</strong></td>
|
||||
<td><MudText Color="Color.Error">@Request.PaymentFailureReason</MudText></td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(Request.AdminNote))
|
||||
{
|
||||
<tr>
|
||||
<td><strong>یادداشت مدیر:</strong></td>
|
||||
<td>@Request.AdminNote</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
</MudPaper>
|
||||
</MudStack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel">بستن</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
[Parameter] public WithdrawalRequestModel Request { get; set; } = null!;
|
||||
[Parameter] public string StatusText { get; set; } = "";
|
||||
[Parameter] public string MethodText { get; set; } = "";
|
||||
|
||||
private Color GetStatusColor()
|
||||
{
|
||||
return Request.Status switch
|
||||
{
|
||||
0 => Color.Warning,
|
||||
1 => Color.Info,
|
||||
2 => Color.Warning,
|
||||
3 => Color.Success,
|
||||
4 => Color.Error,
|
||||
5 => Color.Default,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private void Cancel() => MudDialog.Cancel();
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
@page "/commission/dashboard"
|
||||
@attribute [Authorize]
|
||||
|
||||
@using Foursat.BackOffice.BFF.Commission.Protos
|
||||
@using CMSMicroservice.Protobuf.Protos.Commission
|
||||
@using MudBlazor
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<div>
|
||||
<MudText Typo="Typo.h4" Class="mb-4">داشبورد کمیسیون</MudText>
|
||||
|
||||
@if (_isLoading)
|
||||
@@ -90,7 +90,7 @@
|
||||
|
||||
@if (_availableWeeks?.CurrentWeek != null)
|
||||
{
|
||||
<MudSelectItem Value="@_availableWeeks.CurrentWeek.WeekDefinitionId">
|
||||
<MudSelectItem T="long" Value="@_availableWeeks.CurrentWeek.WeekDefinitionId">
|
||||
<MudText>
|
||||
<MudIcon Icon="@Icons.Material.Filled.Today" Size="Size.Small" Class="ml-2" />
|
||||
@_availableWeeks.CurrentWeek.DisplayText
|
||||
@@ -104,7 +104,7 @@
|
||||
<MudListSubheader>هفتههای محاسبه شده</MudListSubheader>
|
||||
@foreach (var week in _availableWeeks.CalculatedWeeks)
|
||||
{
|
||||
<MudSelectItem Value="@week.WeekDefinitionId">
|
||||
<MudSelectItem T="long" Value="@week.WeekDefinitionId">
|
||||
<MudText>
|
||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" Class="ml-2" />
|
||||
@week.DisplayText
|
||||
@@ -125,7 +125,7 @@
|
||||
<MudListSubheader>هفتههای محاسبه نشده</MudListSubheader>
|
||||
@foreach (var week in _availableWeeks.PendingWeeks)
|
||||
{
|
||||
<MudSelectItem Value="@week.WeekDefinitionId">
|
||||
<MudSelectItem T="long" Value="@week.WeekDefinitionId">
|
||||
<MudText>
|
||||
<MudIcon Icon="@Icons.Material.Filled.AccessTime" Color="Color.Warning" Size="Size.Small" Class="ml-2" />
|
||||
@week.DisplayText
|
||||
@@ -140,7 +140,7 @@
|
||||
<MudListSubheader>هفتههای آینده</MudListSubheader>
|
||||
@foreach (var week in _availableWeeks.FutureWeeks)
|
||||
{
|
||||
<MudSelectItem Value="@week.WeekDefinitionId">
|
||||
<MudSelectItem T="long" Value="@week.WeekDefinitionId">
|
||||
<MudText>
|
||||
<MudIcon Icon="@Icons.Material.Filled.CalendarMonth" Color="Color.Info" Size="Size.Small" Class="ml-2" />
|
||||
@week.DisplayText
|
||||
@@ -204,7 +204,7 @@
|
||||
<td>
|
||||
@if (_poolData.CalculatedAt != null)
|
||||
{
|
||||
@_poolData.CalculatedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm")
|
||||
@_poolData.CalculatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -214,7 +214,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>تاریخ ایجاد:</strong></td>
|
||||
<td>@_poolData.Created.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm")</td>
|
||||
<td>@_poolData.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
@@ -229,7 +229,7 @@
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Payments"
|
||||
Href="/commission/payouts">
|
||||
مشاهده Payout ها
|
||||
مشاهده پرداختها
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Secondary"
|
||||
@@ -247,4 +247,4 @@
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
}
|
||||
</MudContainer>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using Foursat.BackOffice.BFF.Commission.Protos;
|
||||
using CMSMicroservice.Protobuf.Protos.Commission;
|
||||
using BackOffice.Services;
|
||||
|
||||
namespace BackOffice.Pages.Commission;
|
||||
@@ -24,8 +23,8 @@ public partial class Dashboard
|
||||
private GetWeeklyCommissionPoolResponse? _poolData;
|
||||
|
||||
// Available weeks data
|
||||
private Foursat.BackOffice.BFF.Commission.Protos.GetAvailableWeeksResponse? _availableWeeks;
|
||||
private List<Foursat.BackOffice.BFF.Commission.Protos.WeekInfo> _allWeeks = new();
|
||||
private CMSMicroservice.Protobuf.Protos.Commission.GetAvailableWeeksResponse? _availableWeeks;
|
||||
private List<CMSMicroservice.Protobuf.Protos.Commission.WeekInfo> _allWeeks = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
@@ -111,7 +110,6 @@ public partial class Dashboard
|
||||
};
|
||||
|
||||
_poolData = await CommissionContract.GetWeeklyCommissionPoolAsync(request);
|
||||
Console.WriteLine(JsonSerializer.Serialize(_poolData));
|
||||
Snackbar.Add($"اطلاعات Pool هفته {_currentWeekDisplayName} بارگذاری شد", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
@page "/commission/payouts"
|
||||
@attribute [Authorize]
|
||||
|
||||
@using Foursat.BackOffice.BFF.Commission.Protos
|
||||
@using CMSMicroservice.Protobuf.Protos.Commission
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
@using MudBlazor
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<div>
|
||||
<MudText Typo="Typo.h4" Class="mb-4"> پرداخت های کاربران</MudText>
|
||||
|
||||
<MudDataGrid T="UserCommissionPayoutModel"
|
||||
ServerData="@(new Func<GridState<UserCommissionPayoutModel>, Task<GridData<UserCommissionPayoutModel>>>(ServerReload))"
|
||||
Hover="true"
|
||||
@ref="_gridData"
|
||||
Height="75vh">
|
||||
Height="calc(100vh - 240px)">
|
||||
<ToolBarContent>
|
||||
<MudText Typo="Typo.h6">لیست پرداخت ها</MudText>
|
||||
<MudSpacer />
|
||||
@@ -28,13 +28,13 @@
|
||||
Margin="Margin.Dense"
|
||||
Clearable="true"
|
||||
Style="max-width: 150px;">
|
||||
<MudSelectItem Value="@((int?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem Value="@((int?)0)">در انتظار</MudSelectItem>
|
||||
<MudSelectItem Value="@((int?)1)">پرداخت شده</MudSelectItem>
|
||||
<MudSelectItem Value="@((int?)2)">درخواست برداشت</MudSelectItem>
|
||||
<MudSelectItem Value="@((int?)3)">برداشت شده</MudSelectItem>
|
||||
<MudSelectItem Value="@((int?)4)">شکست خورده</MudSelectItem>
|
||||
<MudSelectItem Value="@((int?)5)">لغو شده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)0)">در انتظار</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)1)">پرداخت شده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)2)">درخواست برداشت</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)3)">برداشت شده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)4)">شکست خورده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)5)">لغو شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
@@ -50,7 +50,9 @@
|
||||
<PropertyColumn Property="x => x.UserId" Title="کاربر">
|
||||
<CellTemplate>
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.body2"><strong>ID:</strong> @context.Item.UserId</MudText>
|
||||
<MudLink Href="@($"/network/user-info/{context.Item.UserId}")" Underline="Underline.Hover" Color="Color.Primary">
|
||||
<strong>ID:</strong> @context.Item.UserId
|
||||
</MudLink>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">@context.Item.UserName</MudText>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
@@ -131,11 +133,14 @@
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="UserCommissionPayoutModel"
|
||||
PageSizeOptions="@(new int[] { 10, 25, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد سطرهای صفحه" />
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudContainer>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using BackOffice.BFF.Protobuf.Common;
|
||||
using Foursat.BackOffice.BFF.Commission.Protos;
|
||||
using CMSMicroservice.Protobuf.Protos;
|
||||
using CMSMicroservice.Protobuf.Protos.Commission;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
@@ -22,29 +22,26 @@ public partial class UserPayouts
|
||||
{
|
||||
try
|
||||
{
|
||||
// CMS proto uses flat fields instead of nested PaginationState/Filter
|
||||
var request = new GetUserCommissionPayoutsRequest
|
||||
{
|
||||
PaginationState = new PaginationState()
|
||||
{
|
||||
PageNumber = state.Page + 1,
|
||||
PageSize = state.PageSize
|
||||
},
|
||||
Filter = new GetUserPayoutsFilter()
|
||||
PageIndex = state.Page + 1,
|
||||
PageSize = state.PageSize
|
||||
};
|
||||
|
||||
if (_filterUserId.HasValue && _filterUserId.Value > 0)
|
||||
{
|
||||
request.Filter.UserId = _filterUserId.Value;
|
||||
request.UserId = _filterUserId.Value;
|
||||
}
|
||||
|
||||
if (_filterWeekDefinitionId.HasValue && _filterWeekDefinitionId.Value > 0)
|
||||
{
|
||||
request.Filter.WeekDefinitionId = _filterWeekDefinitionId.Value;
|
||||
request.WeekDefinitionId = _filterWeekDefinitionId.Value;
|
||||
}
|
||||
|
||||
if (_filterStatus.HasValue)
|
||||
{
|
||||
request.Filter.Status = _filterStatus.Value;
|
||||
request.Status = _filterStatus.Value;
|
||||
}
|
||||
|
||||
var result = await CommissionContract.GetUserCommissionPayoutsAsync(request);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
@page "/commission/reports"
|
||||
|
||||
@using MudBlazor
|
||||
@using Foursat.BackOffice.BFF.Commission.Protos
|
||||
@using CMSMicroservice.Protobuf.Protos.Commission
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<div>
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">گزارشهای هفتگی کمیسیون</MudText>
|
||||
<MudText Typo="Typo.body1" Color="Color.Secondary" Class="mb-4">
|
||||
مشاهده تاریخچه محاسبات کمیسیون هفتگی
|
||||
@@ -14,29 +15,32 @@
|
||||
<MudCardContent>
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudTextField @bind-Value="_filterFromWeek"
|
||||
Label="از هفته"
|
||||
Placeholder="2025-W01"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.DateRange" />
|
||||
<WeekNumberPicker Label="از هفته"
|
||||
@bind-SelectedWeekDefinitionId="_filterFromWeekId"
|
||||
PastWeeksCount="24" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudTextField @bind-Value="_filterToWeek"
|
||||
Label="تا هفته"
|
||||
Placeholder="2025-W48"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.DateRange" />
|
||||
<WeekNumberPicker Label="تا هفته"
|
||||
@bind-SelectedWeekDefinitionId="_filterToWeekId"
|
||||
PastWeeksCount="24" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4" Class="d-flex align-center">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
OnClick="ApplyFilter"
|
||||
StartIcon="@Icons.Material.Filled.Search"
|
||||
FullWidth="true">
|
||||
جستجو
|
||||
</MudButton>
|
||||
<MudStack Row="true" Spacing="2" Style="width:100%">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
OnClick="ApplyFilter"
|
||||
StartIcon="@Icons.Material.Filled.Search"
|
||||
FullWidth="true">
|
||||
جستجو
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Info"
|
||||
OnClick="ExportToExcel"
|
||||
StartIcon="@Icons.Material.Filled.FileDownload"
|
||||
FullWidth="true">
|
||||
خروجی Excel
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudCardContent>
|
||||
@@ -85,7 +89,7 @@
|
||||
<CellTemplate>
|
||||
@if (context.Item.CalculatedAt != null)
|
||||
{
|
||||
<MudText>@context.Item.CalculatedAt.ToDateTime().ToString("yyyy/MM/dd HH:mm")</MudText>
|
||||
<MudText>@context.Item.CalculatedAt.ToDateTime().MiladiToJalaliWithTime()</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -151,15 +155,15 @@
|
||||
</MudGrid>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudContainer>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Inject] public CommissionContract.CommissionContractClient CommissionClient { get; set; }
|
||||
|
||||
private List<WeeklyPoolReportModel> _reports = new();
|
||||
private bool _loading = false;
|
||||
private string _filterFromWeek = "";
|
||||
private string _filterToWeek = "";
|
||||
private long? _filterFromWeekId;
|
||||
private long? _filterToWeekId;
|
||||
|
||||
// Statistics
|
||||
private long _totalPoolSum = 0;
|
||||
@@ -183,14 +187,14 @@
|
||||
PageSize = 100
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_filterFromWeek))
|
||||
if (_filterFromWeekId.HasValue && _filterFromWeekId.Value > 0)
|
||||
{
|
||||
request.FromWeek = _filterFromWeek;
|
||||
request.FromWeekDefinitionId = _filterFromWeekId.Value;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_filterToWeek))
|
||||
if (_filterToWeekId.HasValue && _filterToWeekId.Value > 0)
|
||||
{
|
||||
request.ToWeek = _filterToWeek;
|
||||
request.ToWeekDefinitionId = _filterToWeekId.Value;
|
||||
}
|
||||
|
||||
var response = await CommissionClient.GetAllWeeklyPoolsAsync(request);
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
using System.Text;
|
||||
using DateTimeConverterCL;
|
||||
using Microsoft.JSInterop;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Commission;
|
||||
|
||||
public partial class WeeklyReports
|
||||
{
|
||||
private async Task ExportToExcel()
|
||||
{
|
||||
if (_reports == null || !_reports.Any())
|
||||
{
|
||||
Snackbar.Add("دادهای برای خروجی وجود ندارد.", Severity.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("هفته,مبلغ کل استخر,مجموع موجودیها,ارزش هر موجودی,وضعیت,تاریخ محاسبه");
|
||||
|
||||
foreach (var r in _reports)
|
||||
{
|
||||
sb.AppendLine(string.Join(",",
|
||||
EscapeCsv(r.WeekDisplayName),
|
||||
r.TotalPoolAmount,
|
||||
r.TotalBalances,
|
||||
r.ValuePerBalance,
|
||||
r.IsCalculated ? "محاسبه شده" : "در انتظار",
|
||||
r.CalculatedAt != null ? r.CalculatedAt.ToDateTime().MiladiToJalaliWithTime() : "-"));
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetPreamble()
|
||||
.Concat(Encoding.UTF8.GetBytes(sb.ToString()))
|
||||
.ToArray();
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
var filename = $"weekly-reports-{DateTime.Now:yyyyMMddHHmmss}.csv";
|
||||
|
||||
await jsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
|
||||
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private static string EscapeCsv(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return "";
|
||||
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
|
||||
return $"\"{value.Replace("\"", "\"\"")}\"";
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,13 @@
|
||||
@attribute [Authorize]
|
||||
|
||||
@using MudBlazor
|
||||
@using Foursat.BackOffice.BFF.Commission.Protos
|
||||
@using CMSMicroservice.Protobuf.Protos.Commission
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
@using Microsoft.JSInterop
|
||||
@using System.Text
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<div>
|
||||
<MudText Typo="Typo.h4" Class="mb-1">گزارش برداشتها</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary" Class="mb-4">
|
||||
گزارش تجمیعی برداشتهای کمیسیون بر اساس بازه تاریخ و نوع دوره
|
||||
@@ -53,13 +53,13 @@
|
||||
Label="وضعیت (اختیاری)"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense">
|
||||
<MudSelectItem Value="@(null as int?)">همه وضعیتها</MudSelectItem>
|
||||
<MudSelectItem Value="0">در انتظار واریز به کیف پول</MudSelectItem>
|
||||
<MudSelectItem Value="1">واریز شده به کیف پول طلایی</MudSelectItem>
|
||||
<MudSelectItem Value="2">درخواست برداشت ثبت شده</MudSelectItem>
|
||||
<MudSelectItem Value="3">برداشت انجام شده</MudSelectItem>
|
||||
<MudSelectItem Value="4">خطا در پرداخت بانکی</MudSelectItem>
|
||||
<MudSelectItem Value="5">لغو شده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(null as int?)">همه وضعیتها</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="0">در انتظار واریز به کیف پول</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="1">واریز شده به کیف پول طلایی</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="2">درخواست برداشت ثبت شده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="3">برداشت انجام شده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="4">خطا در پرداخت بانکی</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="5">لغو شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4" Class="d-flex align-end">
|
||||
@@ -175,12 +175,12 @@
|
||||
<PropertyColumn Property="x => x.PeriodLabel" Title="دوره" />
|
||||
<PropertyColumn Property="x => x.StartDate" Title="از تاریخ">
|
||||
<CellTemplate>
|
||||
@context.Item.StartDate.ToString("yyyy/MM/dd")
|
||||
@context.Item.StartDate.MiladiToJalali()
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<PropertyColumn Property="x => x.EndDate" Title="تا تاریخ">
|
||||
<CellTemplate>
|
||||
@context.Item.EndDate.ToString("yyyy/MM/dd")
|
||||
@context.Item.EndDate.MiladiToJalali()
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<PropertyColumn Property="x => x.TotalRequests" Title="تعداد درخواستها" />
|
||||
@@ -203,11 +203,14 @@
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
</MudDataGrid>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
}
|
||||
</MudContainer>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Inject] public CommissionContract.CommissionContractClient CommissionClient { get; set; }
|
||||
@@ -394,8 +397,8 @@
|
||||
{
|
||||
sb.AppendLine(string.Join(",",
|
||||
EscapeCsv(r.PeriodLabel),
|
||||
r.StartDate.ToString("yyyy-MM-dd"),
|
||||
r.EndDate.ToString("yyyy-MM-dd"),
|
||||
r.StartDate.MiladiToJalali(),
|
||||
r.EndDate.MiladiToJalali(),
|
||||
r.TotalRequests,
|
||||
r.CompletedCount,
|
||||
r.FailedCount,
|
||||
@@ -423,7 +426,7 @@
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("گزارش برداشتها");
|
||||
sb.AppendLine($"دوره: {_periodType} از تاریخ: {_startDate:yyyy/MM/dd} تا تاریخ: {_endDate:yyyy/MM/dd}");
|
||||
sb.AppendLine($"دوره: {_periodType} از تاریخ: {_startDate?.MiladiToJalali() ?? "-"} تا تاریخ: {_endDate?.MiladiToJalali() ?? "-"}");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("خلاصه:");
|
||||
@@ -441,7 +444,7 @@
|
||||
foreach (var r in _periodReports.OrderBy(r => r.StartDate))
|
||||
{
|
||||
sb.AppendLine(
|
||||
$"{r.PeriodLabel} | {r.StartDate:yyyy/MM/dd} | {r.EndDate:yyyy/MM/dd} | {r.TotalRequests} | {r.CompletedCount} | {r.FailedCount} | {r.PendingCount} | {r.TotalAmount:N0} | {r.PaidAmount:N0} | {r.PendingAmount:N0}");
|
||||
$"{r.PeriodLabel} | {r.StartDate.MiladiToJalali()} | {r.EndDate.MiladiToJalali()} | {r.TotalRequests} | {r.CompletedCount} | {r.FailedCount} | {r.PendingCount} | {r.TotalAmount:N0} | {r.PaidAmount:N0} | {r.PendingAmount:N0}");
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(sb.ToString());
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
@page "/commission/withdrawals"
|
||||
@attribute [Authorize]
|
||||
|
||||
@using Foursat.BackOffice.BFF.Commission.Protos
|
||||
@using CMSMicroservice.Protobuf.Protos.Commission
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<div>
|
||||
<MudText Typo="Typo.h4" Class="mb-4">درخواستهای برداشت</MudText>
|
||||
|
||||
<MudDataGrid T="WithdrawalRequestModel"
|
||||
ServerData="@(new Func<GridState<WithdrawalRequestModel>, Task<GridData<WithdrawalRequestModel>>>(ServerReload))"
|
||||
Hover="true"
|
||||
@ref="_gridData"
|
||||
Height="75vh">
|
||||
Height="calc(100vh - 240px)">
|
||||
<ToolBarContent>
|
||||
<MudText Typo="Typo.h6">درخواستهای برداشت</MudText>
|
||||
<MudSpacer />
|
||||
@@ -30,13 +30,13 @@
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Style="max-width: 150px;">
|
||||
<MudSelectItem Value="@((int?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem Value="@((int?)0)">در انتظار واریز</MudSelectItem>
|
||||
<MudSelectItem Value="@((int?)1)">واریز شده به کیف پول</MudSelectItem>
|
||||
<MudSelectItem Value="@((int?)2)">درخواست برداشت</MudSelectItem>
|
||||
<MudSelectItem Value="@((int?)3)">برداشت شده</MudSelectItem>
|
||||
<MudSelectItem Value="@((int?)4)">خطای پرداخت بانکی</MudSelectItem>
|
||||
<MudSelectItem Value="@((int?)5)">لغو شده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)0)">در انتظار واریز</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)1)">واریز شده به کیف پول</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)2)">درخواست برداشت</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)3)">برداشت شده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)4)">خطای پرداخت بانکی</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@((int?)5)">لغو شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
@@ -44,6 +44,12 @@
|
||||
StartIcon="@Icons.Material.Filled.Search">
|
||||
جستجو
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Info"
|
||||
OnClick="ExportToExcel"
|
||||
StartIcon="@Icons.Material.Filled.FileDownload">
|
||||
خروجی Excel
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</ToolBarContent>
|
||||
<Columns>
|
||||
@@ -52,7 +58,9 @@
|
||||
<PropertyColumn Property="x => x.UserId" Title="کاربر">
|
||||
<CellTemplate>
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.body2"><strong>@context.Item.UserName</strong></MudText>
|
||||
<MudLink Href="@($"/network/user-info/{context.Item.UserId}")" Underline="Underline.Hover" Color="Color.Primary">
|
||||
<strong>@context.Item.UserName</strong>
|
||||
</MudLink>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">ID: @context.Item.UserId</MudText>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
@@ -101,7 +109,7 @@
|
||||
|
||||
<PropertyColumn Property="x => x.RequestedAt" Title="تاریخ درخواست">
|
||||
<CellTemplate>
|
||||
@context.Item.RequestedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm")
|
||||
@context.Item.RequestedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
|
||||
@@ -142,11 +150,14 @@
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="WithdrawalRequestModel"
|
||||
PageSizeOptions="@(new int[] { 10, 25, 50, 100 })"
|
||||
PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد سطرهای صفحه" />
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudContainer>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
using Foursat.BackOffice.BFF.Commission.Protos;
|
||||
using CMSMicroservice.Protobuf.Protos.Commission;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using GrpcWithdrawalRequestModel = Foursat.BackOffice.BFF.Commission.Protos.WithdrawalRequestModel;
|
||||
using GrpcGetWithdrawalRequestsRequest = Foursat.BackOffice.BFF.Commission.Protos.GetWithdrawalRequestsRequest;
|
||||
using BackOffice.Pages.Commission.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using System.Text;
|
||||
using DateTimeConverterCL;
|
||||
using GrpcWithdrawalRequestModel = CMSMicroservice.Protobuf.Protos.Commission.WithdrawalRequestModel;
|
||||
using GrpcGetWithdrawalRequestsRequest = CMSMicroservice.Protobuf.Protos.Commission.GetWithdrawalRequestsRequest;
|
||||
|
||||
namespace BackOffice.Pages.Commission;
|
||||
|
||||
public partial class WithdrawalRequests
|
||||
{
|
||||
[Inject] public CommissionContract.CommissionContractClient CommissionContract { get; set; }
|
||||
[Inject] public IJSRuntime JsRuntime { get; set; }
|
||||
|
||||
private MudDataGrid<WithdrawalRequestModel> _gridData;
|
||||
private int? _filterStatus;
|
||||
@@ -142,10 +147,16 @@ public partial class WithdrawalRequests
|
||||
};
|
||||
}
|
||||
|
||||
private void ViewDetails(WithdrawalRequestModel request)
|
||||
private async Task ViewDetails(WithdrawalRequestModel request)
|
||||
{
|
||||
Snackbar.Add($"جزئیات درخواست {request.Id}", Severity.Info);
|
||||
// TODO: Open details dialog
|
||||
var parameters = new DialogParameters<WithdrawalDetailsDialog>
|
||||
{
|
||||
{ x => x.Request, request },
|
||||
{ x => x.StatusText, GetStatusText(request.Status) },
|
||||
{ x => x.MethodText, GetMethodText(request.Method) }
|
||||
};
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
await DialogService.ShowAsync<WithdrawalDetailsDialog>("جزئیات درخواست برداشت", parameters, options);
|
||||
}
|
||||
|
||||
private async Task ApproveRequest(WithdrawalRequestModel request)
|
||||
@@ -177,19 +188,23 @@ public partial class WithdrawalRequests
|
||||
|
||||
private async Task RejectRequest(WithdrawalRequestModel request)
|
||||
{
|
||||
var confirmed = await DialogService.ShowMessageBox(
|
||||
"رد درخواست",
|
||||
$"آیا از رد درخواست برداشت {request.Amount:N0} ریال برای {request.UserName} مطمئن هستید؟",
|
||||
yesText: "رد", cancelText: "لغو");
|
||||
var parameters = new DialogParameters<RejectReasonDialog>
|
||||
{
|
||||
{ x => x.UserName, request.UserName },
|
||||
{ x => x.Amount, request.Amount }
|
||||
};
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<RejectReasonDialog>("رد درخواست برداشت", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (confirmed == true)
|
||||
if (result is { Canceled: false, Data: string reason })
|
||||
{
|
||||
try
|
||||
{
|
||||
var rejectRequest = new RejectWithdrawalRequest
|
||||
{
|
||||
PayoutId = request.Id,
|
||||
Reason = "رد توسط ادمین پنل مدیریت"
|
||||
Reason = reason
|
||||
};
|
||||
|
||||
await CommissionContract.RejectWithdrawalAsync(rejectRequest);
|
||||
@@ -230,4 +245,66 @@ public partial class WithdrawalRequests
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExportToExcel()
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new GrpcGetWithdrawalRequestsRequest
|
||||
{
|
||||
PageIndex = 1,
|
||||
PageSize = 1000
|
||||
};
|
||||
|
||||
if (_filterStatus.HasValue) request.Status = _filterStatus.Value;
|
||||
if (_filterUserId.HasValue && _filterUserId.Value > 0) request.UserId = _filterUserId.Value;
|
||||
if (!string.IsNullOrWhiteSpace(_filterIban)) request.IbanNumber = _filterIban;
|
||||
|
||||
var result = await CommissionContract.GetWithdrawalRequestsAsync(request);
|
||||
|
||||
if (result?.Models == null || !result.Models.Any())
|
||||
{
|
||||
Snackbar.Add("دادهای برای خروجی وجود ندارد.", Severity.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("شناسه,کاربر,نام کاربر,مبلغ,وضعیت,روش,شماره شبا,تاریخ درخواست,تاریخ پردازش,شماره مرجع بانکی");
|
||||
|
||||
foreach (var r in result.Models)
|
||||
{
|
||||
var mapped = MapToViewModel(r);
|
||||
sb.AppendLine(string.Join(",",
|
||||
mapped.Id,
|
||||
mapped.UserId,
|
||||
EscapeCsv(mapped.UserName),
|
||||
mapped.Amount,
|
||||
EscapeCsv(GetStatusText(mapped.Status)),
|
||||
EscapeCsv(GetMethodText(mapped.Method)),
|
||||
EscapeCsv(mapped.IbanNumber ?? ""),
|
||||
mapped.RequestedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime(),
|
||||
mapped.ProcessedDate?.MiladiToJalaliWithTime() ?? "-",
|
||||
EscapeCsv(mapped.BankReferenceId ?? "")));
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
var filename = $"withdrawal-requests-{DateTime.Now:yyyyMMddHHmmss}.csv";
|
||||
|
||||
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
|
||||
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private static string EscapeCsv(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return "";
|
||||
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
|
||||
return $"\"{value.Replace("\"", "\"\"")}\"";
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
@using BackOffice.Services.Content
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using BackOffice.Common.BaseComponents
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudForm @ref="_form" @bind-IsValid="@_isValid">
|
||||
<MudStack Spacing="2">
|
||||
<MudTextField @bind-Value="Model.PageKey"
|
||||
Label="کلید صفحه (مثال: about, contact)"
|
||||
Required="true"
|
||||
Variant="Variant.Outlined"
|
||||
HelperText="این مقدار باید یکتا باشد و فقط حروف انگلیسی و خط فاصله مجاز است" />
|
||||
|
||||
<MudTextField @bind-Value="Model.Title"
|
||||
Label="عنوان صفحه"
|
||||
Required="true"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudTextField @bind-Value="Model.MetaDescription"
|
||||
Label="توضیحات متا (SEO)"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="2" />
|
||||
|
||||
<MudTextField @bind-Value="Model.HeroTitle"
|
||||
Label="عنوان هیرو"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudTextField @bind-Value="Model.HeroSubtitle"
|
||||
Label="زیرعنوان هیرو"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudText Typo="Typo.subtitle2">تصویر هیرو</MudText>
|
||||
<MudStack Justify="Justify.Center" AlignItems="AlignItems.Center">
|
||||
@if (!string.IsNullOrWhiteSpace(_imagePreview))
|
||||
{
|
||||
<Image Src="@_imagePreview" Width="280" Height="160" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper Class="d-flex align-center justify-center" Style="width:280px;height:160px;">
|
||||
<MudText Typo="Typo.caption">تصویری انتخاب نشده است</MudText>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
<MudFileUpload T="IBrowserFile" Accept="image/*" FilesChanged="OnImageSelected">
|
||||
<ActivatorContent>
|
||||
<MudButton HtmlTag="label"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
ButtonType="ButtonType.Button"
|
||||
StartIcon="@Icons.Material.Filled.Image">
|
||||
انتخاب تصویر هیرو
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
<SelectedTemplate>
|
||||
@if (context != null)
|
||||
{
|
||||
<MudText Class="mt-1" Typo="Typo.caption">@context.Name</MudText>
|
||||
}
|
||||
</SelectedTemplate>
|
||||
</MudFileUpload>
|
||||
</MudStack>
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudSwitch T="bool" @bind-Value="Model.IsActive" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.body2">فعال</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="SubmitAsync" Disabled="@(!_isValid || _loading)">
|
||||
ایجاد صفحه
|
||||
</MudButton>
|
||||
<MudButton Color="Color.Default" Variant="Variant.Text" OnClick="Cancel">
|
||||
انصراف
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,74 @@
|
||||
using BackOffice.Services.Content;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Content.Components;
|
||||
|
||||
public partial class SitePageCreateDialog
|
||||
{
|
||||
[CascadingParameter] IMudDialogInstance DialogInstance { get; set; } = default!;
|
||||
[Inject] public ISitePageService SitePageService { get; set; } = default!;
|
||||
|
||||
[Parameter] public SitePageCreateDto Model { get; set; } = new();
|
||||
|
||||
private MudForm? _form;
|
||||
private bool _isValid = true;
|
||||
private bool _loading;
|
||||
|
||||
private IBrowserFile? _imageFile;
|
||||
private byte[]? _imageBuffer;
|
||||
private string? _imagePreview;
|
||||
private readonly long _maxAllowedSize = (1024 * 1024) * 5;
|
||||
|
||||
private async Task OnImageSelected(IBrowserFile? file)
|
||||
{
|
||||
_imageFile = file;
|
||||
if (file != null)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await file.OpenReadStream(_maxAllowedSize).CopyToAsync(ms);
|
||||
_imageBuffer = ms.ToArray();
|
||||
_imagePreview = $"data:{file.ContentType};base64," + Convert.ToBase64String(_imageBuffer);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SubmitAsync()
|
||||
{
|
||||
if (_form != null)
|
||||
{
|
||||
await _form.Validate();
|
||||
if (!_isValid) return;
|
||||
}
|
||||
|
||||
_loading = true;
|
||||
|
||||
// Handle image upload
|
||||
if (_imageFile != null && _imageBuffer != null)
|
||||
{
|
||||
Model.ImageFile = _imageBuffer;
|
||||
Model.ImageMime = _imageFile.ContentType;
|
||||
Model.ImageFileName = Path.GetFileNameWithoutExtension(_imageFile.Name);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await SitePageService.CreateAsync(Model);
|
||||
DialogInstance.Close(DialogResult.Ok(true));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
DialogInstance.Cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
@using BackOffice.Services.Content
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using BackOffice.Common.BaseComponents
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudForm @ref="_form" @bind-IsValid="@_isValid">
|
||||
<MudStack Spacing="2">
|
||||
<MudTextField @bind-Value="Model.Title"
|
||||
Label="عنوان صفحه"
|
||||
Required="true"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudTextField @bind-Value="Model.MetaDescription"
|
||||
Label="توضیحات متا (SEO)"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="2" />
|
||||
|
||||
<MudTextField @bind-Value="Model.HeroTitle"
|
||||
Label="عنوان هیرو"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudTextField @bind-Value="Model.HeroSubtitle"
|
||||
Label="زیرعنوان هیرو"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudText Typo="Typo.subtitle2">تصویر هیرو</MudText>
|
||||
<MudStack Justify="Justify.Center" AlignItems="AlignItems.Center">
|
||||
@if (!string.IsNullOrWhiteSpace(_imagePreview))
|
||||
{
|
||||
<Image Src="@_imagePreview" Width="280" Height="160" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Model.HeroImagePath))
|
||||
{
|
||||
<Image Src="@Model.HeroImagePath" Width="280" Height="160" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper Class="d-flex align-center justify-center" Style="width:280px;height:160px;">
|
||||
<MudText Typo="Typo.caption">تصویری انتخاب نشده است</MudText>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
<MudFileUpload T="IBrowserFile" Accept="image/*" FilesChanged="OnImageSelected">
|
||||
<ActivatorContent>
|
||||
<MudButton HtmlTag="label"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
ButtonType="ButtonType.Button"
|
||||
StartIcon="@Icons.Material.Filled.Image">
|
||||
انتخاب تصویر هیرو
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
<SelectedTemplate>
|
||||
@if (context != null)
|
||||
{
|
||||
<MudText Class="mt-1" Typo="Typo.caption">@context.Name</MudText>
|
||||
}
|
||||
</SelectedTemplate>
|
||||
</MudFileUpload>
|
||||
</MudStack>
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudSwitch T="bool" @bind-Value="Model.IsActive" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.body2">فعال</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="SubmitAsync" Disabled="@(!_isValid || _loading)">
|
||||
ذخیره تغییرات
|
||||
</MudButton>
|
||||
<MudButton Color="Color.Default" Variant="Variant.Text" OnClick="Cancel">
|
||||
انصراف
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,80 @@
|
||||
using BackOffice.Services.Content;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Content.Components;
|
||||
|
||||
public partial class SitePageEditDialog
|
||||
{
|
||||
[CascadingParameter] IMudDialogInstance DialogInstance { get; set; } = default!;
|
||||
[Inject] public ISitePageService SitePageService { get; set; } = default!;
|
||||
|
||||
[Parameter] public SitePageEditDto Model { get; set; } = new();
|
||||
[Parameter] public long PageId { get; set; }
|
||||
|
||||
private MudForm? _form;
|
||||
private bool _isValid = true;
|
||||
private bool _loading;
|
||||
|
||||
private IBrowserFile? _imageFile;
|
||||
private byte[]? _imageBuffer;
|
||||
private string? _imagePreview;
|
||||
private readonly long _maxAllowedSize = (1024 * 1024) * 5;
|
||||
|
||||
private async Task OnImageSelected(IBrowserFile? file)
|
||||
{
|
||||
_imageFile = file;
|
||||
if (file != null)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await file.OpenReadStream(_maxAllowedSize).CopyToAsync(ms);
|
||||
_imageBuffer = ms.ToArray();
|
||||
_imagePreview = $"data:{file.ContentType};base64," + Convert.ToBase64String(_imageBuffer);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleValidSubmit()
|
||||
{
|
||||
await SubmitAsync();
|
||||
}
|
||||
|
||||
private async Task SubmitAsync()
|
||||
{
|
||||
if (_form != null)
|
||||
{
|
||||
await _form.Validate();
|
||||
if (!_isValid) return;
|
||||
}
|
||||
|
||||
_loading = true;
|
||||
|
||||
// Handle image upload
|
||||
if (_imageFile != null && _imageBuffer != null)
|
||||
{
|
||||
Model.ImageFile = _imageBuffer;
|
||||
Model.ImageMime = _imageFile.ContentType;
|
||||
Model.ImageFileName = Path.GetFileNameWithoutExtension(_imageFile.Name);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await SitePageService.UpdateAsync(PageId, Model);
|
||||
DialogInstance.Close(DialogResult.Ok(true));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
DialogInstance.Cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
@using BackOffice.Services.Content
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Tizzani.MudBlazor.HtmlEditor
|
||||
@using BackOffice.Common.BaseComponents
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudForm @ref="_form" @bind-IsValid="@_isValid">
|
||||
<MudStack Spacing="2">
|
||||
<MudTextField @bind-Value="Model.SectionKey"
|
||||
Label="کلید بخش (اختیاری)"
|
||||
Variant="Variant.Outlined"
|
||||
HelperText="در صورت خالی بودن، به صورت خودکار تولید میشود" />
|
||||
|
||||
<MudTextField @bind-Value="Model.Title"
|
||||
Label="عنوان"
|
||||
Required="true"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudTextField @bind-Value="Model.Subtitle"
|
||||
Label="زیرعنوان"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudText Typo="Typo.subtitle2">محتوای HTML</MudText>
|
||||
<MudHtmlEditor @bind-Html="Model.HtmlContent" MinHeight="300px">
|
||||
<MudHtmlToolbarOptions InsertImage="false" />
|
||||
</MudHtmlEditor>
|
||||
|
||||
<MudTextField @bind-Value="Model.IconName"
|
||||
Label="نام آیکون"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudText Typo="Typo.subtitle2">تصویر بخش</MudText>
|
||||
<MudStack Justify="Justify.Center" AlignItems="AlignItems.Center">
|
||||
@if (!string.IsNullOrWhiteSpace(_imagePreview))
|
||||
{
|
||||
<Image Src="@_imagePreview" Width="220" Height="140" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Model.ImagePath))
|
||||
{
|
||||
<Image Src="@Model.ImagePath" Width="220" Height="140" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper Class="d-flex align-center justify-center" Style="width:220px;height:140px;">
|
||||
<MudText Typo="Typo.caption">تصویری انتخاب نشده است</MudText>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
<MudFileUpload T="IBrowserFile" Accept="image/*" FilesChanged="OnImageSelected">
|
||||
<ActivatorContent>
|
||||
<MudButton HtmlTag="label"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
ButtonType="ButtonType.Button"
|
||||
StartIcon="@Icons.Material.Filled.Image">
|
||||
انتخاب تصویر بخش
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
<SelectedTemplate>
|
||||
@if (context != null)
|
||||
{
|
||||
<MudText Class="mt-1" Typo="Typo.caption">@context.Name</MudText>
|
||||
}
|
||||
</SelectedTemplate>
|
||||
</MudFileUpload>
|
||||
</MudStack>
|
||||
|
||||
<MudNumericField T="int"
|
||||
@bind-Value="Model.SortOrder"
|
||||
Label="ترتیب نمایش"
|
||||
Variant="Variant.Outlined" />
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudSwitch T="bool" @bind-Value="Model.IsActive" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.body2">فعال</MudText>
|
||||
</MudStack>
|
||||
|
||||
<MudTextField @bind-Value="Model.ExtraData"
|
||||
Label="دادههای اضافی (JSON)"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="3"
|
||||
HelperText="اطلاعات اضافی به صورت JSON" />
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="SubmitAsync" Disabled="@(!_isValid || _loading)">
|
||||
@(_isEditMode ? "ذخیره تغییرات" : "ایجاد بخش")
|
||||
</MudButton>
|
||||
<MudButton Color="Color.Default" Variant="Variant.Text" OnClick="Cancel">
|
||||
انصراف
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,90 @@
|
||||
using BackOffice.Services.Content;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Content.Components;
|
||||
|
||||
public partial class SitePageSectionEditDialog
|
||||
{
|
||||
[CascadingParameter] IMudDialogInstance DialogInstance { get; set; } = default!;
|
||||
[Inject] public ISitePageService SitePageService { get; set; } = default!;
|
||||
|
||||
[Parameter] public SitePageSectionEditDto Model { get; set; } = new();
|
||||
[Parameter] public long? SectionId { get; set; }
|
||||
[Parameter] public bool IsEditMode { get; set; }
|
||||
|
||||
private bool _isEditMode => IsEditMode && SectionId.HasValue;
|
||||
private MudForm? _form;
|
||||
private bool _isValid = true;
|
||||
private bool _loading;
|
||||
|
||||
private IBrowserFile? _imageFile;
|
||||
private byte[]? _imageBuffer;
|
||||
private string? _imagePreview;
|
||||
private readonly long _maxAllowedSize = (1024 * 1024) * 5;
|
||||
|
||||
private async Task OnImageSelected(IBrowserFile? file)
|
||||
{
|
||||
_imageFile = file;
|
||||
if (file != null)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await file.OpenReadStream(_maxAllowedSize).CopyToAsync(ms);
|
||||
_imageBuffer = ms.ToArray();
|
||||
_imagePreview = $"data:{file.ContentType};base64," + Convert.ToBase64String(_imageBuffer);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleValidSubmit()
|
||||
{
|
||||
await SubmitAsync();
|
||||
}
|
||||
|
||||
private async Task SubmitAsync()
|
||||
{
|
||||
if (_form != null)
|
||||
{
|
||||
await _form.Validate();
|
||||
if (!_isValid) return;
|
||||
}
|
||||
|
||||
_loading = true;
|
||||
|
||||
// Handle image upload
|
||||
if (_imageFile != null && _imageBuffer != null)
|
||||
{
|
||||
Model.ImageFile = _imageBuffer;
|
||||
Model.ImageMime = _imageFile.ContentType;
|
||||
Model.ImageFileName = Path.GetFileNameWithoutExtension(_imageFile.Name);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_isEditMode && SectionId.HasValue)
|
||||
{
|
||||
await SitePageService.UpdateSectionAsync(SectionId.Value, Model);
|
||||
}
|
||||
else
|
||||
{
|
||||
await SitePageService.CreateSectionAsync(Model);
|
||||
}
|
||||
|
||||
DialogInstance.Close(DialogResult.Ok(true));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
DialogInstance.Cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
@using BackOffice.Services.Content
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.subtitle1">بخشهای صفحه</MudText>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="AddSection">
|
||||
افزودن بخش
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
<MudDataGrid T="SitePageSectionItemDto"
|
||||
Items="Sections"
|
||||
Hover="true"
|
||||
Dense="true"
|
||||
Elevation="0">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.SectionKey" Title="کلید بخش" />
|
||||
<PropertyColumn Property="x => x.Title" Title="عنوان" />
|
||||
<PropertyColumn Property="x => x.SortOrder" Title="ترتیب" />
|
||||
<PropertyColumn Property="x => x.IsActive" Title="وضعیت">
|
||||
<CellTemplate>
|
||||
<MudChip Color="@(context.Item.IsActive ? Color.Success : Color.Error)"
|
||||
Size="Size.Small">
|
||||
@(context.Item.IsActive ? "فعال" : "غیرفعال")
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<TemplateColumn Title="عملیات" Sortable="false">
|
||||
<CellTemplate>
|
||||
<MudStack Row="true" Spacing="1">
|
||||
<MudTooltip Text="ویرایش">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Size="Size.Small"
|
||||
Color="Color.Primary"
|
||||
OnClick="@(() => EditSection(context.Item))" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="حذف">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Size="Size.Small"
|
||||
Color="Color.Error"
|
||||
OnClick="@(() => DeleteSection(context.Item))" />
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
</MudDataGrid>
|
||||
</MudStack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton Color="Color.Default" Variant="Variant.Text" OnClick="Close">
|
||||
بستن
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,114 @@
|
||||
using BackOffice.Services.Content;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Content.Components;
|
||||
|
||||
public partial class SitePageSectionsDialog
|
||||
{
|
||||
[CascadingParameter] IMudDialogInstance DialogInstance { get; set; } = default!;
|
||||
[Inject] public ISitePageService SitePageService { get; set; } = default!;
|
||||
|
||||
[Parameter] public long PageId { get; set; }
|
||||
[Parameter] public string PageTitle { get; set; } = string.Empty;
|
||||
[Parameter] public List<SitePageSectionItemDto> Sections { get; set; } = new();
|
||||
|
||||
private bool _changed;
|
||||
|
||||
private async Task AddSection()
|
||||
{
|
||||
var dto = new SitePageSectionEditDto
|
||||
{
|
||||
SitePageId = PageId,
|
||||
SortOrder = Sections.Count + 1
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters<SitePageSectionEditDialog>
|
||||
{
|
||||
{ nameof(SitePageSectionEditDialog.Model), dto },
|
||||
{ nameof(SitePageSectionEditDialog.IsEditMode), false }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<SitePageSectionEditDialog>("افزودن بخش جدید", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true });
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
Snackbar.Add("بخش با موفقیت ایجاد شد", Severity.Success);
|
||||
_changed = true;
|
||||
await RefreshSections();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EditSection(SitePageSectionItemDto item)
|
||||
{
|
||||
var dto = new SitePageSectionEditDto
|
||||
{
|
||||
SitePageId = PageId,
|
||||
SectionKey = item.SectionKey,
|
||||
Title = item.Title,
|
||||
Subtitle = item.Subtitle,
|
||||
HtmlContent = item.HtmlContent,
|
||||
IconName = item.IconName,
|
||||
ImagePath = item.ImagePath,
|
||||
ImageThumbnailPath = item.ImageThumbnailPath,
|
||||
SortOrder = item.SortOrder,
|
||||
IsActive = item.IsActive,
|
||||
ExtraData = item.ExtraData
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters<SitePageSectionEditDialog>
|
||||
{
|
||||
{ nameof(SitePageSectionEditDialog.Model), dto },
|
||||
{ nameof(SitePageSectionEditDialog.SectionId), item.Id },
|
||||
{ nameof(SitePageSectionEditDialog.IsEditMode), true }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<SitePageSectionEditDialog>("ویرایش بخش", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true });
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
Snackbar.Add("بخش با موفقیت بهروزرسانی شد", Severity.Success);
|
||||
_changed = true;
|
||||
await RefreshSections();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteSection(SitePageSectionItemDto item)
|
||||
{
|
||||
var confirmed = await DialogService.ShowMessageBox(
|
||||
"حذف بخش",
|
||||
$"آیا از حذف بخش «{item.Title}» مطمئن هستید؟",
|
||||
yesText: "حذف",
|
||||
cancelText: "لغو");
|
||||
|
||||
if (confirmed == true)
|
||||
{
|
||||
await SitePageService.DeleteSectionAsync(item.Id);
|
||||
Snackbar.Add("بخش حذف شد", Severity.Success);
|
||||
_changed = true;
|
||||
await RefreshSections();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshSections()
|
||||
{
|
||||
var details = await SitePageService.GetByIdAsync(PageId);
|
||||
if (details != null)
|
||||
{
|
||||
Sections = details.Sections;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void Close()
|
||||
{
|
||||
if (_changed)
|
||||
DialogInstance.Close(DialogResult.Ok(true));
|
||||
else
|
||||
DialogInstance.Cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
@page "/content/pages"
|
||||
@attribute [Authorize(Roles = "Administrator")]
|
||||
|
||||
@using BackOffice.Services.Content
|
||||
|
||||
@inject ISitePageService SitePageService
|
||||
|
||||
<div>
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.h5">مدیریت صفحات سایت</MudText>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="CreatePage">
|
||||
افزودن صفحه جدید
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
<MudDataGrid T="SitePageSummaryDto"
|
||||
Items="_pages"
|
||||
Hover="true"
|
||||
Elevation="1"
|
||||
Dense="true"
|
||||
Loading="_loading">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
<PropertyColumn Property="x => x.PageKey" Title="کلید صفحه" />
|
||||
<PropertyColumn Property="x => x.Title" Title="عنوان" />
|
||||
<PropertyColumn Property="x => x.IsActive" Title="وضعیت">
|
||||
<CellTemplate>
|
||||
<MudChip Color="@(context.Item.IsActive ? Color.Success : Color.Error)"
|
||||
Size="Size.Small">
|
||||
@(context.Item.IsActive ? "فعال" : "غیرفعال")
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<PropertyColumn Property="x => x.SectionCount" Title="تعداد بخش" />
|
||||
<TemplateColumn Title="آخرین ویرایش">
|
||||
<CellTemplate>
|
||||
@context.Item.LastModified.MiladiToJalaliWithTime()
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="عملیات" Sortable="false">
|
||||
<CellTemplate>
|
||||
<MudStack Row="true" Spacing="1">
|
||||
<MudTooltip Text="ویرایش">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Size="Size.Small"
|
||||
Color="Color.Primary"
|
||||
OnClick="@(() => EditPage(context.Item))" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="مدیریت بخشها">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ViewList"
|
||||
Size="Size.Small"
|
||||
Color="Color.Info"
|
||||
OnClick="@(() => ManageSections(context.Item))" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="حذف">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Size="Size.Small"
|
||||
Color="Color.Error"
|
||||
OnClick="@(() => DeletePage(context.Item))" />
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
</MudDataGrid>
|
||||
</MudStack>
|
||||
</div>
|
||||
@@ -0,0 +1,128 @@
|
||||
using BackOffice.Services.Content;
|
||||
using BackOffice.Pages.Content.Components;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.Content;
|
||||
|
||||
public partial class SitePageManagementPage
|
||||
{
|
||||
private List<SitePageSummaryDto> _pages = new();
|
||||
private bool _loading = true;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadPagesAsync();
|
||||
}
|
||||
|
||||
private async Task LoadPagesAsync()
|
||||
{
|
||||
_loading = true;
|
||||
_pages = await SitePageService.GetAllAsync();
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private async Task CreatePage()
|
||||
{
|
||||
var model = new SitePageCreateDto();
|
||||
var parameters = new DialogParameters<SitePageCreateDialog>
|
||||
{
|
||||
{ nameof(SitePageCreateDialog.Model), model }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<SitePageCreateDialog>("افزودن صفحه جدید", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true });
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
Snackbar.Add("صفحه جدید با موفقیت ایجاد شد", Severity.Success);
|
||||
await LoadPagesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EditPage(SitePageSummaryDto item)
|
||||
{
|
||||
var details = await SitePageService.GetByIdAsync(item.Id);
|
||||
if (details == null)
|
||||
{
|
||||
Snackbar.Add("خطا در دریافت اطلاعات صفحه", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var dto = new SitePageEditDto
|
||||
{
|
||||
Title = details.Title,
|
||||
MetaDescription = details.MetaDescription,
|
||||
HeroTitle = details.HeroTitle,
|
||||
HeroSubtitle = details.HeroSubtitle,
|
||||
HeroImagePath = details.HeroImagePath,
|
||||
IsActive = details.IsActive
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters<SitePageEditDialog>
|
||||
{
|
||||
{ nameof(SitePageEditDialog.Model), dto },
|
||||
{ nameof(SitePageEditDialog.PageId), item.Id }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<SitePageEditDialog>("ویرایش صفحه", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true });
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
Snackbar.Add("صفحه با موفقیت بهروزرسانی شد", Severity.Success);
|
||||
await LoadPagesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ManageSections(SitePageSummaryDto item)
|
||||
{
|
||||
var details = await SitePageService.GetByIdAsync(item.Id);
|
||||
if (details == null)
|
||||
{
|
||||
Snackbar.Add("خطا در دریافت اطلاعات صفحه", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var parameters = new DialogParameters<SitePageSectionsDialog>
|
||||
{
|
||||
{ nameof(SitePageSectionsDialog.PageId), item.Id },
|
||||
{ nameof(SitePageSectionsDialog.PageTitle), item.Title },
|
||||
{ nameof(SitePageSectionsDialog.Sections), details.Sections }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<SitePageSectionsDialog>(
|
||||
$"مدیریت بخشهای «{item.Title}»", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Large, FullWidth = true });
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
await LoadPagesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeletePage(SitePageSummaryDto item)
|
||||
{
|
||||
var confirm = await DialogService.ShowMessageBox(
|
||||
"تایید حذف",
|
||||
$"آیا از حذف صفحه «{item.Title}» اطمینان دارید؟ بخشهای وابسته نیز حذف خواهند شد.",
|
||||
yesText: "بله، حذف شود",
|
||||
cancelText: "انصراف");
|
||||
|
||||
if (confirm != true) return;
|
||||
|
||||
try
|
||||
{
|
||||
await SitePageService.DeleteAsync(item.Id);
|
||||
Snackbar.Add("صفحه با موفقیت حذف شد", Severity.Success);
|
||||
await LoadPagesAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در حذف: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.UserContract
|
||||
@inject Microsoft.JSInterop.IJSRuntime JsRuntime
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudSimpleTable Dense="true">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>شناسه قرارداد:</strong></td>
|
||||
<td>@Contract.Id</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>شناسه کاربر:</strong></td>
|
||||
<td>@Contract.UserId</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>شناسه قالب قرارداد:</strong></td>
|
||||
<td>@Contract.ContractId</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>کد امضا:</strong></td>
|
||||
<td>@Contract.SignGuid</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>فایل PDF:</strong></td>
|
||||
<td>
|
||||
@if (!string.IsNullOrWhiteSpace(Contract.SignedPdfFile))
|
||||
{
|
||||
<MudButton Variant="Variant.Text" Color="Color.Primary" Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.PictureAsPdf"
|
||||
OnClick="DownloadPdf">
|
||||
دانلود PDF
|
||||
</MudButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">بدون فایل</MudText>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</MudSimpleTable>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Close">بستن</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
[Parameter] public GetAllUserContractByFilterResponseModel Contract { get; set; } = null!;
|
||||
|
||||
private void Close() => MudDialog.Cancel();
|
||||
|
||||
private async Task DownloadPdf()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(Contract.SignedPdfFile))
|
||||
{
|
||||
var title = $"قرارداد کاربر {Contract.UserId}";
|
||||
await JsRuntime.InvokeVoidAsync("jsHtmlToPdf", Contract.SignedPdfFile, title);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
@page "/contracts"
|
||||
@attribute [Authorize(Roles = "Administrator")]
|
||||
|
||||
@using CMSMicroservice.Protobuf.Protos.UserContract
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
@inject Microsoft.JSInterop.IJSRuntime JsRuntime
|
||||
@inject UserContractContract.UserContractContractClient UserContractClient
|
||||
|
||||
<div>
|
||||
<MudText Typo="Typo.h5" Class="mb-3">قراردادهای کاربران</MudText>
|
||||
|
||||
<MudDataGrid T="GetAllUserContractByFilterResponseModel" @ref="_grid"
|
||||
ServerData="LoadContracts"
|
||||
Hover="true" Dense="true"
|
||||
Height="calc(100vh - 240px)">
|
||||
<ToolBarContent>
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
||||
<UserAutoComplete Label="جستجوی کاربر" @bind-SelectedUserId="_filterUserId" />
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Search"
|
||||
OnClick="ApplyFilter">جستجو</MudButton>
|
||||
</MudStack>
|
||||
</ToolBarContent>
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
<PropertyColumn Property="x => x.UserId" Title="شناسه کاربر" />
|
||||
<PropertyColumn Property="x => x.ContractId" Title="شناسه قرارداد" />
|
||||
<PropertyColumn Property="x => x.SignGuid" Title="کد امضا" />
|
||||
<TemplateColumn Title="فایل PDF" Sortable="false">
|
||||
<CellTemplate>
|
||||
@if (!string.IsNullOrWhiteSpace(context.Item.SignedPdfFile))
|
||||
{
|
||||
<MudButton Variant="Variant.Text" Color="Color.Primary" Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.PictureAsPdf"
|
||||
OnClick="@(() => DownloadPdf(context.Item))">
|
||||
دانلود PDF
|
||||
</MudButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">بدون فایل</MudText>
|
||||
}
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="عملیات" Sortable="false">
|
||||
<CellTemplate>
|
||||
<MudTooltip Text="جزئیات">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Visibility"
|
||||
Size="Size.Small" Color="Color.Info"
|
||||
OnClick="@(() => ViewDetails(context.Item))" />
|
||||
</MudTooltip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">قراردادی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="GetAllUserContractByFilterResponseModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private MudDataGrid<GetAllUserContractByFilterResponseModel>? _grid;
|
||||
private long? _filterUserId;
|
||||
|
||||
private async Task<GridData<GetAllUserContractByFilterResponseModel>> LoadContracts(GridState<GetAllUserContractByFilterResponseModel> state)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new GetAllUserContractByFilterRequest
|
||||
{
|
||||
PaginationState = new() { PageNumber = state.Page + 1, PageSize = state.PageSize },
|
||||
Filter = new GetAllUserContractByFilterFilter()
|
||||
};
|
||||
|
||||
if (_filterUserId.HasValue && _filterUserId.Value > 0)
|
||||
request.Filter.UserId = _filterUserId.Value;
|
||||
|
||||
var result = await UserContractClient.GetAllUserContractByFilterAsync(request);
|
||||
return new GridData<GetAllUserContractByFilterResponseModel>
|
||||
{
|
||||
Items = result?.Models?.ToList() ?? new(),
|
||||
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
return new GridData<GetAllUserContractByFilterResponseModel>();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplyFilter()
|
||||
{
|
||||
if (_grid != null) await _grid.ReloadServerData();
|
||||
}
|
||||
|
||||
private async Task ViewDetails(GetAllUserContractByFilterResponseModel contract)
|
||||
{
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
["Contract"] = contract
|
||||
};
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
await DialogService.ShowAsync<ContractDetailsDialog>("جزئیات قرارداد", parameters, options);
|
||||
}
|
||||
|
||||
private async Task DownloadPdf(GetAllUserContractByFilterResponseModel contract)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(contract.SignedPdfFile))
|
||||
{
|
||||
var title = $"قرارداد کاربر {contract.UserId}";
|
||||
await JsRuntime.InvokeVoidAsync("jsHtmlToPdf", contract.SignedPdfFile, title);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,7 +118,7 @@
|
||||
.OrderBy(g => g.Key)
|
||||
.ToList();
|
||||
|
||||
_dailyLabels = groups.Select(g => g.Key.ToString("MM/dd")).ToArray();
|
||||
_dailyLabels = groups.Select(g => { var j = g.Key.MiladiToJalali(); return j.Length >= 7 ? j.Substring(5) : j; }).ToArray();
|
||||
|
||||
_dailySeries = new List<ChartSeries>
|
||||
{
|
||||
|
||||
@@ -1,340 +0,0 @@
|
||||
@page "/dashboard/overview"
|
||||
@attribute [Authorize]
|
||||
|
||||
@using Foursat.BackOffice.BFF.Commission.Protos
|
||||
@using Foursat.BackOffice.BFF.ClubMembership.Protos
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">داشبورد سیستم</MudText>
|
||||
<MudText Typo="Typo.body1" Color="Color.Secondary" Class="mb-4">
|
||||
مرور کلی عملکرد سیستم شبکه، باشگاه و کمیسیون
|
||||
</MudText>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Commission Stats -->
|
||||
<MudCard Class="mb-4">
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
<MudText Typo="Typo.h6">
|
||||
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Class="mr-2" />
|
||||
کمیسیون هفته جاری
|
||||
</MudText>
|
||||
</CardHeaderContent>
|
||||
<CardHeaderActions>
|
||||
<MudButton Variant="Variant.Text"
|
||||
Color="Color.Primary"
|
||||
Href="/commission/dashboard">
|
||||
جزئیات بیشتر
|
||||
</MudButton>
|
||||
</CardHeaderActions>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="3">
|
||||
<MudCard Outlined="true">
|
||||
<MudCardContent>
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">هفته جاری</MudText>
|
||||
<MudText Typo="Typo.h5">@_currentWeekDisplayName</MudText>
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="3">
|
||||
<MudCard Outlined="true">
|
||||
<MudCardContent>
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">مبلغ کل استخر</MudText>
|
||||
<MudText Typo="Typo.h5" Color="Color.Primary">
|
||||
@(_currentPool?.TotalPoolAmount.ToString("N0") ?? "0") ریال
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="3">
|
||||
<MudCard Outlined="true">
|
||||
<MudCardContent>
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">مجموع موجودیها</MudText>
|
||||
<MudText Typo="Typo.h5">@(_currentPool?.TotalBalances.ToString("N0") ?? "0")</MudText>
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="3">
|
||||
<MudCard Outlined="true">
|
||||
<MudCardContent>
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">وضعیت</MudText>
|
||||
<MudChip T="string"
|
||||
Color="@(_currentPool?.IsCalculated == true ? Color.Success : Color.Warning)"
|
||||
Size="Size.Small">
|
||||
@(_currentPool?.IsCalculated == true ? "محاسبه شده" : "در انتظار")
|
||||
</MudChip>
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
|
||||
<!-- Club Membership Stats -->
|
||||
<MudCard Class="mb-4">
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
<MudText Typo="Typo.h6">
|
||||
<MudIcon Icon="@Icons.Material.Filled.CardMembership" Class="mr-2" />
|
||||
عضویت باشگاه
|
||||
</MudText>
|
||||
</CardHeaderContent>
|
||||
<CardHeaderActions>
|
||||
<MudButton Variant="Variant.Text"
|
||||
Color="Color.Primary"
|
||||
Href="/club/members">
|
||||
مشاهده اعضا
|
||||
</MudButton>
|
||||
</CardHeaderActions>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudCard Outlined="true">
|
||||
<MudCardContent>
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">کل اعضا</MudText>
|
||||
<MudText Typo="Typo.h5" Color="Color.Primary">@_totalClubMembers</MudText>
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudCard Outlined="true">
|
||||
<MudCardContent>
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">اعضای فعال</MudText>
|
||||
<MudText Typo="Typo.h5" Color="Color.Success">@_activeClubMembers</MudText>
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudCard Outlined="true">
|
||||
<MudCardContent>
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">اعضای غیرفعال</MudText>
|
||||
<MudText Typo="Typo.h5" Color="Color.Warning">@_inactiveClubMembers</MudText>
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
|
||||
<!-- Discount Shop Stats - TEMPORARILY DISABLED (needs DiscountOrder proto) -->
|
||||
@* <BackOffice.Pages.Dashboard.DiscountShopWidget /> *@
|
||||
|
||||
<MudCard Class="mb-4">
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
<MudText Typo="Typo.h6">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Discount" Class="mr-2" />
|
||||
آمار فروشگاه تخفیفی
|
||||
</MudText>
|
||||
</CardHeaderContent>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true">
|
||||
این بخش در حال توسعه است و بهزودی فعال میشود.
|
||||
</MudAlert>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<MudCard>
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
<MudText Typo="Typo.h6">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Speed" Class="mr-2" />
|
||||
دسترسی سریع
|
||||
</MudText>
|
||||
</CardHeaderContent>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
FullWidth="true"
|
||||
Href="/commission/dashboard"
|
||||
StartIcon="@Icons.Material.Filled.AccountBalanceWallet">
|
||||
داشبورد کمیسیون
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Secondary"
|
||||
FullWidth="true"
|
||||
Href="/commission/payouts"
|
||||
StartIcon="@Icons.Material.Filled.Payments">
|
||||
پرداختهای کاربران
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Tertiary"
|
||||
FullWidth="true"
|
||||
Href="/commission/withdrawals"
|
||||
StartIcon="@Icons.Material.Filled.RequestQuote">
|
||||
درخواستهای برداشت
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Info"
|
||||
FullWidth="true"
|
||||
Href="/network/tree"
|
||||
StartIcon="@Icons.Material.Filled.AccountTree">
|
||||
درخت شبکه
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Success"
|
||||
FullWidth="true"
|
||||
Href="/network/balances"
|
||||
StartIcon="@Icons.Material.Filled.AccountBalance">
|
||||
گزارش موجودیها
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Warning"
|
||||
FullWidth="true"
|
||||
Href="/club/members"
|
||||
StartIcon="@Icons.Material.Filled.Groups">
|
||||
مدیریت باشگاه
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
}
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
[Inject] public CommissionContract.CommissionContractClient CommissionClient { get; set; } = null!;
|
||||
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubClient { get; set; } = null!;
|
||||
|
||||
private bool _loading = false;
|
||||
private long _currentWeekDefinitionId = 0;
|
||||
private string _currentWeekDisplayName = string.Empty;
|
||||
private GetWeeklyCommissionPoolResponse? _currentPool;
|
||||
|
||||
private long _totalClubMembers = 0;
|
||||
private long _activeClubMembers = 0;
|
||||
private long _inactiveClubMembers = 0;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadDashboardData();
|
||||
}
|
||||
|
||||
private async Task LoadDashboardData()
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
// Get current week from available weeks API
|
||||
try
|
||||
{
|
||||
var availableWeeksRequest = new GetAvailableWeeksRequest
|
||||
{
|
||||
FutureWeeksCount = 0,
|
||||
PastWeeksCount = 0
|
||||
};
|
||||
var availableWeeksResponse = await CommissionClient.GetAvailableWeeksAsync(availableWeeksRequest);
|
||||
if (availableWeeksResponse?.CurrentWeek != null)
|
||||
{
|
||||
_currentWeekDefinitionId = availableWeeksResponse.CurrentWeek.WeekDefinitionId;
|
||||
_currentWeekDisplayName = availableWeeksResponse.CurrentWeek.DisplayName;
|
||||
}
|
||||
}
|
||||
catch { /* Ignore - will skip pool loading */ }
|
||||
|
||||
// Load Commission data
|
||||
if (_currentWeekDefinitionId > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
var commissionRequest = new GetWeeklyCommissionPoolRequest
|
||||
{
|
||||
WeekDefinitionId = _currentWeekDefinitionId
|
||||
};
|
||||
var commissionResponse = await CommissionClient.GetWeeklyCommissionPoolAsync(commissionRequest);
|
||||
_currentPool = commissionResponse;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری دادههای کمیسیون: {ex.Message}", Severity.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
// Load Club membership data
|
||||
try
|
||||
{
|
||||
// Get all members
|
||||
var allMembersRequest = new GetAllClubMembershipsRequest
|
||||
{
|
||||
PageIndex = 1,
|
||||
PageSize = 1
|
||||
};
|
||||
var allMembersResponse = await ClubClient.GetAllClubMembershipsAsync(allMembersRequest);
|
||||
_totalClubMembers = allMembersResponse.MetaData?.TotalCount ?? 0;
|
||||
|
||||
// Get active members
|
||||
var activeMembersRequest = new GetAllClubMembershipsRequest
|
||||
{
|
||||
PageIndex = 1,
|
||||
PageSize = 1,
|
||||
IsActive = true
|
||||
};
|
||||
var activeMembersResponse = await ClubClient.GetAllClubMembershipsAsync(activeMembersRequest);
|
||||
_activeClubMembers = activeMembersResponse.MetaData?.TotalCount ?? 0;
|
||||
|
||||
_inactiveClubMembers = _totalClubMembers - _activeClubMembers;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری دادههای باشگاه: {ex.Message}", Severity.Warning);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری داشبورد: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetCurrentWeekNumber()
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
var jan1 = new DateTime(now.Year, 1, 1);
|
||||
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.FirstDay, DayOfWeek.Saturday);
|
||||
return $"{now.Year}-W{weekNum:D2}";
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@
|
||||
HelperText="برای دسته اصلی خالی بگذارید">
|
||||
@foreach (var category in _availableParents)
|
||||
{
|
||||
<MudSelectItem Value="@((long?)category.Id)">@GetCategoryPath(category)</MudSelectItem>
|
||||
<MudSelectItem T="long?" Value="@((long?)category.Id)">@GetCategoryPath(category)</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
|
||||
@@ -21,13 +21,13 @@
|
||||
Label="وضعیت جدید *"
|
||||
Required="true"
|
||||
Variant="Variant.Outlined">
|
||||
<MudSelectItem Value="@OrderStatus.Pending">در انتظار پرداخت</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Paid">پرداخت شده</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Processing">در حال آمادهسازی</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Pending">در انتظار پرداخت</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Paid">پرداخت شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Processing">در حال آمادهسازی</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
|
||||
|
||||
@@ -26,6 +26,24 @@
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<!-- Timeline وضعیت -->
|
||||
<MudItem xs="12">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.subtitle2" GutterBottom="true">Timeline وضعیت</MudText>
|
||||
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center" Justify="Justify.Center">
|
||||
@foreach (var step in _timelineSteps)
|
||||
{
|
||||
<MudStack Spacing="0" AlignItems="AlignItems.Center">
|
||||
<MudAvatar Color="@GetStepColor(step.State)" Size="Size.Small">
|
||||
@step.Icon
|
||||
</MudAvatar>
|
||||
<MudText Typo="Typo.caption">@step.Title</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<!-- وضعیت سفارش -->
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
@@ -36,7 +54,7 @@
|
||||
@if (Order.Created.HasValue)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mt-2">
|
||||
تاریخ ثبت: @Order.Created.Value.ToString("yyyy/MM/dd HH:mm")
|
||||
تاریخ ثبت: @Order.Created.Value.MiladiToJalaliWithTime()
|
||||
</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
@@ -101,7 +119,14 @@
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>
|
||||
<MudText>@context.ProductTitle</MudText>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
@if (!string.IsNullOrWhiteSpace(context.ThumbnailPath))
|
||||
{
|
||||
<MudImage Src="@context.ThumbnailPath" Width="40" Height="40"
|
||||
ObjectFit="ObjectFit.Cover" Class="rounded" />
|
||||
}
|
||||
<MudText>@context.ProductTitle</MudText>
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
<MudTd>@context.Count</MudTd>
|
||||
<MudTd>@context.UnitPrice.ToString("N0") ریال</MudTd>
|
||||
@@ -147,6 +172,60 @@
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<!-- ویرایش وضعیت ارسال -->
|
||||
<MudItem xs="12">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.subtitle2" GutterBottom="true">مدیریت وضعیت ارسال</MudText>
|
||||
<MudDivider Class="mb-3" />
|
||||
<MudGrid>
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudSelect T="OrderStatus"
|
||||
Label="وضعیت ارسال"
|
||||
Dense="true"
|
||||
Variant="Variant.Outlined"
|
||||
@bind-Value="_newStatus">
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Pending">در انتظار پرداخت</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Paid">پرداخت شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Processing">در حال آمادهسازی</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudTextField T="string"
|
||||
Label="کد رهگیری"
|
||||
Variant="Variant.Outlined"
|
||||
Dense="true"
|
||||
@bind-Value="_trackingCode" />
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudTextField T="string"
|
||||
Label="یادداشت ادمین"
|
||||
Variant="Variant.Outlined"
|
||||
Dense="true"
|
||||
Lines="3"
|
||||
HelperText="دلیل تغییر وضعیت یا توضیحات اضافی"
|
||||
@bind-Value="_adminNotes" />
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
@if (_newStatus == OrderStatus.Cancelled || _newStatus == OrderStatus.Returned)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Class="mt-2">
|
||||
توجه: در صورت لغو یا مرجوعی سفارش، موجودی محصولات به انبار بازگردانده میشود.
|
||||
</MudAlert>
|
||||
}
|
||||
@if (_newStatus == OrderStatus.Shipped)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Class="mt-2">
|
||||
پس از تغییر وضعیت به "ارسال شده"، اطلاعرسانی به کاربر ارسال خواهد شد.
|
||||
</MudAlert>
|
||||
}
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<!-- یادداشتها -->
|
||||
@if (!string.IsNullOrEmpty(Order.Notes) || !string.IsNullOrEmpty(Order.AdminNotes))
|
||||
{
|
||||
@@ -174,46 +253,15 @@
|
||||
</MudGrid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Close" Variant="Variant.Filled" Color="Color.Primary">بستن</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Default" OnClick="Close">
|
||||
بستن
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveAsync" Disabled="_isSaving">
|
||||
@if (_isSaving)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="ml-2" />
|
||||
}
|
||||
ثبت تغییرات
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
[Parameter] public DiscountOrderDetailsDto Order { get; set; } = null!;
|
||||
|
||||
private void Close()
|
||||
{
|
||||
MudDialog.Close();
|
||||
}
|
||||
|
||||
private Color GetStatusColor(OrderStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
OrderStatus.Pending => Color.Warning,
|
||||
OrderStatus.Paid => Color.Info,
|
||||
OrderStatus.Processing => Color.Primary,
|
||||
OrderStatus.Shipped => Color.Secondary,
|
||||
OrderStatus.Delivered => Color.Success,
|
||||
OrderStatus.Cancelled => Color.Error,
|
||||
OrderStatus.Returned => Color.Dark,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private string GetStatusText(OrderStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
OrderStatus.Pending => "در انتظار پرداخت",
|
||||
OrderStatus.Paid => "پرداخت شده",
|
||||
OrderStatus.Processing => "در حال آمادهسازی",
|
||||
OrderStatus.Shipped => "ارسال شده",
|
||||
OrderStatus.Delivered => "تحویل داده شده",
|
||||
OrderStatus.Cancelled => "لغو شده",
|
||||
OrderStatus.Returned => "مرجوع شده",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
using BackOffice.Services.DiscountOrder;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.DiscountShop.Components;
|
||||
|
||||
public partial class OrderDetailsDialog
|
||||
{
|
||||
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
[Inject] public IDiscountOrderService OrderService { get; set; } = default!;
|
||||
|
||||
[Parameter] public DiscountOrderDetailsDto Order { get; set; } = null!;
|
||||
|
||||
private OrderStatus _newStatus;
|
||||
private string? _trackingCode;
|
||||
private string? _adminNotes;
|
||||
private bool _isSaving;
|
||||
private List<TimelineStep> _timelineSteps = new();
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_newStatus = Order.Status;
|
||||
_trackingCode = Order.TrackingCode;
|
||||
_adminNotes = Order.AdminNotes;
|
||||
BuildTimeline();
|
||||
}
|
||||
|
||||
private void BuildTimeline()
|
||||
{
|
||||
var status = Order.Status;
|
||||
var steps = new List<TimelineStep>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Title = "ثبت سفارش",
|
||||
State = status == OrderStatus.Pending ? StepState.Active : StepState.Completed,
|
||||
Icon = "1"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Title = "پرداخت",
|
||||
State = status switch
|
||||
{
|
||||
OrderStatus.Pending => StepState.Inactive,
|
||||
OrderStatus.Paid => StepState.Active,
|
||||
OrderStatus.Cancelled => StepState.Inactive,
|
||||
_ => Order.PaymentCompleted ? StepState.Completed : StepState.Inactive
|
||||
},
|
||||
Icon = "2"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Title = "آمادهسازی",
|
||||
State = status switch
|
||||
{
|
||||
OrderStatus.Processing => StepState.Active,
|
||||
OrderStatus.Shipped or OrderStatus.Delivered => StepState.Completed,
|
||||
_ => StepState.Inactive
|
||||
},
|
||||
Icon = "3"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Title = "ارسال",
|
||||
State = status switch
|
||||
{
|
||||
OrderStatus.Shipped => StepState.Active,
|
||||
OrderStatus.Delivered => StepState.Completed,
|
||||
_ => StepState.Inactive
|
||||
},
|
||||
Icon = "4"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Title = "تحویل / مرجوعی",
|
||||
State = status switch
|
||||
{
|
||||
OrderStatus.Delivered => StepState.Completed,
|
||||
OrderStatus.Returned => StepState.Completed,
|
||||
OrderStatus.Cancelled => StepState.Completed,
|
||||
_ => StepState.Inactive
|
||||
},
|
||||
Icon = "5"
|
||||
}
|
||||
};
|
||||
|
||||
_timelineSteps = steps;
|
||||
}
|
||||
|
||||
private Color GetStepColor(StepState state)
|
||||
{
|
||||
return state switch
|
||||
{
|
||||
StepState.Completed => Color.Success,
|
||||
StepState.Active => Color.Info,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
_isSaving = true;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
await OrderService.UpdateStatusAsync(Order.Id, new UpdateOrderStatusDto
|
||||
{
|
||||
Status = _newStatus,
|
||||
TrackingCode = _trackingCode,
|
||||
AdminNotes = _adminNotes
|
||||
});
|
||||
|
||||
Snackbar.Add("تغییرات سفارش با موفقیت ذخیره شد.", Severity.Success);
|
||||
MudDialog.Close(DialogResult.Ok(true));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ذخیره تغییرات: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSaving = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void Close() => MudDialog.Close();
|
||||
|
||||
private Color GetStatusColor(OrderStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
OrderStatus.Pending => Color.Warning,
|
||||
OrderStatus.Paid => Color.Info,
|
||||
OrderStatus.Processing => Color.Primary,
|
||||
OrderStatus.Shipped => Color.Secondary,
|
||||
OrderStatus.Delivered => Color.Success,
|
||||
OrderStatus.Cancelled => Color.Error,
|
||||
OrderStatus.Returned => Color.Dark,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private string GetStatusText(OrderStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
OrderStatus.Pending => "در انتظار پرداخت",
|
||||
OrderStatus.Paid => "پرداخت شده",
|
||||
OrderStatus.Processing => "در حال آمادهسازی",
|
||||
OrderStatus.Shipped => "ارسال شده",
|
||||
OrderStatus.Delivered => "تحویل داده شده",
|
||||
OrderStatus.Cancelled => "لغو شده",
|
||||
OrderStatus.Returned => "مرجوع شده",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
}
|
||||
|
||||
private enum StepState
|
||||
{
|
||||
Inactive = 0,
|
||||
Active = 1,
|
||||
Completed = 2
|
||||
}
|
||||
|
||||
private class TimelineStep
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public StepState State { get; set; }
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -1,129 +1,163 @@
|
||||
@using BackOffice.Services.DiscountProduct
|
||||
@using BackOffice.Services.DiscountCategory
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Tizzani.MudBlazor.HtmlEditor
|
||||
@using BackOffice.Common.BaseComponents
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudForm @ref="_form" @bind-IsValid="@_isValid">
|
||||
<MudGrid>
|
||||
<MudItem xs="12">
|
||||
<MudTextField @bind-Value="Model.Title"
|
||||
Label="عنوان محصول *"
|
||||
Required="true"
|
||||
RequiredError="عنوان الزامی است"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
<MudStack Spacing="3">
|
||||
<MudStack Justify="Justify.Center" AlignItems="AlignItems.Center">
|
||||
@if (!string.IsNullOrWhiteSpace(_mainImagePreview))
|
||||
{
|
||||
<Image Src="@(_mainImagePreview)" Width="220" Height="140" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Model.ImagePath))
|
||||
{
|
||||
<Image Src="@Model.ImagePath" Width="220" Height="140" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper Class="d-flex align-center justify-center" Style="width:220px;height:140px;">
|
||||
<MudText Typo="Typo.caption">تصویری انتخاب نشده است</MudText>
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudTextField @bind-Value="Model.ShortInformation"
|
||||
Label="توضیح کوتاه"
|
||||
Lines="2"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
<MudFileUpload T="IBrowserFile" Accept="image/*" FilesChanged="OnMainImageSelected">
|
||||
<ActivatorContent>
|
||||
<MudButton HtmlTag="label"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
ButtonType="ButtonType.Button"
|
||||
StartIcon="@Icons.Material.Filled.Image"
|
||||
>
|
||||
انتخاب تصویر اصلی
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
<SelectedTemplate>
|
||||
@if (context != null)
|
||||
{
|
||||
<MudText Class="mt-2" Typo="Typo.subtitle2">
|
||||
<MudTooltip Text="@context.Name" Inline="true">
|
||||
@context.Name
|
||||
</MudTooltip>
|
||||
</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Class="mt-2" Typo="Typo.subtitle2">فایلی انتخاب نشده</MudText>
|
||||
}
|
||||
</SelectedTemplate>
|
||||
</MudFileUpload>
|
||||
</MudStack>
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudTextField @bind-Value="Model.FullInformation"
|
||||
Label="توضیحات کامل"
|
||||
Lines="4"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudNumericField @bind-Value="Model.Price"
|
||||
Label="قیمت (ریال) *"
|
||||
Required="true"
|
||||
Min="0"
|
||||
Format="N0"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudNumericField @bind-Value="Model.MaxDiscountPercent"
|
||||
Label="حداکثر درصد تخفیف *"
|
||||
Required="true"
|
||||
Min="0"
|
||||
Max="100"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudNumericField @bind-Value="Model.InitialCount"
|
||||
Label="موجودی اولیه *"
|
||||
Required="true"
|
||||
Min="0"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudNumericField @bind-Value="Model.SortOrder"
|
||||
Label="ترتیب نمایش"
|
||||
Min="0"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudSelect @bind-SelectedValues="_selectedCategoryIds"
|
||||
Label="دستهبندیها"
|
||||
Variant="Variant.Outlined"
|
||||
MultiSelection="true"
|
||||
T="long">
|
||||
@foreach (var category in _categories)
|
||||
{
|
||||
<MudSelectItem Value="@category.Id">@GetCategoryPath(category)</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudTextField @bind-Value="Model.ImagePath"
|
||||
Label="مسیر تصویر اصلی"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.End"
|
||||
AdornmentIcon="@Icons.Material.Filled.Image" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudTextField @bind-Value="Model.ThumbnailPath"
|
||||
Label="مسیر تصویر بندانگشتی"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.End"
|
||||
AdornmentIcon="@Icons.Material.Filled.Image" />
|
||||
</MudItem>
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.ThumbnailPath))
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<MudImage Src="@Model.ThumbnailPath"
|
||||
Alt="پیشنمایش"
|
||||
Height="150"
|
||||
ObjectFit="ObjectFit.Contain"
|
||||
Class="rounded" />
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
<MudItem xs="6">
|
||||
<MudTextField T="string" @bind-Value="Model.Title"
|
||||
Disabled="_loading"
|
||||
Label="عنوان محصول *"
|
||||
Required="true"
|
||||
RequiredError="عنوان الزامی است"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
</MudItem>
|
||||
}
|
||||
<MudItem xs="6">
|
||||
<MudNumericField T="long" HideSpinButtons="true"
|
||||
@bind-Value="Model.Price"
|
||||
Format="N0"
|
||||
Immediate="true"
|
||||
Disabled="_loading"
|
||||
Label="قیمت (ریال) *"
|
||||
Required="true"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
</MudItem>
|
||||
</MudStack>
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudSwitch T="bool" @bind-Value="Model.IsActive"
|
||||
Label="محصول فعال"
|
||||
Color="Color.Success" />
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
<MudItem xs="6">
|
||||
<MudNumericField T="int" HideSpinButtons="true"
|
||||
@bind-Value="Model.MaxDiscountPercent"
|
||||
Disabled="_loading"
|
||||
Label="حداکثر درصد تخفیف *"
|
||||
Required="true"
|
||||
Min="0"
|
||||
Max="100"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
</MudItem>
|
||||
<MudItem xs="6">
|
||||
<MudNumericField T="int" HideSpinButtons="true"
|
||||
@bind-Value="Model.SortOrder"
|
||||
Disabled="_loading"
|
||||
Label="ترتیب نمایش"
|
||||
Min="0"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
</MudItem>
|
||||
</MudStack>
|
||||
|
||||
<MudTextField T="string" @bind-Value="Model.ShortInformation"
|
||||
Disabled="_loading"
|
||||
Label="توضیح کوتاه"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" />
|
||||
|
||||
<MudHtmlEditor @bind-Html="Model.FullInformation" MinHeight="300px">
|
||||
<MudHtmlToolbarOptions InsertImage="false" />
|
||||
</MudHtmlEditor>
|
||||
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.subtitle2">دستهبندیها</MudText>
|
||||
<DiscountCategoryMultiSelectCombo @bind-SelectedIds="_selectedCategoryIds"
|
||||
Label="انتخاب دستهبندیها"
|
||||
Disabled="_loading" />
|
||||
@if (_selectedCategoryIds?.Count > 0)
|
||||
{
|
||||
<MudText Typo="Typo.caption">
|
||||
تعداد دستهبندیهای انتخابشده: @_selectedCategoryIds.Count
|
||||
</MudText>
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.subtitle2">تصویر بندانگشتی</MudText>
|
||||
@if (!string.IsNullOrWhiteSpace(_thumbnailPreview))
|
||||
{
|
||||
<Image Src="@(_thumbnailPreview)" Width="120" Height="80" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Model.ThumbnailPath))
|
||||
{
|
||||
<Image Src="@Model.ThumbnailPath" Width="120" Height="80" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
|
||||
}
|
||||
<MudFileUpload T="IBrowserFile" Accept="image/*" FilesChanged="OnThumbnailSelected">
|
||||
<ActivatorContent>
|
||||
<MudButton HtmlTag="label"
|
||||
Variant="Variant.Outlined"
|
||||
Color="Color.Secondary"
|
||||
ButtonType="ButtonType.Button"
|
||||
StartIcon="@Icons.Material.Filled.Image"
|
||||
>
|
||||
انتخاب تصویر بندانگشتی (اختیاری)
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
</MudFileUpload>
|
||||
</MudStack>
|
||||
|
||||
<MudSwitch T="bool" @bind-Value="Model.IsActive"
|
||||
Label="محصول فعال"
|
||||
Color="Color.Success" />
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel" Variant="Variant.Text">انصراف</MudButton>
|
||||
<MudButton OnClick="Cancel" Disabled="_loading">انصراف</MudButton>
|
||||
<MudButton OnClick="Submit"
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
Disabled="@(!_isValid || _loading)">
|
||||
@if (_loading)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>@(IsEditMode ? "ذخیره تغییرات" : "ایجاد محصول")</text>
|
||||
}
|
||||
</MudButton>
|
||||
Disabled="@(!_isValid || _loading)">ثبت</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@@ -131,61 +165,52 @@
|
||||
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
[Parameter] public ProductFormModel Model { get; set; } = new();
|
||||
[Parameter] public bool IsEditMode { get; set; }
|
||||
[Inject] private IDiscountCategoryService CategoryService { get; set; } = null!;
|
||||
|
||||
private MudForm? _form;
|
||||
private bool _isValid;
|
||||
private bool _loading;
|
||||
private List<DiscountCategoryDto> _categories = new();
|
||||
private IEnumerable<long> _selectedCategoryIds = new List<long>();
|
||||
private IBrowserFile? _mainImageFile;
|
||||
private IBrowserFile? _thumbnailImageFile;
|
||||
private readonly long _maxAllowedSize = (1024 * 1024) * 5;
|
||||
private string? _mainImagePreview;
|
||||
private string? _thumbnailPreview;
|
||||
private byte[]? _mainImageBuffer;
|
||||
private byte[]? _thumbnailImageBuffer;
|
||||
private List<long> _selectedCategoryIds = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
protected override Task OnInitializedAsync()
|
||||
{
|
||||
await LoadCategories();
|
||||
|
||||
if (Model.CategoryIds?.Any() == true)
|
||||
{
|
||||
_selectedCategoryIds = Model.CategoryIds;
|
||||
}
|
||||
return base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
private async Task LoadCategories()
|
||||
private async Task OnMainImageSelected(IBrowserFile? file)
|
||||
{
|
||||
try
|
||||
_mainImageFile = file;
|
||||
if (file != null)
|
||||
{
|
||||
var tree = await CategoryService.GetCategoriesAsync(isActive: true);
|
||||
_categories = FlattenCategories(tree);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری دستهبندیها: {ex.Message}", Severity.Error);
|
||||
using var ms = new MemoryStream();
|
||||
await file.OpenReadStream(_maxAllowedSize).CopyToAsync(ms);
|
||||
_mainImageBuffer = ms.ToArray();
|
||||
_mainImagePreview = $"data:{file.ContentType};base64," + Convert.ToBase64String(_mainImageBuffer);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private List<DiscountCategoryDto> FlattenCategories(List<DiscountCategoryDto> categories, string prefix = "")
|
||||
private async Task OnThumbnailSelected(IBrowserFile? file)
|
||||
{
|
||||
var result = new List<DiscountCategoryDto>();
|
||||
foreach (var category in categories)
|
||||
_thumbnailImageFile = file;
|
||||
if (file != null)
|
||||
{
|
||||
var catCopy = new DiscountCategoryDto
|
||||
{
|
||||
Id = category.Id,
|
||||
Title = prefix + category.Title,
|
||||
ParentCategoryId = category.ParentCategoryId
|
||||
};
|
||||
result.Add(catCopy);
|
||||
|
||||
if (category.Children.Any())
|
||||
{
|
||||
result.AddRange(FlattenCategories(category.Children.ToList(), prefix + " "));
|
||||
}
|
||||
using var ms = new MemoryStream();
|
||||
await file.OpenReadStream(_maxAllowedSize).CopyToAsync(ms);
|
||||
_thumbnailImageBuffer = ms.ToArray();
|
||||
_thumbnailPreview = $"data:{file.ContentType};base64," + Convert.ToBase64String(_thumbnailImageBuffer);
|
||||
StateHasChanged();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private string GetCategoryPath(DiscountCategoryDto category)
|
||||
{
|
||||
return category.Title;
|
||||
}
|
||||
|
||||
private async Task Submit()
|
||||
@@ -201,6 +226,22 @@
|
||||
// Update CategoryIds from selected values
|
||||
Model.CategoryIds = _selectedCategoryIds.ToList();
|
||||
|
||||
// Handle main image upload
|
||||
if (_mainImageFile != null && _mainImageBuffer != null)
|
||||
{
|
||||
Model.ImageFile = _mainImageBuffer;
|
||||
Model.ImageMime = _mainImageFile.ContentType;
|
||||
Model.ImageFileName = Path.GetFileNameWithoutExtension(_mainImageFile.Name);
|
||||
}
|
||||
|
||||
// Handle thumbnail upload
|
||||
if (_thumbnailImageFile != null && _thumbnailImageBuffer != null)
|
||||
{
|
||||
Model.ThumbnailFile = _thumbnailImageBuffer;
|
||||
Model.ThumbnailMime = _thumbnailImageFile.ContentType;
|
||||
Model.ThumbnailFileName = Path.GetFileNameWithoutExtension(_thumbnailImageFile.Name);
|
||||
}
|
||||
|
||||
MudDialog.Close(DialogResult.Ok(Model));
|
||||
_loading = false;
|
||||
}
|
||||
@@ -219,9 +260,16 @@
|
||||
public string? ThumbnailPath { get; set; }
|
||||
public long Price { get; set; }
|
||||
public int MaxDiscountPercent { get; set; }
|
||||
public int InitialCount { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public List<long>? CategoryIds { get; set; }
|
||||
|
||||
// Image upload properties
|
||||
public byte[]? ImageFile { get; set; }
|
||||
public string? ImageMime { get; set; }
|
||||
public string? ImageFileName { get; set; }
|
||||
public byte[]? ThumbnailFile { get; set; }
|
||||
public string? ThumbnailMime { get; set; }
|
||||
public string? ThumbnailFileName { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Collections"
|
||||
Style="cursor:pointer;">
|
||||
>
|
||||
انتخاب تصاویر (چندتایی)
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
@@ -89,8 +89,9 @@
|
||||
{
|
||||
if (file == null) continue;
|
||||
|
||||
var buffer = new byte[file.Size];
|
||||
await file.OpenReadStream(_maxAllowedSize).ReadAsync(buffer);
|
||||
using var ms = new MemoryStream();
|
||||
await file.OpenReadStream(_maxAllowedSize).CopyToAsync(ms);
|
||||
var buffer = ms.ToArray();
|
||||
var preview = $"data:{file.ContentType};base64," + Convert.ToBase64String(buffer);
|
||||
|
||||
_items.Add(new ProductImageItem
|
||||
|
||||
@@ -1,343 +1,157 @@
|
||||
@page "/discount-categories"
|
||||
@using BackOffice.Services.DiscountCategory
|
||||
@using BackOffice.Pages.DiscountShop.Components
|
||||
@using BackOffice.Common.BaseComponents
|
||||
@using static BackOffice.Pages.DiscountShop.Components.CategoryFormDialog
|
||||
@inject IDiscountCategoryService DiscountCategoryService
|
||||
@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.Category" Class="ml-2" />
|
||||
مدیریت دستهبندیهای فروشگاه تخفیفی
|
||||
</MudText>
|
||||
|
||||
<MudPaper Class="pa-4 mt-4">
|
||||
<BasePageComponent @ref="_basePage" OnClearFilterClick="OnFilterCleared" OnSubmitClick="OnFilterSubmit">
|
||||
<Filters>
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudTextField T="string" Clearable="true" Label="جستجو در دستهبندیها"
|
||||
@bind-Value="@_searchQuery" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
</MudItem>
|
||||
</Filters>
|
||||
<Content>
|
||||
<MudGrid>
|
||||
<MudItem xs="12" sm="8">
|
||||
<MudTextField @bind-Value="_searchQuery"
|
||||
Label="جستجو در دستهبندیها"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||
OnKeyUp="@(() => FilterCategories())" />
|
||||
@* ────── Tree sidebar (same as regular store) ────── *@
|
||||
<MudItem xs="12" md="3">
|
||||
<MudPaper Class="pa-2" Style="height:calc(100vh - 240px); overflow:auto;">
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2">درخت دستهبندیها</MudText>
|
||||
<MudList T="string" Dense="true">
|
||||
<MudListItem T="string"
|
||||
OnClick="@(() => OnTreeNodeSelected(null))"
|
||||
Selected="@(_selectedParentId == null)">
|
||||
<MudText Typo="Typo.body2">همه دستهبندیها</MudText>
|
||||
</MudListItem>
|
||||
@foreach (var node in _treeNodes)
|
||||
{
|
||||
<MudListItem T="string"
|
||||
OnClick="@(() => OnTreeNodeSelected(node.Id))"
|
||||
Selected="@(_selectedParentId == node.Id)">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
<div style="width:@(node.Depth * 16)px;"></div>
|
||||
<MudText Typo="Typo.body2">@node.Title</MudText>
|
||||
</MudStack>
|
||||
</MudListItem>
|
||||
}
|
||||
</MudList>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
FullWidth="true"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="@(() => OpenCreateDialog())">
|
||||
دستهبندی جدید
|
||||
</MudButton>
|
||||
|
||||
@* ────── Grid (9 cols) ────── *@
|
||||
<MudItem xs="12" md="9">
|
||||
<MudDataGrid T="DiscountCategoryDto"
|
||||
Items="@_filteredCategories"
|
||||
Hover="true"
|
||||
Dense="true"
|
||||
Height="calc(100vh - 240px)">
|
||||
<ColGroup>
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
<col style="width: 80px;" />
|
||||
</ColGroup>
|
||||
<ToolBarContent>
|
||||
<MudText>مدیریت دستهبندیها</MudText>
|
||||
<MudSpacer />
|
||||
<MudStack Spacing="2" Row="true" Justify="Justify.Center" AlignItems="AlignItems.Center">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Large"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => OpenCreateDialog())">
|
||||
افزودن
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</ToolBarContent>
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
<PropertyColumn Property="x => x.Name" Title="نام لاتین" />
|
||||
<PropertyColumn Property="x => x.Title" Title="عنوان" />
|
||||
|
||||
<TemplateColumn Title="دستهبندی والد">
|
||||
<CellTemplate>
|
||||
@{
|
||||
var parentTitle = "-";
|
||||
if (context.Item.ParentCategoryId.HasValue &&
|
||||
context.Item.ParentCategoryId.Value > 0)
|
||||
{
|
||||
var parent = _allCategories.FirstOrDefault(c => c.Id == context.Item.ParentCategoryId.Value);
|
||||
if (parent != null)
|
||||
{
|
||||
parentTitle = parent.Title;
|
||||
}
|
||||
}
|
||||
}
|
||||
<MudText Typo="Typo.body2">@parentTitle</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="تعداد محصولات">
|
||||
<CellTemplate>
|
||||
<MudChip T="string" Color="Color.Info" Size="Size.Small">@context.Item.ProductCount</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.SortOrder" Title="ترتیب" />
|
||||
|
||||
<TemplateColumn Title="فعال؟">
|
||||
<CellTemplate>
|
||||
<MudChip T="string"
|
||||
Color="@(context.Item.IsActive ? Color.Success : Color.Error)"
|
||||
Variant="Variant.Filled"
|
||||
Size="Size.Small">
|
||||
@(context.Item.IsActive ? "فعال" : "غیرفعال")
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn StickyLeft="true" Title="عملیات" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
|
||||
<CellTemplate>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
<MudTooltip Text="افزودن زیردسته">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Add"
|
||||
Color="Color.Success"
|
||||
Size="Size.Small"
|
||||
OnClick="@(() => OpenCreateDialog(context.Item.Id))" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="ویرایش">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.EditNote"
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => OpenEditDialog(context.Item.Id))" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="حذف">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.DeleteOutline"
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
Disabled="@(context.Item.Children.Any())"
|
||||
OnClick="@(() => DeleteCategory(context.Item.Id))" />
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="مدیریت محصولات این دسته (درگ و دراپ)">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ShoppingCart"
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => OpenCategoryProducts(context.Item))" />
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="DiscountCategoryDto"
|
||||
PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mt-4" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudDataGrid T="DiscountCategoryDto"
|
||||
Items="@_filteredCategories"
|
||||
Hover="true"
|
||||
Dense="true"
|
||||
Class="mt-4">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
|
||||
<TemplateColumn Title="عنوان">
|
||||
<CellTemplate>
|
||||
@{
|
||||
var indent = GetIndentLevel(context.Item);
|
||||
}
|
||||
<div style="padding-right: @(indent * 20)px;">
|
||||
@if (context.Item.Children.Any())
|
||||
{
|
||||
<MudIcon Icon="@Icons.Material.Filled.Folder" Size="Size.Small" Class="ml-1" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudIcon Icon="@Icons.Material.Filled.FolderOpen" Size="Size.Small" Class="ml-1" />
|
||||
}
|
||||
<strong>@context.Item.Title</strong>
|
||||
@if (!string.IsNullOrEmpty(context.Item.Name))
|
||||
{
|
||||
<MudText Typo="Typo.caption" Class="mr-2">(@context.Item.Name)</MudText>
|
||||
}
|
||||
</div>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="تعداد محصولات">
|
||||
<CellTemplate>
|
||||
<MudChip T="string" Color="Color.Info" Size="Size.Small">@context.Item.ProductCount</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.SortOrder" Title="ترتیب" />
|
||||
|
||||
<TemplateColumn Title="وضعیت">
|
||||
<CellTemplate>
|
||||
<MudChip T="string" Color="@(context.Item.IsActive ? Color.Success : Color.Default)"
|
||||
Size="Size.Small">
|
||||
@(context.Item.IsActive ? "فعال" : "غیرفعال")
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="عملیات" Sortable="false">
|
||||
<CellTemplate>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Add"
|
||||
Color="Color.Success"
|
||||
Size="Size.Small"
|
||||
Title="افزودن زیردسته"
|
||||
OnClick="@(() => OpenCreateDialog(context.Item.Id))" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
Title="ویرایش"
|
||||
OnClick="@(() => OpenEditDialog(context.Item.Id))" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Color="Color.Error"
|
||||
Size="Size.Small"
|
||||
Title="حذف"
|
||||
Disabled="@(context.Item.Children.Any())"
|
||||
OnClick="@(() => DeleteCategory(context.Item.Id))" />
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="DiscountCategoryDto" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
|
||||
@if (!_filteredCategories.Any())
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Class="mt-4">
|
||||
@if (string.IsNullOrEmpty(_searchQuery))
|
||||
{
|
||||
<text>هنوز دستهبندیای ایجاد نشده است.</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>دستهبندی با عبارت "@_searchQuery" یافت نشد.</text>
|
||||
}
|
||||
</MudAlert>
|
||||
}
|
||||
}
|
||||
</MudPaper>
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
private List<DiscountCategoryDto> _allCategories = new();
|
||||
private List<DiscountCategoryDto> _filteredCategories = new();
|
||||
private bool _loading = false;
|
||||
private string? _searchQuery;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadCategories();
|
||||
}
|
||||
|
||||
private async Task LoadCategories()
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var rootCategories = await DiscountCategoryService.GetCategoriesAsync();
|
||||
_allCategories = FlattenCategoriesWithDepth(rootCategories, 0);
|
||||
FilterCategories();
|
||||
Snackbar.Add("دستهبندیها بارگذاری شدند", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری دستهبندیها: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private List<DiscountCategoryDto> FlattenCategoriesWithDepth(List<DiscountCategoryDto> categories, int depth)
|
||||
{
|
||||
var result = new List<DiscountCategoryDto>();
|
||||
foreach (var category in categories.OrderBy(c => c.SortOrder))
|
||||
{
|
||||
category.IsExpanded = false; // Use IsExpanded to store depth temporarily
|
||||
result.Add(category);
|
||||
if (category.Children.Any())
|
||||
{
|
||||
result.AddRange(FlattenCategoriesWithDepth(category.Children, depth + 1));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private int GetIndentLevel(DiscountCategoryDto category)
|
||||
{
|
||||
// Calculate depth by traversing parents
|
||||
int depth = 0;
|
||||
var current = category;
|
||||
while (current.ParentCategoryId.HasValue)
|
||||
{
|
||||
var parent = _allCategories.FirstOrDefault(c => c.Id == current.ParentCategoryId.Value);
|
||||
if (parent == null) break;
|
||||
current = parent;
|
||||
depth++;
|
||||
}
|
||||
return depth;
|
||||
}
|
||||
|
||||
private void FilterCategories()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_searchQuery))
|
||||
{
|
||||
_filteredCategories = _allCategories;
|
||||
}
|
||||
else
|
||||
{
|
||||
var query = _searchQuery.ToLower();
|
||||
_filteredCategories = _allCategories
|
||||
.Where(c => c.Title.ToLower().Contains(query) ||
|
||||
c.Name.ToLower().Contains(query) ||
|
||||
(c.Description?.ToLower().Contains(query) ?? false))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenCreateDialog(long? parentId = null)
|
||||
{
|
||||
var model = new CategoryFormModel { ParentCategoryId = parentId };
|
||||
var parameters = new DialogParameters<CategoryFormDialog>
|
||||
{
|
||||
{ x => x.Model, model },
|
||||
{ x => x.IsEditMode, false },
|
||||
{ x => x.ExcludeCategoryId, (long?)null }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<CategoryFormDialog>(
|
||||
parentId.HasValue ? "ایجاد زیردسته" : "ایجاد دستهبندی جدید",
|
||||
parameters,
|
||||
options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false, Data: CategoryFormModel formData })
|
||||
{
|
||||
try
|
||||
{
|
||||
var dto = new CreateDiscountCategoryDto
|
||||
{
|
||||
ParentCategoryId = formData.ParentCategoryId,
|
||||
Name = formData.Name,
|
||||
Title = formData.Title,
|
||||
Description = formData.Description,
|
||||
ImagePath = formData.ImagePath,
|
||||
SortOrder = formData.SortOrder,
|
||||
IsActive = formData.IsActive
|
||||
};
|
||||
|
||||
await DiscountCategoryService.CreateAsync(dto);
|
||||
Snackbar.Add("دستهبندی با موفقیت ایجاد شد", Severity.Success);
|
||||
await LoadCategories();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ایجاد دستهبندی: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenEditDialog(long categoryId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var category = await DiscountCategoryService.GetByIdAsync(categoryId);
|
||||
if (category == null)
|
||||
{
|
||||
Snackbar.Add("دستهبندی یافت نشد", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var model = new CategoryFormModel
|
||||
{
|
||||
ParentCategoryId = category.ParentCategoryId,
|
||||
Name = category.Name,
|
||||
Title = category.Title,
|
||||
Description = category.Description,
|
||||
ImagePath = category.ImagePath,
|
||||
SortOrder = category.SortOrder,
|
||||
IsActive = category.IsActive
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters<CategoryFormDialog>
|
||||
{
|
||||
{ x => x.Model, model },
|
||||
{ x => x.IsEditMode, true },
|
||||
{ x => x.ExcludeCategoryId, categoryId }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<CategoryFormDialog>("ویرایش دستهبندی", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false, Data: CategoryFormModel formData })
|
||||
{
|
||||
var dto = new UpdateDiscountCategoryDto
|
||||
{
|
||||
ParentCategoryId = formData.ParentCategoryId,
|
||||
Name = formData.Name,
|
||||
Title = formData.Title,
|
||||
Description = formData.Description,
|
||||
ImagePath = formData.ImagePath,
|
||||
SortOrder = formData.SortOrder,
|
||||
IsActive = formData.IsActive
|
||||
};
|
||||
|
||||
await DiscountCategoryService.UpdateAsync(categoryId, dto);
|
||||
Snackbar.Add("دستهبندی با موفقیت ویرایش شد", Severity.Success);
|
||||
await LoadCategories();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ویرایش دستهبندی: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteCategory(long categoryId)
|
||||
{
|
||||
var category = _allCategories.FirstOrDefault(c => c.Id == categoryId);
|
||||
if (category?.Children.Any() == true)
|
||||
{
|
||||
Snackbar.Add("ابتدا زیردستههای این دستهبندی را حذف کنید", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await DialogService.ShowMessageBox(
|
||||
"تأیید حذف",
|
||||
"آیا از حذف این دستهبندی اطمینان دارید؟",
|
||||
yesText: "بله", cancelText: "خیر");
|
||||
|
||||
if (result == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DiscountCategoryService.DeleteAsync(categoryId);
|
||||
Snackbar.Add("دستهبندی با موفقیت حذف شد", Severity.Success);
|
||||
await LoadCategories();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در حذف دستهبندی: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</Content>
|
||||
</BasePageComponent>
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
using BackOffice.Common.BaseComponents;
|
||||
using BackOffice.Common.Utilities;
|
||||
using BackOffice.Services.DiscountCategory;
|
||||
using BackOffice.Pages.DiscountShop.Components;
|
||||
using static BackOffice.Pages.DiscountShop.Components.CategoryFormDialog;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.DiscountShop;
|
||||
|
||||
public partial class DiscountCategoriesMainPage
|
||||
{
|
||||
[Inject] public IDiscountCategoryService DiscountCategoryService { get; set; } = default!;
|
||||
|
||||
private List<DiscountCategoryDto> _allCategories = new();
|
||||
private List<DiscountCategoryDto> _filteredCategories = new();
|
||||
private List<TreeNodeModel> _treeNodes = new();
|
||||
private BasePageComponent _basePage = default!;
|
||||
private bool _loading;
|
||||
private string? _searchQuery;
|
||||
private long? _selectedParentId;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadCategories();
|
||||
}
|
||||
|
||||
private async Task LoadCategories()
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var rootCategories = await DiscountCategoryService.GetCategoriesAsync();
|
||||
_allCategories = FlattenCategoriesWithDepth(rootCategories, 0);
|
||||
BuildTreeNodes(rootCategories);
|
||||
FilterCategories();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری دستهبندیها: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildTreeNodes(List<DiscountCategoryDto> categories, int depth = 0)
|
||||
{
|
||||
_treeNodes.Clear();
|
||||
BuildTreeNodesRecursive(categories, depth);
|
||||
}
|
||||
|
||||
private void BuildTreeNodesRecursive(List<DiscountCategoryDto> categories, int depth)
|
||||
{
|
||||
foreach (var cat in categories.OrderBy(c => c.SortOrder))
|
||||
{
|
||||
_treeNodes.Add(new TreeNodeModel
|
||||
{
|
||||
Id = cat.Id,
|
||||
Title = cat.Title,
|
||||
Depth = depth,
|
||||
HasChildren = cat.Children.Any()
|
||||
});
|
||||
if (cat.Children.Any())
|
||||
{
|
||||
BuildTreeNodesRecursive(cat.Children, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<DiscountCategoryDto> FlattenCategoriesWithDepth(List<DiscountCategoryDto> categories, int depth)
|
||||
{
|
||||
var result = new List<DiscountCategoryDto>();
|
||||
foreach (var category in categories.OrderBy(c => c.SortOrder))
|
||||
{
|
||||
result.Add(category);
|
||||
if (category.Children.Any())
|
||||
{
|
||||
result.AddRange(FlattenCategoriesWithDepth(category.Children, depth + 1));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void OnTreeNodeSelected(long? parentId)
|
||||
{
|
||||
_selectedParentId = parentId;
|
||||
FilterCategories();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void FilterCategories()
|
||||
{
|
||||
var items = _allCategories.AsEnumerable();
|
||||
|
||||
if (_selectedParentId.HasValue)
|
||||
{
|
||||
items = items.Where(c =>
|
||||
c.Id == _selectedParentId.Value ||
|
||||
c.ParentCategoryId == _selectedParentId.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_searchQuery))
|
||||
{
|
||||
var query = _searchQuery.ToLower();
|
||||
items = items.Where(c =>
|
||||
c.Title.ToLower().Contains(query) ||
|
||||
c.Name.ToLower().Contains(query) ||
|
||||
(c.Description?.ToLower().Contains(query) ?? false));
|
||||
}
|
||||
|
||||
_filteredCategories = items.ToList();
|
||||
}
|
||||
|
||||
private async Task OnFilterSubmit()
|
||||
{
|
||||
_basePage.IsFiltered = true;
|
||||
FilterCategories();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task OnFilterCleared()
|
||||
{
|
||||
_basePage.IsFiltered = false;
|
||||
_searchQuery = null;
|
||||
_selectedParentId = null;
|
||||
FilterCategories();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task OpenCreateDialog(long? parentId = null)
|
||||
{
|
||||
var model = new CategoryFormModel { ParentCategoryId = parentId };
|
||||
var parameters = new DialogParameters<CategoryFormDialog>
|
||||
{
|
||||
{ x => x.Model, model },
|
||||
{ x => x.IsEditMode, false },
|
||||
{ x => x.ExcludeCategoryId, (long?)null }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<CategoryFormDialog>(
|
||||
parentId.HasValue ? "ایجاد زیردسته" : "ایجاد دستهبندی جدید",
|
||||
parameters,
|
||||
options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false, Data: CategoryFormModel formData })
|
||||
{
|
||||
try
|
||||
{
|
||||
var dto = new CreateDiscountCategoryDto
|
||||
{
|
||||
ParentCategoryId = formData.ParentCategoryId,
|
||||
Name = formData.Name,
|
||||
Title = formData.Title,
|
||||
Description = formData.Description,
|
||||
ImagePath = formData.ImagePath,
|
||||
SortOrder = formData.SortOrder,
|
||||
IsActive = formData.IsActive
|
||||
};
|
||||
|
||||
await DiscountCategoryService.CreateAsync(dto);
|
||||
Snackbar.Add("دستهبندی با موفقیت ایجاد شد", Severity.Success);
|
||||
await LoadCategories();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ایجاد دستهبندی: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenEditDialog(long categoryId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var category = await DiscountCategoryService.GetByIdAsync(categoryId);
|
||||
if (category == null)
|
||||
{
|
||||
Snackbar.Add("دستهبندی یافت نشد", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var model = new CategoryFormModel
|
||||
{
|
||||
ParentCategoryId = category.ParentCategoryId,
|
||||
Name = category.Name,
|
||||
Title = category.Title,
|
||||
Description = category.Description,
|
||||
ImagePath = category.ImagePath,
|
||||
SortOrder = category.SortOrder,
|
||||
IsActive = category.IsActive
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters<CategoryFormDialog>
|
||||
{
|
||||
{ x => x.Model, model },
|
||||
{ x => x.IsEditMode, true },
|
||||
{ x => x.ExcludeCategoryId, categoryId }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<CategoryFormDialog>("ویرایش دستهبندی", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false, Data: CategoryFormModel formData })
|
||||
{
|
||||
var dto = new UpdateDiscountCategoryDto
|
||||
{
|
||||
ParentCategoryId = formData.ParentCategoryId,
|
||||
Name = formData.Name,
|
||||
Title = formData.Title,
|
||||
Description = formData.Description,
|
||||
ImagePath = formData.ImagePath,
|
||||
SortOrder = formData.SortOrder,
|
||||
IsActive = formData.IsActive
|
||||
};
|
||||
|
||||
await DiscountCategoryService.UpdateAsync(categoryId, dto);
|
||||
Snackbar.Add("دستهبندی با موفقیت ویرایش شد", Severity.Success);
|
||||
await LoadCategories();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ویرایش دستهبندی: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteCategory(long categoryId)
|
||||
{
|
||||
var category = _allCategories.FirstOrDefault(c => c.Id == categoryId);
|
||||
if (category?.Children.Any() == true)
|
||||
{
|
||||
Snackbar.Add("ابتدا زیردستههای این دستهبندی را حذف کنید", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small };
|
||||
bool? result = await DialogService.ShowMessageBox(
|
||||
"اخطار",
|
||||
$"آیا از حذف دستهبندی «{category?.Title}» مطمئن هستید؟",
|
||||
yesText: "حذف", cancelText: "لغو",
|
||||
options: options);
|
||||
|
||||
if (result == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DiscountCategoryService.DeleteAsync(categoryId);
|
||||
Snackbar.Add("دستهبندی با موفقیت حذف شد", Severity.Success);
|
||||
await LoadCategories();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در حذف دستهبندی: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TreeNodeModel
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public int Depth { get; set; }
|
||||
public bool HasChildren { get; set; }
|
||||
}
|
||||
|
||||
public void OpenCategoryProducts(DiscountCategoryDto model)
|
||||
{
|
||||
Navigation.NavigateTo($"{RouteConstance.CategoryProducts}{model.Id}");
|
||||
}
|
||||
}
|
||||
@@ -1,283 +1,213 @@
|
||||
@page "/discount-orders"
|
||||
@* Merged into /discount-shop *@
|
||||
@using BackOffice.Services.DiscountOrder
|
||||
@using BackOffice.Pages.DiscountShop.Components
|
||||
@inject IDiscountOrderService DiscountOrderService
|
||||
@inject IDialogService DialogService
|
||||
@inject ISnackbar Snackbar
|
||||
@using BackOffice.Common.BaseComponents
|
||||
@using BackOffice.Pages.AutoComplete
|
||||
|
||||
<PageTitle>مدیریت سفارشات فروشگاه تخفیفی</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-4">
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ShoppingCart" Class="ml-2" />
|
||||
مدیریت سفارشات فروشگاه تخفیفی
|
||||
</MudText>
|
||||
|
||||
<MudPaper Class="pa-4 mt-4">
|
||||
<MudGrid>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField @bind-Value="_searchQuery"
|
||||
Label="جستجو (شماره سفارش)"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||
OnKeyUp="@(() => OnSearch())" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect @bind-Value="_statusFilter"
|
||||
Label="وضعیت سفارش"
|
||||
Variant="Variant.Outlined"
|
||||
T="OrderStatus?"
|
||||
Clearable="true"
|
||||
OnClearButtonClick="@(() => OnSearch())">
|
||||
<MudSelectItem Value="@((OrderStatus?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Pending">در انتظار پرداخت</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Paid">پرداخت شده</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Processing">در حال آمادهسازی</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
|
||||
<MudSelectItem Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect @bind-Value="_paymentFilter"
|
||||
Label="وضعیت پرداخت"
|
||||
Variant="Variant.Outlined"
|
||||
T="bool?"
|
||||
Clearable="true">
|
||||
<MudSelectItem Value="@((bool?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem Value="@((bool?)true)">پرداخت شده</MudSelectItem>
|
||||
<MudSelectItem Value="@((bool?)false)">پرداخت نشده</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
FullWidth="true"
|
||||
StartIcon="@Icons.Material.Filled.Search"
|
||||
OnClick="@(() => OnSearch())">
|
||||
جستجو
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudDataGrid @ref="_dataGrid" T="DiscountOrderDto"
|
||||
Items="@_orders"
|
||||
<BackOffice.Common.BaseComponents.BasePageComponent @ref="_basePage" OnClearFilterClick="OnFilterCleared"
|
||||
OnSubmitClick="OnFilterSubmit">
|
||||
<Filters>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudNumericField HideSpinButtons="true" T="long?" Clearable="true" Label="شناسه سفارش"
|
||||
@bind-Value="@_orderIdFilter" Variant="Variant.Outlined" Margin="Margin.Dense"/>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<UserAutoComplete Label="جستجوی کاربر" @bind-SelectedUserId="@_userIdFilter" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField T="string" Clearable="true" Label="موبایل کاربر"
|
||||
@bind-Value="@_userMobileFilter" Variant="Variant.Outlined" Margin="Margin.Dense"/>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField T="string" Clearable="true" Label="کد رهگیری"
|
||||
@bind-Value="@_trackingCodeFilter" Variant="Variant.Outlined" Margin="Margin.Dense"/>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudDatePicker T="DateTime?" Label="از تاریخ" @bind-Date="_fromDateFilter"
|
||||
Variant="Variant.Outlined" Margin="Margin.Dense"/>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudDatePicker T="DateTime?" Label="تا تاریخ" @bind-Date="_toDateFilter"
|
||||
Variant="Variant.Outlined" Margin="Margin.Dense"/>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect T="int?" Clearable="true" Label="وضعیت پرداخت" Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" @bind-Value="_paymentStatusFilter">
|
||||
<MudSelectItem T="int?" Value="@((int?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(0)">در انتظار پرداخت</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(1)">پرداخت شده</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(2)">ناموفق</MudSelectItem>
|
||||
<MudSelectItem T="int?" Value="@(3)">بازپرداخت</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect T="OrderStatus?" Clearable="true" Label="وضعیت ارسال" Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense" @bind-Value="_statusFilter">
|
||||
<MudSelectItem T="OrderStatus?" Value="@((OrderStatus?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Pending">در انتظار ارسال</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Processing">در حال آمادهسازی</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
</Filters>
|
||||
<Content>
|
||||
<MudDataGrid T="DiscountOrderDto"
|
||||
Items="@_orders"
|
||||
Loading="@_loading"
|
||||
Hover="true"
|
||||
Dense="true"
|
||||
Class="mt-4">
|
||||
Hover="true" @ref="_dataGrid" Height="calc(100vh - 240px)">
|
||||
<ColGroup>
|
||||
<col/>
|
||||
<col/>
|
||||
<col/>
|
||||
<col style="width: 58px;"/>
|
||||
</ColGroup>
|
||||
<ToolBarContent>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Style="width:100%">
|
||||
<MudText>لیست سفارشات</MudText>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Info"
|
||||
OnClick="ExportToExcel"
|
||||
StartIcon="@Icons.Material.Filled.FileDownload"
|
||||
Size="Size.Small">
|
||||
خروجی Excel
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</ToolBarContent>
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.OrderNumber" Title="شماره سفارش" />
|
||||
|
||||
<PropertyColumn Property="x => x.Created" Title="تاریخ ثبت" Format="yyyy/MM/dd HH:mm" />
|
||||
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه"/>
|
||||
|
||||
<TemplateColumn Title="نام کاربر">
|
||||
<CellTemplate>
|
||||
@if (context.Item.UserId > 0)
|
||||
{
|
||||
<MudLink Href="@($"/network/user-info/{context.Item.UserId}")" Underline="Underline.Hover" Color="Color.Primary">
|
||||
@context.Item.UserFullName
|
||||
</MudLink>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText>@context.Item.UserFullName</MudText>
|
||||
}
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.UserMobile" Title="موبایل"/>
|
||||
|
||||
<TemplateColumn Title="مبلغ کل">
|
||||
<CellTemplate>
|
||||
<MudText>@context.Item.TotalPrice.ToString("N0") ریال</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="تخفیف">
|
||||
|
||||
<TemplateColumn Title="تخفیف کیفپول">
|
||||
<CellTemplate>
|
||||
<MudText Color="Color.Success">@context.Item.DiscountBalanceUsed.ToString("N0") ریال</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="قابل پرداخت">
|
||||
|
||||
<TemplateColumn Title="پرداخت درگاه">
|
||||
<CellTemplate>
|
||||
<MudText Typo="Typo.body2">
|
||||
<strong>@context.Item.GatewayAmount.ToString("N0") ریال</strong>
|
||||
</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="تعداد">
|
||||
|
||||
<TemplateColumn Title="تعداد آیتم">
|
||||
<CellTemplate>
|
||||
<MudChip T="string" Color="Color.Info" Size="Size.Small">@context.Item.ItemsCount آیتم</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="وضعیت">
|
||||
|
||||
<TemplateColumn Title="وضعیت پرداخت">
|
||||
<CellTemplate>
|
||||
<MudChip T="string" Color="@GetStatusColor(context.Item.Status)" Size="Size.Small">
|
||||
<MudChip T="string"
|
||||
Size="Size.Small"
|
||||
Color="@GetPaymentStatusColor(context.Item.PaymentStatusValue)">
|
||||
@GetPaymentStatusText(context.Item.PaymentStatusValue)
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="تاریخ پرداخت" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
|
||||
<CellTemplate>
|
||||
@(context.Item.PaymentDate?.MiladiToJalaliWithTime() ?? "-")
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="آدرس">
|
||||
<CellTemplate>
|
||||
<MudTooltip Text="@context.Item.ShippingAddress">
|
||||
<MudText Typo="Typo.body2">
|
||||
@(string.IsNullOrWhiteSpace(context.Item.ShippingAddress)
|
||||
? "-"
|
||||
: (context.Item.ShippingAddress.Length > 30
|
||||
? context.Item.ShippingAddress.Substring(0, 30) + "..."
|
||||
: context.Item.ShippingAddress))
|
||||
</MudText>
|
||||
</MudTooltip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="وضعیت ارسال">
|
||||
<CellTemplate>
|
||||
<MudChip T="string"
|
||||
Size="Size.Small"
|
||||
Color="@GetStatusColor(context.Item.Status)">
|
||||
@GetStatusText(context.Item.Status)
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="پرداخت">
|
||||
|
||||
<TemplateColumn Title="تاریخ ثبت" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
|
||||
<CellTemplate>
|
||||
@if (context.Item.PaymentCompleted)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Success" Size="Size.Small" Icon="@Icons.Material.Filled.CheckCircle">
|
||||
پرداخت شده
|
||||
</MudChip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChip T="string" Color="Color.Warning" Size="Size.Small" Icon="@Icons.Material.Filled.Schedule">
|
||||
در انتظار
|
||||
</MudChip>
|
||||
}
|
||||
@(context.Item.Created?.MiladiToJalaliWithTime() ?? "—")
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="عملیات" Sortable="false">
|
||||
|
||||
<TemplateColumn StickyLeft="true" Title="عملیات" CellStyle="text-wrap: nowrap;"
|
||||
HeaderStyle="text-wrap: nowrap;">
|
||||
<CellTemplate>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Visibility"
|
||||
Color="Color.Info"
|
||||
Size="Size.Small"
|
||||
Title="مشاهده جزئیات"
|
||||
OnClick="@(() => OpenOrderDetails(context.Item.Id))" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
Title="تغییر وضعیت"
|
||||
OnClick="@(() => OpenChangeStatusDialog(context.Item.Id))" />
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
|
||||
<MudTooltip Text="جزئیات سفارش">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Info"
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => OpenOrderDetails(context.Item.Id))"
|
||||
/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="تغییر وضعیت ارسال">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.LocalShipping"
|
||||
Size="Size.Small"
|
||||
Color="Color.Primary"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => OpenChangeStatusDialog(context.Item))"
|
||||
/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="لغو سفارش">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Cancel" Size="Size.Small"
|
||||
Color="Color.Error"
|
||||
ButtonType="ButtonType.Button" OnClick="@(() => OnDelete(context.Item))"
|
||||
/>
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="DiscountOrderDto" />
|
||||
<MudDataGridPager T="DiscountOrderDto" PageSizeOptions=@(new int[] { 20, 50, 100 })
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه"/>
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudPaper>
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
private MudDataGrid<DiscountOrderDto>? _dataGrid;
|
||||
private List<DiscountOrderDto> _orders = new();
|
||||
private bool _loading = false;
|
||||
private int _totalCount;
|
||||
private int _totalPages;
|
||||
|
||||
private string? _searchQuery;
|
||||
private OrderStatus? _statusFilter;
|
||||
private bool? _paymentFilter;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadOrders();
|
||||
}
|
||||
|
||||
private async Task LoadOrders()
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var filter = new OrderFilterDto
|
||||
{
|
||||
Status = _statusFilter,
|
||||
PaymentCompleted = _paymentFilter
|
||||
};
|
||||
|
||||
var (orders, totalCount, totalPages) = await DiscountOrderService.GetOrdersAsync(filter);
|
||||
_orders = orders;
|
||||
_totalCount = totalCount;
|
||||
_totalPages = totalPages;
|
||||
Snackbar.Add("سفارشات بارگذاری شدند", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری سفارشات: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnSearch()
|
||||
{
|
||||
await LoadOrders();
|
||||
}
|
||||
|
||||
private async Task OpenOrderDetails(long orderId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var order = await DiscountOrderService.GetByIdAsync(orderId);
|
||||
if (order == null)
|
||||
{
|
||||
Snackbar.Add("سفارش یافت نشد", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var parameters = new DialogParameters<OrderDetailsDialog> { { x => x.Order, order } };
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Large, FullWidth = true };
|
||||
await DialogService.ShowAsync<OrderDetailsDialog>("جزئیات سفارش", parameters, options);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری جزئیات: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenChangeStatusDialog(long orderId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var order = _orders.FirstOrDefault(o => o.Id == orderId);
|
||||
if (order == null)
|
||||
{
|
||||
Snackbar.Add("سفارش یافت نشد", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var parameters = new DialogParameters<ChangeOrderStatusDialog>
|
||||
{
|
||||
{ x => x.OrderId, orderId },
|
||||
{ x => x.CurrentStatus, order.Status }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<ChangeOrderStatusDialog>("تغییر وضعیت سفارش", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false, Data: UpdateOrderStatusDto dto })
|
||||
{
|
||||
await DiscountOrderService.UpdateStatusAsync(orderId, dto);
|
||||
Snackbar.Add("وضعیت سفارش با موفقیت تغییر یافت", Severity.Success);
|
||||
await LoadOrders();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در تغییر وضعیت: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private Color GetStatusColor(OrderStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
OrderStatus.Pending => Color.Warning,
|
||||
OrderStatus.Paid => Color.Info,
|
||||
OrderStatus.Processing => Color.Primary,
|
||||
OrderStatus.Shipped => Color.Secondary,
|
||||
OrderStatus.Delivered => Color.Success,
|
||||
OrderStatus.Cancelled => Color.Error,
|
||||
OrderStatus.Returned => Color.Dark,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private string GetStatusText(OrderStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
OrderStatus.Pending => "در انتظار پرداخت",
|
||||
OrderStatus.Paid => "پرداخت شده",
|
||||
OrderStatus.Processing => "در حال آمادهسازی",
|
||||
OrderStatus.Shipped => "ارسال شده",
|
||||
OrderStatus.Delivered => "تحویل داده شده",
|
||||
OrderStatus.Cancelled => "لغو شده",
|
||||
OrderStatus.Returned => "مرجوع شده",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
</Content>
|
||||
</BackOffice.Common.BaseComponents.BasePageComponent>
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
using System.Text;
|
||||
using BackOffice.Common.BaseComponents;
|
||||
using BackOffice.Services.DiscountOrder;
|
||||
using BackOffice.Pages.DiscountShop.Components;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using MudBlazor;
|
||||
using DateTimeConverterCL;
|
||||
|
||||
namespace BackOffice.Pages.DiscountShop;
|
||||
|
||||
public partial class DiscountOrdersMainPage
|
||||
{
|
||||
[Inject] public IDiscountOrderService DiscountOrderService { get; set; } = default!;
|
||||
[Inject] public IJSRuntime JsRuntime { get; set; } = default!;
|
||||
|
||||
private MudDataGrid<DiscountOrderDto>? _dataGrid;
|
||||
private BasePageComponent _basePage = default!;
|
||||
private List<DiscountOrderDto> _orders = new();
|
||||
private bool _loading;
|
||||
|
||||
// فیلترها - یکسان با فروشگاه عادی
|
||||
private long? _orderIdFilter;
|
||||
private long? _userIdFilter;
|
||||
private string? _userMobileFilter;
|
||||
private string? _trackingCodeFilter;
|
||||
private DateTime? _fromDateFilter;
|
||||
private DateTime? _toDateFilter;
|
||||
private int? _paymentStatusFilter;
|
||||
private OrderStatus? _statusFilter;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadOrders();
|
||||
}
|
||||
|
||||
private async Task LoadOrders()
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var filter = new OrderFilterDto
|
||||
{
|
||||
UserId = _userIdFilter ?? _orderIdFilter, // orderId → search by userId too
|
||||
Status = _statusFilter,
|
||||
PaymentStatus = _paymentStatusFilter,
|
||||
UserMobile = _userMobileFilter,
|
||||
TrackingCode = _trackingCodeFilter,
|
||||
FromDate = _fromDateFilter,
|
||||
ToDate = _toDateFilter
|
||||
};
|
||||
|
||||
var (orders, totalCount, totalPages) = await DiscountOrderService.GetOrdersAsync(filter);
|
||||
_orders = orders;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری سفارشات: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenOrderDetails(long orderId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var order = await DiscountOrderService.GetByIdAsync(orderId);
|
||||
if (order == null)
|
||||
{
|
||||
Snackbar.Add("سفارش یافت نشد", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var parameters = new DialogParameters<OrderDetailsDialog> { { x => x.Order, order } };
|
||||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Large, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<OrderDetailsDialog>("جزئیات سفارش", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false })
|
||||
{
|
||||
await LoadOrders();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری جزئیات: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenChangeStatusDialog(DiscountOrderDto order)
|
||||
{
|
||||
var parameters = new DialogParameters<ChangeOrderStatusDialog>
|
||||
{
|
||||
{ x => x.OrderId, order.Id },
|
||||
{ x => x.CurrentStatus, order.Status }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<ChangeOrderStatusDialog>(
|
||||
"تغییر وضعیت سفارش", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false, Data: UpdateOrderStatusDto dto })
|
||||
{
|
||||
try
|
||||
{
|
||||
await DiscountOrderService.UpdateStatusAsync(order.Id, dto);
|
||||
Snackbar.Add("وضعیت سفارش با موفقیت تغییر یافت", Severity.Success);
|
||||
await LoadOrders();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در تغییر وضعیت: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnDelete(DiscountOrderDto model)
|
||||
{
|
||||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small };
|
||||
bool? result = await DialogService.ShowMessageBox(
|
||||
"اخطار",
|
||||
$"آیا از لغو سفارش شماره «{model.Id}» مطمئن هستید؟ این عملیات وضعیت سفارش را به «لغو شده» تغییر میدهد.",
|
||||
yesText: "لغو سفارش", cancelText: "انصراف",
|
||||
options: options);
|
||||
if (result == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DiscountOrderService.UpdateStatusAsync(model.Id, new UpdateOrderStatusDto
|
||||
{
|
||||
Status = OrderStatus.Cancelled,
|
||||
AdminNotes = "لغو شده توسط ادمین"
|
||||
});
|
||||
Snackbar.Add("سفارش با موفقیت لغو شد.", Severity.Success);
|
||||
await LoadOrders();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در لغو سفارش: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public async Task OnFilterSubmit()
|
||||
{
|
||||
_basePage.IsFiltered = true;
|
||||
StateHasChanged();
|
||||
await LoadOrders();
|
||||
}
|
||||
|
||||
public async Task OnFilterCleared()
|
||||
{
|
||||
_basePage.IsFiltered = false;
|
||||
StateHasChanged();
|
||||
_orderIdFilter = null;
|
||||
_userIdFilter = null;
|
||||
_userMobileFilter = null;
|
||||
_trackingCodeFilter = null;
|
||||
_fromDateFilter = null;
|
||||
_toDateFilter = null;
|
||||
_paymentStatusFilter = null;
|
||||
_statusFilter = null;
|
||||
await LoadOrders();
|
||||
}
|
||||
|
||||
private Color GetStatusColor(OrderStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
OrderStatus.Pending => Color.Warning,
|
||||
OrderStatus.Paid => Color.Info,
|
||||
OrderStatus.Processing => Color.Primary,
|
||||
OrderStatus.Shipped => Color.Secondary,
|
||||
OrderStatus.Delivered => Color.Success,
|
||||
OrderStatus.Cancelled => Color.Error,
|
||||
OrderStatus.Returned => Color.Dark,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private string GetStatusText(OrderStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
OrderStatus.Pending => "در انتظار ارسال",
|
||||
OrderStatus.Paid => "پرداخت شده",
|
||||
OrderStatus.Processing => "در حال آمادهسازی",
|
||||
OrderStatus.Shipped => "ارسال شده",
|
||||
OrderStatus.Delivered => "تحویل داده شده",
|
||||
OrderStatus.Cancelled => "لغو شده",
|
||||
OrderStatus.Returned => "مرجوع شده",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
}
|
||||
|
||||
private Color GetPaymentStatusColor(int status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
0 => Color.Warning, // Pending
|
||||
1 => Color.Success, // Completed
|
||||
2 => Color.Error, // Failed
|
||||
3 => Color.Info, // Refunded
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private string GetPaymentStatusText(int status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
0 => "در انتظار پرداخت",
|
||||
1 => "پرداخت شده",
|
||||
2 => "ناموفق",
|
||||
3 => "بازپرداخت شده",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task ExportToExcel()
|
||||
{
|
||||
if (_orders == null || !_orders.Any())
|
||||
{
|
||||
Snackbar.Add("دادهای برای خروجی وجود ندارد.", Severity.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("شناسه,نام کاربر,موبایل,مبلغ کل,تخفیف کیفپول,پرداخت درگاه,تعداد آیتم,وضعیت پرداخت,وضعیت ارسال,تاریخ پرداخت,تاریخ ثبت,آدرس");
|
||||
|
||||
foreach (var o in _orders)
|
||||
{
|
||||
sb.AppendLine(string.Join(",",
|
||||
o.Id,
|
||||
EscapeCsv(o.UserFullName),
|
||||
EscapeCsv(o.UserMobile),
|
||||
o.TotalPrice.ToString("N0"),
|
||||
o.DiscountBalanceUsed.ToString("N0"),
|
||||
o.GatewayAmount.ToString("N0"),
|
||||
o.ItemsCount,
|
||||
EscapeCsv(GetPaymentStatusText(o.PaymentStatusValue)),
|
||||
EscapeCsv(GetStatusText(o.Status)),
|
||||
o.PaymentDate?.MiladiToJalaliWithTime() ?? "-",
|
||||
o.Created?.MiladiToJalaliWithTime() ?? "-",
|
||||
EscapeCsv(o.ShippingAddress ?? "-")));
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
var filename = $"discount-orders-{DateTime.Now:yyyyMMddHHmmss}.csv";
|
||||
|
||||
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
|
||||
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private static string EscapeCsv(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return "";
|
||||
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
|
||||
return $"\"{value.Replace("\"", "\"\"")}\"";
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -1,336 +1,208 @@
|
||||
@page "/discount-products"
|
||||
@using BackOffice.Services.DiscountProduct
|
||||
@using BackOffice.Services.DiscountCategory
|
||||
@using BackOffice.Pages.DiscountShop.Components
|
||||
@using BackOffice.Common.BaseComponents
|
||||
@using static BackOffice.Pages.DiscountShop.Components.ProductFormDialog
|
||||
@inject IDiscountProductService DiscountProductService
|
||||
@inject IDialogService DialogService
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<PageTitle>مدیریت محصولات تخفیفی</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-4">
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Discount" Class="ml-2" />
|
||||
مدیریت محصولات تخفیفی
|
||||
</MudText>
|
||||
|
||||
<MudPaper Class="pa-4 mt-4">
|
||||
<MudGrid>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField @bind-Value="_searchQuery"
|
||||
Label="جستجو"
|
||||
Variant="Variant.Outlined"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||
OnKeyUp="@(() => OnSearch())" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect @bind-Value="_categoryFilter"
|
||||
Label="دستهبندی"
|
||||
Variant="Variant.Outlined"
|
||||
T="long?"
|
||||
Clearable="true"
|
||||
OnClearButtonClick="@(() => OnCategoryFilterChanged())">
|
||||
<MudSelectItem Value="@((long?)null)">همه</MudSelectItem>
|
||||
@* TODO: Load categories *@
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="2">
|
||||
<MudSelect @bind-Value="_statusFilter"
|
||||
Label="وضعیت"
|
||||
Variant="Variant.Outlined"
|
||||
T="bool?"
|
||||
Clearable="true">
|
||||
<MudSelectItem Value="@((bool?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem Value="@((bool?)true)">فعال</MudSelectItem>
|
||||
<MudSelectItem Value="@((bool?)false)">غیرفعال</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="2">
|
||||
<MudSelect @bind-Value="_stockFilter"
|
||||
Label="موجودی"
|
||||
Variant="Variant.Outlined"
|
||||
T="bool?"
|
||||
Clearable="true">
|
||||
<MudSelectItem Value="@((bool?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem Value="@((bool?)true)">موجود</MudSelectItem>
|
||||
<MudSelectItem Value="@((bool?)false)">ناموجود</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="12" md="2">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
FullWidth="true"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="@(() => OpenCreateDialog())">
|
||||
محصول جدید
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudDataGrid @ref="_dataGrid" T="DiscountProductDto"
|
||||
Items="@_products"
|
||||
<BasePageComponent @ref="_basePage" OnClearFilterClick="OnFilterCleared" OnSubmitClick="OnFilterSubmit">
|
||||
<Filters>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField T="string" Clearable="true" Label="جستجو"
|
||||
@bind-Value="@_searchQuery" Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect @bind-Value="_categoryFilter"
|
||||
Label="دستهبندی"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
T="long?"
|
||||
Clearable="true">
|
||||
<MudSelectItem T="long?" Value="@((long?)null)">همه</MudSelectItem>
|
||||
@foreach (var cat in _categories)
|
||||
{
|
||||
<MudSelectItem T="long?" Value="@((long?)cat.Id)">@cat.Title</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect @bind-Value="_statusFilter"
|
||||
Label="وضعیت"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
T="bool?"
|
||||
Clearable="true">
|
||||
<MudSelectItem T="bool?" Value="@((bool?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem T="bool?" Value="@((bool?)true)">فعال</MudSelectItem>
|
||||
<MudSelectItem T="bool?" Value="@((bool?)false)">غیرفعال</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect @bind-Value="_stockFilter"
|
||||
Label="موجودی"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
T="bool?"
|
||||
Clearable="true">
|
||||
<MudSelectItem T="bool?" Value="@((bool?)null)">همه</MudSelectItem>
|
||||
<MudSelectItem T="bool?" Value="@((bool?)true)">موجود</MudSelectItem>
|
||||
<MudSelectItem T="bool?" Value="@((bool?)false)">ناموجود</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
</Filters>
|
||||
<Content>
|
||||
<MudDataGrid @ref="_dataGrid" T="DiscountProductDto"
|
||||
ServerData="LoadServerData"
|
||||
Loading="@_loading"
|
||||
Hover="true"
|
||||
Dense="true"
|
||||
Class="mt-4">
|
||||
Hover="true"
|
||||
Height="calc(100vh - 240px)">
|
||||
<ColGroup>
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
<col style="width: 58px;" />
|
||||
</ColGroup>
|
||||
<ToolBarContent>
|
||||
<MudText>مدیریت محصولات</MudText>
|
||||
<MudSpacer />
|
||||
<MudStack Spacing="2" Row="true" Justify="Justify.Center" AlignItems="AlignItems.Center">
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Secondary"
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
StartIcon="@Icons.Material.Filled.Download"
|
||||
OnClick="ExportToExcel">
|
||||
خروجی Excel
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Large"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => OpenCreateDialog())">
|
||||
افزودن
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</ToolBarContent>
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
|
||||
<TemplateColumn Title="تصویر">
|
||||
|
||||
<PropertyColumn Property="x => x.Title" Title="عنوان" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
|
||||
<CellTemplate>
|
||||
@if (!string.IsNullOrEmpty(context.Item.ThumbnailPath))
|
||||
{
|
||||
<MudAvatar Image="@context.Item.ThumbnailPath" Size="Size.Medium" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudAvatar Color="Color.Secondary" Size="Size.Medium">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Image" />
|
||||
</MudAvatar>
|
||||
}
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
<MudTooltip Text="پیشنمایش تصویر">
|
||||
<div @onclick="@(() => OpenImagePreview(context.Item.ImagePath, context.Item.Title))">
|
||||
<Image Src="@context.Item.ImagePath"
|
||||
Width="25"
|
||||
Height="25"
|
||||
ObjectPosition="ObjectPosition.Center"
|
||||
ObjectFit="ObjectFit.Fill" />
|
||||
</div>
|
||||
</MudTooltip>
|
||||
@if (string.IsNullOrWhiteSpace(context.Item.Title))
|
||||
{
|
||||
<MudText Typo="Typo.inherit">-</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTooltip Text="@(context.Item.Title)" Arrow="true">
|
||||
<MudText Typo="Typo.inherit" Style="text-wrap: nowrap;">
|
||||
@(context.Item.Title.Truncate(20, true))
|
||||
</MudText>
|
||||
</MudTooltip>
|
||||
}
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.Title" Title="عنوان" />
|
||||
|
||||
<TemplateColumn Title="قیمت">
|
||||
</PropertyColumn>
|
||||
|
||||
<TemplateColumn Title="قیمت" CellStyle="text-wrap: nowrap;">
|
||||
<CellTemplate>
|
||||
<MudText>@context.Item.Price.ToString("N0") ریال</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="حداکثر تخفیف">
|
||||
|
||||
<TemplateColumn Title="حداکثر تخفیف" CellStyle="text-wrap: nowrap;">
|
||||
<CellTemplate>
|
||||
<MudChip T="string" Color="Color.Success" Size="Size.Small">@context.Item.MaxDiscountPercent%</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="موجودی">
|
||||
|
||||
<TemplateColumn Title="موجودی" CellStyle="text-wrap: nowrap;">
|
||||
<CellTemplate>
|
||||
<MudChip T="string" Color="@(context.Item.RemainingCount > 0 ? Color.Info : Color.Error)" Size="Size.Small">
|
||||
@context.Item.RemainingCount
|
||||
</MudChip>
|
||||
@if (context.Item.RemainingCount <= 0)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Error" Size="Size.Small">ناموجود</MudChip>
|
||||
}
|
||||
else if (context.Item.RemainingCount < 10)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Warning" Size="Size.Small">@context.Item.RemainingCount</MudChip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChip T="string" Color="Color.Success" Size="Size.Small">@context.Item.RemainingCount</MudChip>
|
||||
}
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="بازدید">
|
||||
<CellTemplate>
|
||||
<MudText>@context.Item.ViewCount</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
|
||||
<TemplateColumn Title="وضعیت">
|
||||
<CellTemplate>
|
||||
<MudChip T="string" Color="@(context.Item.IsActive ? Color.Success : Color.Default)"
|
||||
<MudChip T="string"
|
||||
Color="@(context.Item.IsActive ? Color.Success : Color.Error)"
|
||||
Variant="Variant.Filled"
|
||||
Size="Size.Small">
|
||||
@(context.Item.IsActive ? "فعال" : "غیرفعال")
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="عملیات" Sortable="false">
|
||||
|
||||
<TemplateColumn StickyLeft="true" Title="عملیات" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
|
||||
<CellTemplate>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
OnClick="@(() => OpenEditDialog(context.Item.Id))" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Color="Color.Error"
|
||||
Size="Size.Small"
|
||||
OnClick="@(() => DeleteProduct(context.Item.Id))" />
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
<MudTooltip Text="ویرایش">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.EditNote"
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => OpenEditDialog(context.Item.Id))" />
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="حذف">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.DeleteOutline"
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => DeleteProduct(context.Item.Id))" />
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="گالری تصاویر">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Collections"
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => OpenGallery(context.Item))" />
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="مدیریت دستهبندی (درگ و دراپ)">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Category"
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => OpenCategoryMapping(context.Item))" />
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="تگهای محصول">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Label"
|
||||
Size="Size.Small"
|
||||
ButtonType="ButtonType.Button"
|
||||
OnClick="@(() => OpenTagAssignment(context.Item))" />
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="DiscountProductDto" />
|
||||
<MudDataGridPager T="DiscountProductDto" PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudPaper>
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
private MudDataGrid<DiscountProductDto>? _dataGrid;
|
||||
private List<DiscountProductDto> _products = new();
|
||||
private bool _loading = false;
|
||||
private int _totalCount;
|
||||
private int _totalPages;
|
||||
|
||||
private string? _searchQuery;
|
||||
private long? _categoryFilter;
|
||||
private bool? _statusFilter;
|
||||
private bool? _stockFilter;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadProducts();
|
||||
}
|
||||
|
||||
private async Task LoadProducts()
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var filter = new ProductFilterDto
|
||||
{
|
||||
SearchQuery = _searchQuery,
|
||||
CategoryId = _categoryFilter,
|
||||
IsActive = _statusFilter,
|
||||
InStock = _stockFilter
|
||||
};
|
||||
|
||||
var (products, totalCount, totalPages) = await DiscountProductService.GetProductsAsync(filter);
|
||||
_products = products;
|
||||
_totalCount = totalCount;
|
||||
_totalPages = totalPages;
|
||||
Snackbar.Add("محصولات بارگذاری شدند", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری محصولات: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnSearch()
|
||||
{
|
||||
await LoadProducts();
|
||||
}
|
||||
|
||||
private async Task OnCategoryFilterChanged()
|
||||
{
|
||||
await LoadProducts();
|
||||
}
|
||||
|
||||
private async Task OpenCreateDialog()
|
||||
{
|
||||
var model = new ProductFormModel();
|
||||
var parameters = new DialogParameters<ProductFormDialog>
|
||||
{
|
||||
{ x => x.Model, model },
|
||||
{ x => x.IsEditMode, false }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<ProductFormDialog>("ایجاد محصول جدید", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false, Data: ProductFormModel formData })
|
||||
{
|
||||
try
|
||||
{
|
||||
var dto = new CreateDiscountProductDto
|
||||
{
|
||||
Title = formData.Title,
|
||||
ShortInformation = formData.ShortInformation,
|
||||
FullInformation = formData.FullInformation,
|
||||
ImagePath = formData.ImagePath,
|
||||
ThumbnailPath = formData.ThumbnailPath,
|
||||
Price = formData.Price,
|
||||
MaxDiscountPercent = formData.MaxDiscountPercent,
|
||||
InitialCount = formData.InitialCount,
|
||||
SortOrder = formData.SortOrder,
|
||||
IsActive = formData.IsActive,
|
||||
CategoryIds = formData.CategoryIds
|
||||
};
|
||||
|
||||
await DiscountProductService.CreateAsync(dto);
|
||||
Snackbar.Add("محصول با موفقیت ایجاد شد", Severity.Success);
|
||||
await LoadProducts();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ایجاد محصول: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenEditDialog(long productId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var product = await DiscountProductService.GetByIdAsync(productId);
|
||||
if (product == null)
|
||||
{
|
||||
Snackbar.Add("محصول یافت نشد", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var model = new ProductFormModel
|
||||
{
|
||||
Title = product.Title,
|
||||
ShortInformation = product.ShortInformation,
|
||||
FullInformation = product.FullInformation,
|
||||
ImagePath = product.ImagePath,
|
||||
ThumbnailPath = product.ThumbnailPath,
|
||||
Price = product.Price,
|
||||
MaxDiscountPercent = product.MaxDiscountPercent,
|
||||
InitialCount = product.RemainingCount,
|
||||
SortOrder = product.SortOrder,
|
||||
IsActive = product.IsActive,
|
||||
CategoryIds = product.Categories?.Select(c => c.Id).ToList() ?? new()
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters<ProductFormDialog>
|
||||
{
|
||||
{ x => x.Model, model },
|
||||
{ x => x.IsEditMode, true }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<ProductFormDialog>("ویرایش محصول", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false, Data: ProductFormModel formData })
|
||||
{
|
||||
var dto = new UpdateDiscountProductDto
|
||||
{
|
||||
Title = formData.Title,
|
||||
ShortInformation = formData.ShortInformation,
|
||||
FullInformation = formData.FullInformation,
|
||||
ImagePath = formData.ImagePath,
|
||||
ThumbnailPath = formData.ThumbnailPath,
|
||||
Price = formData.Price,
|
||||
MaxDiscountPercent = formData.MaxDiscountPercent,
|
||||
SortOrder = formData.SortOrder,
|
||||
IsActive = formData.IsActive,
|
||||
CategoryIds = formData.CategoryIds
|
||||
};
|
||||
|
||||
await DiscountProductService.UpdateAsync(productId, dto);
|
||||
Snackbar.Add("محصول با موفقیت ویرایش شد", Severity.Success);
|
||||
await LoadProducts();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ویرایش محصول: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteProduct(long productId)
|
||||
{
|
||||
var result = await DialogService.ShowMessageBox(
|
||||
"تأیید حذف",
|
||||
"آیا از حذف این محصول اطمینان دارید؟",
|
||||
yesText: "بله", cancelText: "خیر");
|
||||
|
||||
if (result == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DiscountProductService.DeleteAsync(productId);
|
||||
Snackbar.Add("محصول با موفقیت حذف شد", Severity.Success);
|
||||
await LoadProducts();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در حذف محصول: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</Content>
|
||||
</BasePageComponent>
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
using System.Text;
|
||||
using BackOffice.Common.BaseComponents;
|
||||
using BackOffice.Common.Utilities;
|
||||
using BackOffice.Services.DiscountProduct;
|
||||
using BackOffice.Services.DiscountCategory;
|
||||
using BackOffice.Pages.DiscountShop.Components;
|
||||
using BackOffice.Pages.Products.Components;
|
||||
using BackOffice.Pages.Tag.Components;
|
||||
using static BackOffice.Pages.DiscountShop.Components.ProductFormDialog;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using MudBlazor;
|
||||
|
||||
namespace BackOffice.Pages.DiscountShop;
|
||||
|
||||
public partial class DiscountProductsMainPage
|
||||
{
|
||||
[Inject] public IDiscountProductService DiscountProductService { get; set; } = default!;
|
||||
[Inject] public IDiscountCategoryService DiscountCategoryService { get; set; } = default!;
|
||||
|
||||
private MudDataGrid<DiscountProductDto>? _dataGrid;
|
||||
private BasePageComponent _basePage = default!;
|
||||
private List<DiscountProductDto> _products = new();
|
||||
private List<DiscountCategoryDto> _categories = new();
|
||||
private bool _loading;
|
||||
|
||||
private string? _searchQuery;
|
||||
private long? _categoryFilter;
|
||||
private bool? _statusFilter;
|
||||
private bool? _stockFilter;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadCategories();
|
||||
}
|
||||
|
||||
private async Task LoadCategories()
|
||||
{
|
||||
try
|
||||
{
|
||||
_categories = await DiscountCategoryService.GetCategoriesAsync(isActive: true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_categories = new();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<GridData<DiscountProductDto>> LoadServerData(GridState<DiscountProductDto> state)
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var filter = new ProductFilterDto
|
||||
{
|
||||
SearchQuery = _searchQuery,
|
||||
CategoryId = _categoryFilter,
|
||||
IsActive = _statusFilter,
|
||||
InStock = _stockFilter,
|
||||
PageNumber = state.Page + 1,
|
||||
PageSize = state.PageSize
|
||||
};
|
||||
|
||||
var (products, totalCount, _) = await DiscountProductService.GetProductsAsync(filter);
|
||||
_products = products;
|
||||
|
||||
return new GridData<DiscountProductDto>
|
||||
{
|
||||
Items = products,
|
||||
TotalItems = totalCount
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری محصولات: {ex.Message}", Severity.Error);
|
||||
return new GridData<DiscountProductDto> { Items = new List<DiscountProductDto>(), TotalItems = 0 };
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ReLoadData()
|
||||
{
|
||||
_dataGrid?.ReloadServerData();
|
||||
}
|
||||
|
||||
private async Task OnFilterSubmit()
|
||||
{
|
||||
_basePage.IsFiltered = true;
|
||||
StateHasChanged();
|
||||
ReLoadData();
|
||||
}
|
||||
|
||||
private async Task OnFilterCleared()
|
||||
{
|
||||
_basePage.IsFiltered = false;
|
||||
_searchQuery = null;
|
||||
_categoryFilter = null;
|
||||
_statusFilter = null;
|
||||
_stockFilter = null;
|
||||
StateHasChanged();
|
||||
ReLoadData();
|
||||
}
|
||||
|
||||
private async Task OpenCreateDialog()
|
||||
{
|
||||
var model = new ProductFormModel();
|
||||
var parameters = new DialogParameters<ProductFormDialog>
|
||||
{
|
||||
{ x => x.Model, model },
|
||||
{ x => x.IsEditMode, false }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<ProductFormDialog>("ایجاد محصول جدید", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false, Data: ProductFormModel formData })
|
||||
{
|
||||
try
|
||||
{
|
||||
var dto = new CreateDiscountProductDto
|
||||
{
|
||||
Title = formData.Title,
|
||||
ShortInformation = formData.ShortInformation,
|
||||
FullInformation = formData.FullInformation,
|
||||
ImagePath = formData.ImagePath,
|
||||
ThumbnailPath = formData.ThumbnailPath,
|
||||
Price = formData.Price,
|
||||
MaxDiscountPercent = formData.MaxDiscountPercent,
|
||||
InitialCount = 0,
|
||||
SortOrder = formData.SortOrder,
|
||||
IsActive = formData.IsActive,
|
||||
CategoryIds = formData.CategoryIds,
|
||||
ImageFile = formData.ImageFile,
|
||||
ImageMime = formData.ImageMime,
|
||||
ImageFileName = formData.ImageFileName,
|
||||
ThumbnailFile = formData.ThumbnailFile,
|
||||
ThumbnailMime = formData.ThumbnailMime,
|
||||
ThumbnailFileName = formData.ThumbnailFileName
|
||||
};
|
||||
|
||||
await DiscountProductService.CreateAsync(dto);
|
||||
Snackbar.Add("محصول با موفقیت ایجاد شد", Severity.Success);
|
||||
ReLoadData();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ایجاد محصول: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenEditDialog(long productId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var product = await DiscountProductService.GetByIdAsync(productId);
|
||||
if (product == null)
|
||||
{
|
||||
Snackbar.Add("محصول یافت نشد", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var model = new ProductFormModel
|
||||
{
|
||||
Title = product.Title,
|
||||
ShortInformation = product.ShortInformation,
|
||||
FullInformation = product.FullInformation,
|
||||
ImagePath = product.ImagePath,
|
||||
ThumbnailPath = product.ThumbnailPath,
|
||||
Price = product.Price,
|
||||
MaxDiscountPercent = product.MaxDiscountPercent,
|
||||
SortOrder = product.SortOrder,
|
||||
IsActive = product.IsActive,
|
||||
CategoryIds = product.Categories?.Select(c => c.Id).ToList() ?? new()
|
||||
};
|
||||
|
||||
var parameters = new DialogParameters<ProductFormDialog>
|
||||
{
|
||||
{ x => x.Model, model },
|
||||
{ x => x.IsEditMode, true }
|
||||
};
|
||||
|
||||
var options = new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true };
|
||||
var dialog = await DialogService.ShowAsync<ProductFormDialog>("ویرایش محصول", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (result is { Canceled: false, Data: ProductFormModel formData })
|
||||
{
|
||||
var dto = new UpdateDiscountProductDto
|
||||
{
|
||||
Title = formData.Title,
|
||||
ShortInformation = formData.ShortInformation,
|
||||
FullInformation = formData.FullInformation,
|
||||
ImagePath = formData.ImagePath,
|
||||
ThumbnailPath = formData.ThumbnailPath,
|
||||
Price = formData.Price,
|
||||
MaxDiscountPercent = formData.MaxDiscountPercent,
|
||||
SortOrder = formData.SortOrder,
|
||||
IsActive = formData.IsActive,
|
||||
CategoryIds = formData.CategoryIds,
|
||||
ImageFile = formData.ImageFile,
|
||||
ImageMime = formData.ImageMime,
|
||||
ImageFileName = formData.ImageFileName,
|
||||
ThumbnailFile = formData.ThumbnailFile,
|
||||
ThumbnailMime = formData.ThumbnailMime,
|
||||
ThumbnailFileName = formData.ThumbnailFileName
|
||||
};
|
||||
|
||||
await DiscountProductService.UpdateAsync(productId, dto);
|
||||
Snackbar.Add("محصول با موفقیت ویرایش شد", Severity.Success);
|
||||
ReLoadData();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در ویرایش محصول: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteProduct(long productId)
|
||||
{
|
||||
var product = _products.FirstOrDefault(p => p.Id == productId);
|
||||
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small };
|
||||
bool? result = await DialogService.ShowMessageBox(
|
||||
"اخطار",
|
||||
$"آیا از حذف محصول «{product?.Title}» مطمئن هستید؟",
|
||||
yesText: "حذف", cancelText: "لغو",
|
||||
options: options);
|
||||
|
||||
if (result == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DiscountProductService.DeleteAsync(productId);
|
||||
Snackbar.Add("محصول با موفقیت حذف شد", Severity.Success);
|
||||
ReLoadData();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در حذف محصول: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenGallery(DiscountProductDto product)
|
||||
{
|
||||
var parameters = new DialogParameters<GalleryDialog>
|
||||
{
|
||||
{ x => x.ProductId, product.Id },
|
||||
{ x => x.ProductTitle, product.Title }
|
||||
};
|
||||
await DialogService.ShowAsync<GalleryDialog>("گالری تصاویر", parameters,
|
||||
new DialogOptions { CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.Medium });
|
||||
}
|
||||
|
||||
public void OpenCategoryMapping(DiscountProductDto product)
|
||||
{
|
||||
Navigation.NavigateTo($"{RouteConstance.ProductCategories}{product.Id}");
|
||||
}
|
||||
|
||||
private async Task OpenImagePreview(string? imagePath, string? title)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(imagePath))
|
||||
return;
|
||||
|
||||
var parameters = new DialogParameters<ImagePreviewDialog>
|
||||
{
|
||||
{ x => x.ImageUrl, imagePath },
|
||||
{ x => x.Title, title }
|
||||
};
|
||||
|
||||
await DialogService.ShowAsync<ImagePreviewDialog>("پیشنمایش تصویر محصول", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true });
|
||||
}
|
||||
|
||||
private async Task ExportToExcel()
|
||||
{
|
||||
try
|
||||
{
|
||||
var filter = new ProductFilterDto
|
||||
{
|
||||
SearchQuery = _searchQuery,
|
||||
CategoryId = _categoryFilter,
|
||||
IsActive = _statusFilter,
|
||||
InStock = _stockFilter,
|
||||
PageNumber = 1,
|
||||
PageSize = 1000
|
||||
};
|
||||
|
||||
var (products, _, _) = await DiscountProductService.GetProductsAsync(filter);
|
||||
|
||||
if (products == null || products.Count == 0)
|
||||
{
|
||||
Snackbar.Add("دادهای برای خروجی وجود ندارد.", Severity.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("شناسه,عنوان,قیمت,حداکثر تخفیف,موجودی,بازدید,وضعیت");
|
||||
|
||||
foreach (var p in products)
|
||||
{
|
||||
sb.AppendLine(string.Join(",",
|
||||
p.Id,
|
||||
EscapeCsv(p.Title),
|
||||
p.Price,
|
||||
p.MaxDiscountPercent,
|
||||
p.RemainingCount,
|
||||
p.ViewCount,
|
||||
p.IsActive ? "فعال" : "غیرفعال"));
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
var filename = $"discount-products-{DateTime.Now:yyyyMMddHHmmss}.csv";
|
||||
|
||||
await jsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
|
||||
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task OpenTagAssignment(DiscountProductDto model)
|
||||
{
|
||||
var parameters = new DialogParameters<AssignTagsDialog>
|
||||
{
|
||||
{ x => x.ProductId, model.Id },
|
||||
{ x => x.ProductTitle, model.Title }
|
||||
};
|
||||
|
||||
await DialogService.ShowAsync<AssignTagsDialog>("مدیریت تگهای محصول", parameters,
|
||||
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true });
|
||||
}
|
||||
|
||||
private static string EscapeCsv(string? value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return "";
|
||||
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
|
||||
return "\"" + value.Replace("\"", "\"\"") + "\"";
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
@page "/discount-shop"
|
||||
@page "/discount-orders"
|
||||
@page "/discount-sales-reports"
|
||||
@attribute [Authorize(Roles = "Administrator")]
|
||||
|
||||
<div>
|
||||
<MudText Typo="Typo.h5" Class="mb-3">سفارشات فروشگاه تخفیفی</MudText>
|
||||
|
||||
<MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true"
|
||||
PanelClass="pt-4" @bind-ActivePanelIndex="_activeTab">
|
||||
<MudTabPanel Text="سفارشات" Icon="@Icons.Material.Filled.ReceiptLong">
|
||||
<DiscountOrdersMainPage />
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="گزارش فروش" Icon="@Icons.Material.Filled.BarChart">
|
||||
<SalesReports />
|
||||
</MudTabPanel>
|
||||
</MudTabs>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private int _activeTab;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
var uri = Navigation.ToBaseRelativePath(Navigation.Uri).ToLowerInvariant().TrimEnd('/');
|
||||
_activeTab = uri switch
|
||||
{
|
||||
"discount-sales-reports" => 1,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
@page "/discount-sales-reports"
|
||||
@* Merged into /discount-shop *@
|
||||
@attribute [Authorize(Roles = "Administrator")]
|
||||
|
||||
@using MudBlazor
|
||||
@@ -10,13 +10,13 @@
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IJSRuntime JsRuntime
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-4">
|
||||
<div>
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">
|
||||
<MudIcon Icon="@Icons.Material.Filled.BarChart" Class="ml-2" />
|
||||
گزارش فروش فروشگاه تخفیفی
|
||||
گزارش فروش فروشگاه
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary" Class="mb-4">
|
||||
آمار فروش، تخفیف و وضعیت سفارشهای فروشگاه تخفیفی بر اساس بازه تاریخ و وضعیت سفارش
|
||||
آمار فروش و وضعیت سفارشها بر اساس بازه تاریخ و وضعیت سفارش
|
||||
</MudText>
|
||||
|
||||
<MudPaper Class="pa-4 mb-4">
|
||||
@@ -42,14 +42,14 @@
|
||||
Clearable="true"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense">
|
||||
<MudSelectItem Value="@(null as OrderStatus?)">همه وضعیتها</MudSelectItem>
|
||||
<MudSelectItem Value="OrderStatus.Pending">در انتظار</MudSelectItem>
|
||||
<MudSelectItem Value="OrderStatus.Paid">پرداخت شده</MudSelectItem>
|
||||
<MudSelectItem Value="OrderStatus.Processing">در حال پردازش</MudSelectItem>
|
||||
<MudSelectItem Value="OrderStatus.Shipped">ارسال شده</MudSelectItem>
|
||||
<MudSelectItem Value="OrderStatus.Delivered">تحویل شده</MudSelectItem>
|
||||
<MudSelectItem Value="OrderStatus.Cancelled">لغو شده</MudSelectItem>
|
||||
<MudSelectItem Value="OrderStatus.Returned">مرجوع شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="@(null as OrderStatus?)">همه وضعیتها</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Pending">در انتظار</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Paid">پرداخت شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Processing">در حال پردازش</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Shipped">ارسال شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Delivered">تحویل شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Cancelled">لغو شده</MudSelectItem>
|
||||
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Returned">مرجوع شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
@@ -137,14 +137,49 @@
|
||||
<MudText Typo="Typo.h6">روند فروش</MudText>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudChart ChartType="ChartType.Line"
|
||||
ChartSeries="@_salesSeries"
|
||||
XAxisLabels="@_salesLabels"
|
||||
Width="100%"
|
||||
Height="300px" />
|
||||
@if (_salesLabels.Length == 0)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">
|
||||
داده کافی برای نمایش نمودار وجود ندارد.
|
||||
</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChart ChartType="ChartType.Line"
|
||||
ChartSeries="@_salesSeries"
|
||||
XAxisLabels="@_salesLabels"
|
||||
Width="100%"
|
||||
Height="300px" />
|
||||
}
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
<MudCard>
|
||||
<MudCardHeader>
|
||||
<MudText Typo="Typo.h6">وضعیت سفارشها</MudText>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
@if (_statusChartLabels.Length == 0)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">
|
||||
داده کافی برای نمایش نمودار وجود ندارد.
|
||||
</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChart ChartType="ChartType.Bar"
|
||||
ChartSeries="@_statusChartSeries"
|
||||
XAxisLabels="@_statusChartLabels"
|
||||
Width="100%"
|
||||
Height="300px" />
|
||||
}
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudGrid Class="mb-4">
|
||||
<MudItem xs="12" md="6">
|
||||
<MudCard>
|
||||
<MudCardHeader>
|
||||
@@ -180,10 +215,24 @@
|
||||
Hover="true"
|
||||
Dense="true">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.OrderNumber" Title="شماره سفارش" />
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
<TemplateColumn Title="نام کاربر">
|
||||
<CellTemplate>
|
||||
@if (context.Item.UserId > 0)
|
||||
{
|
||||
<MudLink Href="@($"/network/user-info/{context.Item.UserId}")" Underline="Underline.Hover" Color="Color.Primary">
|
||||
@context.Item.UserFullName
|
||||
</MudLink>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText>@context.Item.UserFullName</MudText>
|
||||
}
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="تاریخ ثبت">
|
||||
<CellTemplate>
|
||||
@(context.Item.Created?.ToString("yyyy/MM/dd HH:mm") ?? "-")
|
||||
@(context.Item.Created?.MiladiToJalaliWithTime() ?? "-")
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="تعداد آیتم">
|
||||
@@ -211,11 +260,14 @@
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
</MudDataGrid>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
}
|
||||
</MudContainer>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private readonly List<DiscountOrderDto> _orders = new();
|
||||
@@ -230,6 +282,8 @@
|
||||
private long _totalSales;
|
||||
private long _totalDiscount;
|
||||
private long _averageOrder;
|
||||
private int _uniqueCustomers;
|
||||
private int _totalProductsSold;
|
||||
|
||||
private string[] _salesLabels = Array.Empty<string>();
|
||||
private List<ChartSeries> _salesSeries = new();
|
||||
@@ -237,6 +291,9 @@
|
||||
private string[] _topProductLabels = Array.Empty<string>();
|
||||
private List<ChartSeries> _topProductSeries = new();
|
||||
|
||||
private string[] _statusChartLabels = Array.Empty<string>();
|
||||
private List<ChartSeries> _statusChartSeries = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadReports();
|
||||
@@ -255,32 +312,82 @@
|
||||
|
||||
try
|
||||
{
|
||||
// ۱) گزارش سرور-ساید (خلاصه + نمودار روزانه + محصولات پرفروش)
|
||||
DiscountSalesReportDto? report = null;
|
||||
if (_fromDate.HasValue && _toDate.HasValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
report = await DiscountOrderService.GetSalesReportAsync(_fromDate.Value, _toDate.Value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// اگر RPC سرور ساید پیادهسازی نشده بود، fallback به کلاینتساید
|
||||
}
|
||||
}
|
||||
|
||||
// ۲) لیست سفارشات برای جدول و نمودار وضعیت
|
||||
var filter = new OrderFilterDto
|
||||
{
|
||||
Status = _statusFilter,
|
||||
FromDate = _fromDate,
|
||||
ToDate = _toDate,
|
||||
PageNumber = 1,
|
||||
PageSize = 200
|
||||
};
|
||||
|
||||
var (orders, _, _) = await DiscountOrderService.GetOrdersAsync(filter);
|
||||
_orders.AddRange(orders);
|
||||
|
||||
BuildSummary();
|
||||
BuildSalesChart();
|
||||
await BuildTopProductsChart();
|
||||
if (report?.Summary != null && report.Summary.TotalOrders > 0)
|
||||
{
|
||||
// استفاده از گزارش سرور-ساید
|
||||
_totalOrders = report.Summary.TotalOrders;
|
||||
_totalSales = report.Summary.TotalGatewayPaid;
|
||||
_totalDiscount = report.Summary.TotalDiscountUsed;
|
||||
_averageOrder = report.Summary.AverageOrderValue;
|
||||
_uniqueCustomers = report.Summary.UniqueCustomers;
|
||||
_totalProductsSold = report.Summary.TotalProductsSold;
|
||||
|
||||
// نمودار روند فروش از periods
|
||||
if (report.Periods.Count > 0)
|
||||
{
|
||||
_salesLabels = report.Periods.Select(p => p.PeriodLabel).ToArray();
|
||||
_salesSeries = new List<ChartSeries>
|
||||
{
|
||||
new ChartSeries { Name = "مبلغ نهایی", Data = report.Periods.Select(p => (double)p.GatewayPaid).ToArray() },
|
||||
new ChartSeries { Name = "تخفیف", Data = report.Periods.Select(p => (double)p.DiscountUsed).ToArray() }
|
||||
};
|
||||
}
|
||||
|
||||
// نمودار محصولات پرفروش
|
||||
if (report.TopProducts.Count > 0)
|
||||
{
|
||||
_topProductLabels = report.TopProducts.Take(5).Select(x => x.ProductTitle).ToArray();
|
||||
_topProductSeries = new List<ChartSeries>
|
||||
{
|
||||
new ChartSeries
|
||||
{
|
||||
Name = "مبلغ فروش",
|
||||
Data = report.TopProducts.Take(5).Select(x => (double)x.TotalRevenue).ToArray()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: محاسبه کلاینتساید
|
||||
BuildSummary();
|
||||
BuildSalesChart();
|
||||
BuildTopProductsChartFromOrders();
|
||||
}
|
||||
|
||||
// نمودار وضعیت همیشه از لیست سفارشات
|
||||
BuildStatusChart();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری گزارش فروش: {ex.Message}", Severity.Error);
|
||||
_orders.Clear();
|
||||
_totalOrders = 0;
|
||||
_totalSales = 0;
|
||||
_totalDiscount = 0;
|
||||
_averageOrder = 0;
|
||||
_salesLabels = Array.Empty<string>();
|
||||
_salesSeries = new List<ChartSeries>();
|
||||
_topProductLabels = Array.Empty<string>();
|
||||
_topProductSeries = new List<ChartSeries>();
|
||||
ClearData();
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -288,6 +395,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearData()
|
||||
{
|
||||
_orders.Clear();
|
||||
_totalOrders = 0;
|
||||
_totalSales = 0;
|
||||
_totalDiscount = 0;
|
||||
_averageOrder = 0;
|
||||
_uniqueCustomers = 0;
|
||||
_totalProductsSold = 0;
|
||||
_salesLabels = Array.Empty<string>();
|
||||
_salesSeries = new List<ChartSeries>();
|
||||
_topProductLabels = Array.Empty<string>();
|
||||
_topProductSeries = new List<ChartSeries>();
|
||||
_statusChartLabels = Array.Empty<string>();
|
||||
_statusChartSeries = new List<ChartSeries>();
|
||||
}
|
||||
|
||||
private void BuildSummary()
|
||||
{
|
||||
_totalOrders = _orders.Count;
|
||||
@@ -312,7 +436,7 @@
|
||||
.ToList();
|
||||
|
||||
_salesLabels = grouped
|
||||
.Select(g => g.Key.ToString("yyyy/MM/dd"))
|
||||
.Select(g => g.Key.MiladiToJalali())
|
||||
.ToArray();
|
||||
|
||||
var totalSeries = grouped
|
||||
@@ -330,73 +454,41 @@
|
||||
};
|
||||
}
|
||||
|
||||
private async Task BuildTopProductsChart()
|
||||
private void BuildStatusChart()
|
||||
{
|
||||
_topProductLabels = Array.Empty<string>();
|
||||
_topProductSeries = new List<ChartSeries>();
|
||||
|
||||
if (_orders.Count == 0)
|
||||
return;
|
||||
|
||||
// برای جلوگیری از فشار بیشازحد، فقط روی 50 سفارش اول کار میکنیم
|
||||
var sampleOrders = _orders
|
||||
.Where(o => o.Created.HasValue)
|
||||
.OrderByDescending(o => o.Created)
|
||||
.Take(50)
|
||||
.ToList();
|
||||
|
||||
var productTotals = new Dictionary<long, (string Name, long Amount)>();
|
||||
|
||||
foreach (var order in sampleOrders)
|
||||
{
|
||||
DiscountOrderDetailsDto? details;
|
||||
try
|
||||
{
|
||||
details = await DiscountOrderService.GetByIdAsync(order.Id);
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (details?.Items == null)
|
||||
continue;
|
||||
|
||||
foreach (var item in details.Items)
|
||||
{
|
||||
if (!productTotals.TryGetValue(item.ProductId, out var existing))
|
||||
{
|
||||
productTotals[item.ProductId] = (item.ProductTitle, item.TotalPrice);
|
||||
}
|
||||
else
|
||||
{
|
||||
productTotals[item.ProductId] = (existing.Name, existing.Amount + item.TotalPrice);
|
||||
}
|
||||
}
|
||||
_statusChartLabels = Array.Empty<string>();
|
||||
_statusChartSeries = new List<ChartSeries>();
|
||||
return;
|
||||
}
|
||||
|
||||
if (productTotals.Count == 0)
|
||||
return;
|
||||
|
||||
var top = productTotals.Values
|
||||
.OrderByDescending(x => x.Amount)
|
||||
.Take(5)
|
||||
var groups = _orders
|
||||
.GroupBy(x => x.Status)
|
||||
.OrderBy(g => (int)g.Key)
|
||||
.ToList();
|
||||
|
||||
_topProductLabels = top
|
||||
.Select(x => x.Name)
|
||||
_statusChartLabels = groups
|
||||
.Select(g => GetStatusText(g.Key))
|
||||
.ToArray();
|
||||
|
||||
_topProductSeries = new List<ChartSeries>
|
||||
_statusChartSeries = new List<ChartSeries>
|
||||
{
|
||||
new ChartSeries
|
||||
{
|
||||
Name = "مبلغ فروش",
|
||||
Data = top.Select(x => (double)x.Amount).ToArray()
|
||||
Name = "تعداد سفارشها",
|
||||
Data = groups.Select(g => (double)g.Count()).ToArray()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void BuildTopProductsChartFromOrders()
|
||||
{
|
||||
// Fallback ساده بدون N+1 — فقط از اطلاعات لیست سفارشات
|
||||
_topProductLabels = Array.Empty<string>();
|
||||
_topProductSeries = new List<ChartSeries>();
|
||||
}
|
||||
|
||||
private Color GetStatusColor(OrderStatus status)
|
||||
{
|
||||
return status switch
|
||||
@@ -436,20 +528,22 @@
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("OrderNumber,Created,ItemsCount,GatewayAmount,DiscountBalanceUsed,Status");
|
||||
sb.AppendLine("شناسه,نام کاربر,موبایل,تاریخ ثبت,تعداد آیتم,مبلغ نهایی,تخفیف,وضعیت");
|
||||
|
||||
foreach (var o in _orders.Where(o => o.Created.HasValue).OrderBy(o => o.Created))
|
||||
{
|
||||
sb.AppendLine(string.Join(",",
|
||||
EscapeCsv(o.OrderNumber),
|
||||
o.Created?.ToString("yyyy-MM-dd HH:mm") ?? "-",
|
||||
o.Id,
|
||||
EscapeCsv(o.UserFullName),
|
||||
EscapeCsv(o.UserMobile),
|
||||
o.Created?.MiladiToJalaliWithTime() ?? "-",
|
||||
o.ItemsCount,
|
||||
o.GatewayAmount,
|
||||
o.DiscountBalanceUsed,
|
||||
GetStatusText(o.Status)));
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(sb.ToString());
|
||||
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
var filename = $"discount-sales-{DateTime.Now:yyyyMMddHHmmss}.csv";
|
||||
|
||||
@@ -467,7 +561,7 @@
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("گزارش فروش فروشگاه تخفیفی");
|
||||
sb.AppendLine($"بازه: از {_fromDate:yyyy/MM/dd} تا {_toDate:yyyy/MM/dd}");
|
||||
sb.AppendLine($"بازه: از {_fromDate?.MiladiToJalali() ?? "-"} تا {_toDate?.MiladiToJalali() ?? "-"}");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("خلاصه:");
|
||||
@@ -478,15 +572,15 @@
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("جزئیات سفارشها:");
|
||||
sb.AppendLine("شماره سفارش | تاریخ | تعداد آیتم | مبلغ نهایی | تخفیف | وضعیت");
|
||||
sb.AppendLine("شناسه | نام کاربر | تاریخ | تعداد آیتم | مبلغ نهایی | تخفیف | وضعیت");
|
||||
|
||||
foreach (var o in _orders.Where(o => o.Created.HasValue).OrderBy(o => o.Created))
|
||||
{
|
||||
sb.AppendLine(
|
||||
$"{o.OrderNumber} | {o.Created?.ToString("yyyy/MM/dd HH:mm") ?? "-"} | {o.ItemsCount} | {o.GatewayAmount:N0} | {o.DiscountBalanceUsed:N0} | {GetStatusText(o.Status)}");
|
||||
$"{o.Id} | {o.UserFullName} | {o.Created?.MiladiToJalaliWithTime() ?? "-"} | {o.ItemsCount} | {o.GatewayAmount:N0} | {o.DiscountBalanceUsed:N0} | {GetStatusText(o.Status)}");
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(sb.ToString());
|
||||
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
var filename = $"discount-sales-{DateTime.Now:yyyyMMddHHmmss}.txt";
|
||||
|
||||
|
||||
+328
-122
@@ -1,113 +1,243 @@
|
||||
@attribute [Route(RouteConstance.HomePage)]
|
||||
@attribute [Route("/dashboard/overview")]
|
||||
|
||||
@using BackOffice.BFF.User.Protobuf.Protos.User
|
||||
@using BackOffice.BFF.UserOrder.Protobuf.Protos.UserOrder
|
||||
@using BackOffice.BFF.Products.Protobuf.Protos.Products
|
||||
@using BackOffice.BFF.Package.Protobuf.Protos.Package
|
||||
@using BackOffice.BFF.Category.Protobuf.Protos.Category
|
||||
@using CMSMicroservice.Protobuf.Protos.User
|
||||
@using CMSMicroservice.Protobuf.Protos.UserOrder
|
||||
@using CMSMicroservice.Protobuf.Protos
|
||||
@using CMSMicroservice.Protobuf.Protos.Products
|
||||
@using CMSMicroservice.Protobuf.Protos.Package
|
||||
@using CMSMicroservice.Protobuf.Protos.Category
|
||||
@using CMSMicroservice.Protobuf.Protos.Commission
|
||||
@using CMSMicroservice.Protobuf.Protos.ClubMembership
|
||||
@inject UserContract.UserContractClient UserContract
|
||||
@inject UserOrderContract.UserOrderContractClient UserOrderContract
|
||||
@inject ProductsContract.ProductsContractClient ProductsContract
|
||||
@inject PackageContract.PackageContractClient PackageContract
|
||||
@inject CategoryContract.CategoryContractClient CategoryContract
|
||||
@inject CommissionContract.CommissionContractClient CommissionClient
|
||||
@inject ClubMembershipContract.ClubMembershipContractClient ClubClient
|
||||
|
||||
<MudStack Spacing="3">
|
||||
<MudText Typo="Typo.h5">داشبورد مدیریت</MudText>
|
||||
|
||||
<MudGrid GutterSize="3">
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4">
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.subtitle2">تعداد کاربران</MudText>
|
||||
<MudText Typo="Typo.h5">@_totalUsers.ToString("N0")</MudText>
|
||||
@* ── عنوان و وضعیت بارگذاری ── *@
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
||||
<MudText Typo="Typo.h5">داشبورد مدیریت</MudText>
|
||||
@if (_loading)
|
||||
{
|
||||
<MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true" />
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
@* ── آمار کلی فروشگاه ── *@
|
||||
<MudGrid Spacing="2">
|
||||
<MudItem xs="6" sm="4" md="2">
|
||||
<MudPaper Class="pa-3 rounded-lg" Elevation="0" Outlined="true">
|
||||
<MudStack Spacing="1" AlignItems="AlignItems.Start">
|
||||
<MudIcon Icon="@Icons.Material.Filled.People" Color="Color.Primary" Size="Size.Small" />
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">کاربران</MudText>
|
||||
<MudText Typo="Typo.h6">@_totalUsers.ToString("N0")</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4">
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.subtitle2">تعداد محصولات</MudText>
|
||||
<MudText Typo="Typo.h5">@_totalProducts.ToString("N0")</MudText>
|
||||
<MudItem xs="6" sm="4" md="2">
|
||||
<MudPaper Class="pa-3 rounded-lg" Elevation="0" Outlined="true">
|
||||
<MudStack Spacing="1" AlignItems="AlignItems.Start">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ShoppingBag" Color="Color.Info" Size="Size.Small" />
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">محصولات</MudText>
|
||||
<MudText Typo="Typo.h6">@_totalProducts.ToString("N0")</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4">
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.subtitle2">تعداد پکیجها</MudText>
|
||||
<MudText Typo="Typo.h5">@_totalPackages.ToString("N0")</MudText>
|
||||
<MudItem xs="6" sm="4" md="2">
|
||||
<MudPaper Class="pa-3 rounded-lg" Elevation="0" Outlined="true">
|
||||
<MudStack Spacing="1" AlignItems="AlignItems.Start">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Inventory2" Color="Color.Secondary" Size="Size.Small" />
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">پکیجها</MudText>
|
||||
<MudText Typo="Typo.h6">@_totalPackages.ToString("N0")</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4">
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.subtitle2">تعداد دستهبندیها</MudText>
|
||||
<MudText Typo="Typo.h5">@_totalCategories.ToString("N0")</MudText>
|
||||
<MudItem xs="6" sm="4" md="2">
|
||||
<MudPaper Class="pa-3 rounded-lg" Elevation="0" Outlined="true">
|
||||
<MudStack Spacing="1" AlignItems="AlignItems.Start">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Category" Color="Color.Tertiary" Size="Size.Small" />
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">دستهبندیها</MudText>
|
||||
<MudText Typo="Typo.h6">@_totalCategories.ToString("N0")</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4">
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.subtitle2">تعداد سفارشها</MudText>
|
||||
<MudText Typo="Typo.h5">@_totalOrders.ToString("N0")</MudText>
|
||||
<MudItem xs="6" sm="4" md="2">
|
||||
<MudPaper Class="pa-3 rounded-lg" Elevation="0" Outlined="true">
|
||||
<MudStack Spacing="1" AlignItems="AlignItems.Start">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Receipt" Color="Color.Success" Size="Size.Small" />
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">سفارشها</MudText>
|
||||
<MudText Typo="Typo.h6">@_totalOrders.ToString("N0")</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4">
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.subtitle2">سفارشهای پرداختشده</MudText>
|
||||
<MudText Typo="Typo.h5">@_paidOrders.ToString("N0")</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4">
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.subtitle2">جمع مبلغ سفارشهای پرداختشده</MudText>
|
||||
<MudText Typo="Typo.h6">@_totalPaidAmount.ToString("N0") تومان</MudText>
|
||||
<MudItem xs="6" sm="4" md="2">
|
||||
<MudPaper Class="pa-3 rounded-lg" Elevation="0" Outlined="true">
|
||||
<MudStack Spacing="1" AlignItems="AlignItems.Start">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Paid" Color="Color.Warning" Size="Size.Small" />
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">مبلغ پرداختشده</MudText>
|
||||
<MudText Typo="Typo.h6">@_totalPaidAmount.ToString("N0")</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudPaper Class="pa-4">
|
||||
<MudStack Spacing="2">
|
||||
<MudText Typo="Typo.subtitle2">آخرین سفارشهای پرداختشده</MudText>
|
||||
@if (_lastOrders.Count == 0)
|
||||
{
|
||||
<MudText Typo="Typo.body2">سفارشی ثبت نشده است.</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTable Items="_lastOrders" Dense="true">
|
||||
<HeaderContent>
|
||||
<MudTh>شناسه سفارش</MudTh>
|
||||
<MudTh>کاربر</MudTh>
|
||||
<MudTh>مبلغ</MudTh>
|
||||
<MudTh>تاریخ پرداخت</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.Id</MudTd>
|
||||
<MudTd>@context.UserFullName (@context.UserNationalCode)</MudTd>
|
||||
<MudTd>@context.Amount.ToString("N0")</MudTd>
|
||||
<MudTd>@context.PaymentDate.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm")</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
}
|
||||
@* ── کمیسیون هفته جاری ── *@
|
||||
<MudPaper Class="pa-4 rounded-lg" Outlined="true">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-3">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.subtitle1">کمیسیون هفته جاری</MudText>
|
||||
</MudStack>
|
||||
<MudButton Variant="Variant.Text" Color="Color.Primary" Href="/commission/dashboard" Size="Size.Small">
|
||||
جزئیات بیشتر
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
<MudGrid Spacing="2">
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">هفته جاری</MudText>
|
||||
<MudText Typo="Typo.h6">@(_currentWeekDisplayName ?? "—")</MudText>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">مبلغ کل استخر</MudText>
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary">
|
||||
@(_currentPool?.TotalPoolAmount.ToString("N0") ?? "0") ریال
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">مجموع موجودیها</MudText>
|
||||
<MudText Typo="Typo.h6">@(_currentPool?.TotalBalances.ToString("N0") ?? "0")</MudText>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">وضعیت</MudText>
|
||||
<MudChip T="string" Color="@(_currentPool?.IsCalculated == true ? Color.Success : Color.Warning)"
|
||||
Size="Size.Small">
|
||||
@(_currentPool?.IsCalculated == true ? "محاسبه شده" : "در انتظار")
|
||||
</MudChip>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
||||
@* ── آمار باشگاه ── *@
|
||||
<MudPaper Class="pa-4 rounded-lg" Outlined="true">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-3">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.CardMembership" Color="Color.Secondary" />
|
||||
<MudText Typo="Typo.subtitle1">عضویت باشگاه</MudText>
|
||||
</MudStack>
|
||||
<MudButton Variant="Variant.Text" Color="Color.Primary" Href="/club/members" Size="Size.Small">
|
||||
مشاهده اعضا
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
<MudGrid Spacing="2">
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">کل اعضا</MudText>
|
||||
<MudText Typo="Typo.h6" Color="Color.Primary">@_totalClubMembers.ToString("N0")</MudText>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">فعال</MudText>
|
||||
<MudText Typo="Typo.h6" Color="Color.Success">@_activeClubMembers.ToString("N0")</MudText>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="4">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">غیرفعال</MudText>
|
||||
<MudText Typo="Typo.h6" Color="Color.Warning">@_inactiveClubMembers.ToString("N0")</MudText>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
||||
@* ── آخرین سفارشها ── *@
|
||||
<MudPaper Class="pa-4 rounded-lg" Outlined="true">
|
||||
<MudText Typo="Typo.subtitle1" Class="mb-2">آخرین سفارشهای پرداختشده</MudText>
|
||||
@if (_lastOrders.Count == 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text">سفارشی ثبت نشده است.</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTable Items="_lastOrders" Dense="true" Hover="true" Elevation="0">
|
||||
<HeaderContent>
|
||||
<MudTh>شناسه</MudTh>
|
||||
<MudTh>کاربر</MudTh>
|
||||
<MudTh>مبلغ</MudTh>
|
||||
<MudTh>تاریخ پرداخت</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.Id</MudTd>
|
||||
<MudTd>@context.UserFullName</MudTd>
|
||||
<MudTd>@context.Amount.ToString("N0")</MudTd>
|
||||
<MudTd>@(context.PaymentDate != null ? context.PaymentDate.ToDateTime().MiladiToJalali() : "—")</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
}
|
||||
</MudPaper>
|
||||
|
||||
@* ── دسترسی سریع ── *@
|
||||
<MudPaper Class="pa-4 rounded-lg" Outlined="true">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mb-3">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Speed" Color="Color.Info" />
|
||||
<MudText Typo="Typo.subtitle1">دسترسی سریع</MudText>
|
||||
</MudStack>
|
||||
<MudGrid Spacing="2">
|
||||
<MudItem xs="6" md="4">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary" FullWidth="true"
|
||||
Href="/commission/dashboard" StartIcon="@Icons.Material.Filled.AccountBalanceWallet">
|
||||
داشبورد کمیسیون
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
<MudItem xs="6" md="4">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" FullWidth="true"
|
||||
Href="/commission/payouts" StartIcon="@Icons.Material.Filled.Payments">
|
||||
واریزیها
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
<MudItem xs="6" md="4">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Warning" FullWidth="true"
|
||||
Href="/commission/withdrawals" StartIcon="@Icons.Material.Filled.RequestQuote">
|
||||
درخواستهای برداشت
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
<MudItem xs="6" md="4">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Info" FullWidth="true"
|
||||
Href="/network/tree" StartIcon="@Icons.Material.Filled.AccountTree">
|
||||
درخت شبکه
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
<MudItem xs="6" md="4">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Success" FullWidth="true"
|
||||
Href="/network/balances" StartIcon="@Icons.Material.Filled.AccountBalance">
|
||||
گزارش موجودیها
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
<MudItem xs="6" md="4">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Tertiary" FullWidth="true"
|
||||
Href="/club/members" StartIcon="@Icons.Material.Filled.Groups">
|
||||
مدیریت باشگاه
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
</MudStack>
|
||||
|
||||
@code {
|
||||
private bool _loading = true;
|
||||
private long _totalUsers;
|
||||
private long _totalOrders;
|
||||
private long _paidOrders;
|
||||
@@ -117,63 +247,139 @@
|
||||
private long _totalCategories;
|
||||
private readonly List<GetUserOrderResponse> _lastOrders = new();
|
||||
|
||||
// Commission
|
||||
private string? _currentWeekDisplayName;
|
||||
private long _currentWeekDefinitionId;
|
||||
private GetWeeklyCommissionPoolResponse? _currentPool;
|
||||
|
||||
// Club
|
||||
private long _totalClubMembers;
|
||||
private long _activeClubMembers;
|
||||
private long _inactiveClubMembers;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// کاربران
|
||||
var userRequest = new GetAllUserByFilterRequest
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
PaginationState = new() { PageNumber = 1, PageSize = 1 },
|
||||
Filter = new()
|
||||
};
|
||||
var userResult = await UserContract.GetAllUserByFilterAsync(userRequest);
|
||||
_totalUsers = userResult?.MetaData?.TotalCount ?? 0;
|
||||
|
||||
// سفارشها
|
||||
var orderRequest = new GetAllUserOrderByFilterRequest
|
||||
{
|
||||
PaginationState = new() { PageNumber = 1, PageSize = 10 },
|
||||
};
|
||||
var orderResult = await UserOrderContract.GetAllUserOrderByFilterAsync(orderRequest);
|
||||
if (orderResult != null && orderResult.Models != null)
|
||||
{
|
||||
_totalOrders = orderResult.MetaData?.TotalCount ?? 0;
|
||||
_paidOrders = orderResult.Models.Count(m => m.PaymentStatus==PaymentStatus.Success);
|
||||
_totalPaidAmount = orderResult.Models
|
||||
.Where(m => m.PaymentStatus==PaymentStatus.Success)
|
||||
.Aggregate(0L, (sum, m) => sum + m.Amount);
|
||||
|
||||
foreach (var item in orderResult.Models.OrderByDescending(m => m.PaymentDate).Take(5))
|
||||
// Load all data in parallel for speed
|
||||
var tasks = new List<Task>
|
||||
{
|
||||
var full = await UserOrderContract.GetUserOrderAsync(new GetUserOrderRequest { Id = item.Id });
|
||||
_lastOrders.Add(full);
|
||||
LoadCmsStats(),
|
||||
LoadCommissionStats(),
|
||||
LoadClubStats(),
|
||||
LoadRecentOrders()
|
||||
};
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
catch { /* Individual loaders handle their own errors */ }
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadCmsStats()
|
||||
{
|
||||
try
|
||||
{
|
||||
var userCall = UserContract.GetAllUserByFilterAsync(new GetAllUserByFilterRequest
|
||||
{
|
||||
PaginationState = new() { PageNumber = 1, PageSize = 1 },
|
||||
Filter = new()
|
||||
});
|
||||
var productCall = ProductsContract.GetAllProductsByFilterAsync(new GetAllProductsByFilterRequest
|
||||
{
|
||||
PaginationState = new() { PageNumber = 1, PageSize = 1 },
|
||||
Filter = new()
|
||||
});
|
||||
var packageCall = PackageContract.GetAllPackageByFilterAsync(new GetAllPackageByFilterRequest
|
||||
{
|
||||
PaginationState = new() { PageNumber = 1, PageSize = 1 },
|
||||
Filter = new()
|
||||
});
|
||||
var categoryCall = CategoryContract.GetAllCategoryByFilterAsync(new GetAllCategoryByFilterRequest
|
||||
{
|
||||
PaginationState = new() { PageNumber = 1, PageSize = 1 },
|
||||
Filter = new()
|
||||
});
|
||||
|
||||
await Task.WhenAll(userCall.ResponseAsync, productCall.ResponseAsync, packageCall.ResponseAsync, categoryCall.ResponseAsync);
|
||||
|
||||
_totalUsers = (await userCall)?.MetaData?.TotalCount ?? 0;
|
||||
_totalProducts = (await productCall)?.MetaData?.TotalCount ?? 0;
|
||||
_totalPackages = (await packageCall)?.MetaData?.TotalCount ?? 0;
|
||||
_totalCategories = (await categoryCall)?.MetaData?.TotalCount ?? 0;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private async Task LoadCommissionStats()
|
||||
{
|
||||
try
|
||||
{
|
||||
var availableWeeks = await CommissionClient.GetAvailableWeeksAsync(new GetAvailableWeeksRequest
|
||||
{
|
||||
FutureWeeksCount = 0,
|
||||
PastWeeksCount = 0
|
||||
});
|
||||
if (availableWeeks?.CurrentWeek != null)
|
||||
{
|
||||
_currentWeekDefinitionId = availableWeeks.CurrentWeek.WeekDefinitionId;
|
||||
_currentWeekDisplayName = availableWeeks.CurrentWeek.DisplayName;
|
||||
|
||||
_currentPool = await CommissionClient.GetWeeklyCommissionPoolAsync(new GetWeeklyCommissionPoolRequest
|
||||
{
|
||||
WeekDefinitionId = _currentWeekDefinitionId
|
||||
});
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// محصولات
|
||||
var productRequest = new GetAllProductsByFilterRequest
|
||||
private async Task LoadClubStats()
|
||||
{
|
||||
try
|
||||
{
|
||||
PaginationState = new() { PageNumber = 1, PageSize = 1 },
|
||||
Filter = new()
|
||||
};
|
||||
var productResult = await ProductsContract.GetAllProductsByFilterAsync(productRequest);
|
||||
_totalProducts = productResult?.MetaData?.TotalCount ?? 0;
|
||||
var allReq = ClubClient.GetAllClubMembershipsAsync(new GetAllClubMembershipsRequest { PageIndex = 1, PageSize = 1 });
|
||||
var activeReq = ClubClient.GetAllClubMembershipsAsync(new GetAllClubMembershipsRequest { PageIndex = 1, PageSize = 1, IsActive = true });
|
||||
|
||||
// پکیجها
|
||||
var packageRequest = new GetAllPackageByFilterRequest
|
||||
{
|
||||
PaginationState = new() { PageNumber = 1, PageSize = 1 },
|
||||
Filter = new()
|
||||
};
|
||||
var packageResult = await PackageContract.GetAllPackageByFilterAsync(packageRequest);
|
||||
_totalPackages = packageResult?.MetaData?.TotalCount ?? 0;
|
||||
await Task.WhenAll(allReq.ResponseAsync, activeReq.ResponseAsync);
|
||||
|
||||
// دستهبندیها
|
||||
var categoryRequest = new GetAllCategoryByFilterRequest
|
||||
_totalClubMembers = (await allReq).MetaData?.TotalCount ?? 0;
|
||||
_activeClubMembers = (await activeReq).MetaData?.TotalCount ?? 0;
|
||||
_inactiveClubMembers = _totalClubMembers - _activeClubMembers;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private async Task LoadRecentOrders()
|
||||
{
|
||||
try
|
||||
{
|
||||
PaginationState = new() { PageNumber = 1, PageSize = 1 },
|
||||
Filter = new()
|
||||
};
|
||||
var categoryResult = await CategoryContract.GetAllCategoryByFilterAsync(categoryRequest);
|
||||
_totalCategories = categoryResult?.MetaData?.TotalCount ?? 0;
|
||||
var orderResult = await UserOrderContract.GetAllUserOrderByFilterAsync(new GetAllUserOrderByFilterRequest
|
||||
{
|
||||
PaginationState = new() { PageNumber = 1, PageSize = 5 },
|
||||
});
|
||||
if (orderResult?.Models != null)
|
||||
{
|
||||
_totalOrders = orderResult.MetaData?.TotalCount ?? 0;
|
||||
_paidOrders = orderResult.Models.Count(m => m.PaymentStatus == PaymentStatus.Success);
|
||||
_totalPaidAmount = orderResult.Models
|
||||
.Where(m => m.PaymentStatus == PaymentStatus.Success)
|
||||
.Sum(m => m.Amount);
|
||||
|
||||
foreach (var item in orderResult.Models.OrderByDescending(m => m.PaymentDate).Take(5))
|
||||
{
|
||||
try
|
||||
{
|
||||
var full = await UserOrderContract.GetUserOrderAsync(new GetUserOrderRequest { Id = item.Id });
|
||||
_lastOrders.Add(full);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.Inventory
|
||||
@using Google.Protobuf.WellKnownTypes
|
||||
@using InventoryProductType = CMSMicroservice.Protobuf.Protos.Inventory.ProductType
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudForm @ref="_form" @bind-IsValid="_formValid">
|
||||
<MudGrid>
|
||||
@if (ProductIdParam == null)
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<MudSelect T="InventoryProductType" @bind-Value="_productType"
|
||||
Label="نوع محصول"
|
||||
Variant="Variant.Outlined"
|
||||
Required="true"
|
||||
RequiredError="نوع محصول الزامی است">
|
||||
<MudSelectItem T="InventoryProductType" Value="InventoryProductType.RegularProduct">محصول عادی</MudSelectItem>
|
||||
<MudSelectItem T="InventoryProductType" Value="InventoryProductType.DiscountProduct">محصول تخفیفی</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudNumericField T="long" @bind-Value="_productId"
|
||||
Label="شناسه محصول"
|
||||
Variant="Variant.Outlined"
|
||||
Required="true"
|
||||
RequiredError="شناسه محصول الزامی است"
|
||||
Min="1" />
|
||||
</MudItem>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<MudAlert Severity="Severity.Info">
|
||||
محصول: <strong>@ProductName</strong>
|
||||
</MudAlert>
|
||||
</MudItem>
|
||||
}
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudSelect T="long?" @bind-Value="_warehouseId"
|
||||
Label="انبار"
|
||||
Variant="Variant.Outlined"
|
||||
Required="true"
|
||||
RequiredError="انبار الزامی است">
|
||||
@foreach (var wh in Warehouses)
|
||||
{
|
||||
<MudSelectItem T="long?" Value="@((long?)wh.Id)">@wh.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudNumericField T="int" @bind-Value="_quantity"
|
||||
Label="تعداد"
|
||||
Variant="Variant.Outlined"
|
||||
Required="true"
|
||||
RequiredError="تعداد الزامی است"
|
||||
Min="1" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudTextField T="string" @bind-Value="_referenceNumber"
|
||||
Label="شماره مرجع (فاکتور/رسید)"
|
||||
Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudTextField T="string" @bind-Value="_note"
|
||||
Label="توضیحات"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="3" />
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel">انصراف</MudButton>
|
||||
<MudButton Color="Color.Primary"
|
||||
Variant="Variant.Filled"
|
||||
OnClick="Submit"
|
||||
Disabled="@(!_formValid || _isSubmitting)">
|
||||
@if (_isSubmitting)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
|
||||
}
|
||||
ثبت موجودی
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
[Inject] public InventoryContract.InventoryContractClient InventoryContract { get; set; } = default!;
|
||||
|
||||
[Parameter] public List<WarehouseDto> Warehouses { get; set; } = new();
|
||||
[Parameter] public long? ProductIdParam { get; set; }
|
||||
[Parameter] public InventoryProductType? ProductTypeParam { get; set; }
|
||||
[Parameter] public string? ProductName { get; set; }
|
||||
[Parameter] public long? WarehouseIdParam { get; set; }
|
||||
|
||||
private MudForm? _form;
|
||||
private bool _formValid;
|
||||
private bool _isSubmitting;
|
||||
|
||||
private InventoryProductType _productType = InventoryProductType.RegularProduct;
|
||||
private long _productId;
|
||||
private long? _warehouseId;
|
||||
private int _quantity = 1;
|
||||
private string _referenceNumber = string.Empty;
|
||||
private string _note = string.Empty;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
if (ProductIdParam.HasValue)
|
||||
{
|
||||
_productId = ProductIdParam.Value;
|
||||
}
|
||||
if (ProductTypeParam.HasValue)
|
||||
{
|
||||
_productType = ProductTypeParam.Value;
|
||||
}
|
||||
if (WarehouseIdParam.HasValue)
|
||||
{
|
||||
_warehouseId = WarehouseIdParam.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private void Cancel() => MudDialog.Cancel();
|
||||
|
||||
private async Task Submit()
|
||||
{
|
||||
if (_form != null)
|
||||
{
|
||||
await _form.Validate();
|
||||
if (!_formValid) return;
|
||||
}
|
||||
|
||||
if (!_warehouseId.HasValue)
|
||||
{
|
||||
Snackbar.Add("لطفاً انبار را انتخاب کنید", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
_isSubmitting = true;
|
||||
try
|
||||
{
|
||||
var request = new AddStockRequest
|
||||
{
|
||||
ProductId = ProductIdParam ?? _productId,
|
||||
ProductType = ProductTypeParam ?? _productType,
|
||||
Quantity = _quantity,
|
||||
ReferenceNumber = _referenceNumber ?? string.Empty,
|
||||
Note = _note ?? string.Empty,
|
||||
WarehouseId = _warehouseId.Value
|
||||
};
|
||||
|
||||
var response = await InventoryContract.AddStockAsync(request);
|
||||
|
||||
Snackbar.Add($"موجودی جدید: {response.NewQuantity} عدد", Severity.Success);
|
||||
MudDialog.Close(DialogResult.Ok(true));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSubmitting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.Inventory
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudForm @ref="_form" @bind-IsValid="_formValid">
|
||||
<MudStack Spacing="4">
|
||||
<MudAlert Severity="Severity.Info">
|
||||
<MudStack Spacing="1">
|
||||
<MudText>محصول: <strong>@Item.ProductTitle</strong></MudText>
|
||||
<MudText>موجودی فعلی: <strong>@Item.Quantity</strong> عدد</MudText>
|
||||
<MudText>رزرو شده: <strong>@Item.ReservedQuantity</strong> عدد</MudText>
|
||||
<MudText>قابل فروش: <strong>@Item.AvailableQuantity</strong> عدد</MudText>
|
||||
</MudStack>
|
||||
</MudAlert>
|
||||
|
||||
<MudNumericField T="int" @bind-Value="_newQuantity"
|
||||
Label="موجودی جدید"
|
||||
Variant="Variant.Outlined"
|
||||
Required="true"
|
||||
RequiredError="موجودی الزامی است"
|
||||
Min="0"
|
||||
HelperText="@GetDifferenceText()" />
|
||||
|
||||
<MudTextField T="string" @bind-Value="_note"
|
||||
Label="توضیحات (دلیل تغییر)"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="3"
|
||||
Required="true"
|
||||
RequiredError="توضیحات الزامی است" />
|
||||
|
||||
@if (_newQuantity != Item.Quantity)
|
||||
{
|
||||
<MudAlert Severity="@(GetDifference() > 0 ? Severity.Success : Severity.Warning)">
|
||||
@GetDifferenceAlertText()
|
||||
</MudAlert>
|
||||
}
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel">انصراف</MudButton>
|
||||
<MudButton Color="Color.Primary"
|
||||
Variant="Variant.Filled"
|
||||
OnClick="Submit"
|
||||
Disabled="@(!_formValid || _isSubmitting || _newQuantity == Item.Quantity)">
|
||||
@if (_isSubmitting)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
|
||||
}
|
||||
ذخیره تغییرات
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
[Inject] public InventoryContract.InventoryContractClient InventoryContract { get; set; } = default!;
|
||||
|
||||
[Parameter] public InventoryItemDto Item { get; set; } = default!;
|
||||
|
||||
private MudForm? _form;
|
||||
private bool _formValid;
|
||||
private bool _isSubmitting;
|
||||
|
||||
private int _newQuantity;
|
||||
private string _note = string.Empty;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_newQuantity = Item.Quantity;
|
||||
}
|
||||
|
||||
private int GetDifference() => _newQuantity - Item.Quantity;
|
||||
|
||||
private string GetDifferenceText()
|
||||
{
|
||||
var diff = GetDifference();
|
||||
if (diff == 0) return "بدون تغییر";
|
||||
return diff > 0 ? $"افزایش {diff} عدد" : $"کاهش {Math.Abs(diff)} عدد";
|
||||
}
|
||||
|
||||
private string GetDifferenceAlertText()
|
||||
{
|
||||
var diff = GetDifference();
|
||||
if (diff > 0)
|
||||
return $"موجودی {diff} عدد افزایش مییابد (از {Item.Quantity} به {_newQuantity})";
|
||||
return $"موجودی {Math.Abs(diff)} عدد کاهش مییابد (از {Item.Quantity} به {_newQuantity})";
|
||||
}
|
||||
|
||||
private void Cancel() => MudDialog.Cancel();
|
||||
|
||||
private async Task Submit()
|
||||
{
|
||||
if (_form != null)
|
||||
{
|
||||
await _form.Validate();
|
||||
if (!_formValid) return;
|
||||
}
|
||||
|
||||
if (_newQuantity == Item.Quantity)
|
||||
{
|
||||
Snackbar.Add("هیچ تغییری در موجودی انجام نشده است", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
_isSubmitting = true;
|
||||
try
|
||||
{
|
||||
var productId = Item.ProductType == ProductType.RegularProduct
|
||||
? (Item.ProductId ?? 0)
|
||||
: (Item.DiscountProductId ?? 0);
|
||||
|
||||
var request = new AdjustStockRequest
|
||||
{
|
||||
ProductId = productId,
|
||||
ProductType = Item.ProductType,
|
||||
NewQuantity = _newQuantity,
|
||||
Reason = _note ?? string.Empty
|
||||
};
|
||||
|
||||
var response = await InventoryContract.AdjustStockAsync(request);
|
||||
|
||||
Snackbar.Add($"موجودی از {response.PreviousQuantity} به {response.NewQuantity} تغییر کرد (تفاوت: {response.Difference})", Severity.Success);
|
||||
MudDialog.Close(DialogResult.Ok(true));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSubmitting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.Inventory
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudForm @ref="_form" @bind-IsValid="_formValid">
|
||||
<MudStack Spacing="4">
|
||||
<MudTextField T="string" @bind-Value="_name"
|
||||
Label="نام انبار"
|
||||
Variant="Variant.Outlined"
|
||||
Required="true"
|
||||
RequiredError="نام انبار الزامی است"
|
||||
MaxLength="100" />
|
||||
|
||||
<MudTextField T="string" @bind-Value="_code"
|
||||
Label="کد انبار"
|
||||
Variant="Variant.Outlined"
|
||||
Required="true"
|
||||
RequiredError="کد انبار الزامی است"
|
||||
MaxLength="20"
|
||||
HelperText="کد منحصر به فرد برای شناسایی انبار (مثال: WH-001)" />
|
||||
|
||||
<MudTextField T="string" @bind-Value="_address"
|
||||
Label="آدرس"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="2"
|
||||
MaxLength="500" />
|
||||
|
||||
<MudSwitch T="bool" @bind-Value="_isDefault"
|
||||
Color="Color.Primary"
|
||||
Label="انبار پیشفرض"
|
||||
LabelPosition="LabelPosition.Start" />
|
||||
|
||||
@if (_isDefault)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info">
|
||||
این انبار به عنوان انبار پیشفرض برای ورود کالا تنظیم میشود.
|
||||
</MudAlert>
|
||||
}
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel">انصراف</MudButton>
|
||||
<MudButton Color="Color.Primary"
|
||||
Variant="Variant.Filled"
|
||||
OnClick="Submit"
|
||||
Disabled="@(!_formValid || _isSubmitting)">
|
||||
@if (_isSubmitting)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
|
||||
}
|
||||
ایجاد انبار
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
[Inject] public InventoryContract.InventoryContractClient InventoryContract { get; set; } = default!;
|
||||
|
||||
private MudForm? _form;
|
||||
private bool _formValid;
|
||||
private bool _isSubmitting;
|
||||
|
||||
private string _name = string.Empty;
|
||||
private string _code = string.Empty;
|
||||
private string _address = string.Empty;
|
||||
private bool _isDefault = false;
|
||||
|
||||
private void Cancel() => MudDialog.Cancel();
|
||||
|
||||
private async Task Submit()
|
||||
{
|
||||
if (_form != null)
|
||||
{
|
||||
await _form.Validate();
|
||||
if (!_formValid) return;
|
||||
}
|
||||
|
||||
_isSubmitting = true;
|
||||
try
|
||||
{
|
||||
var request = new CreateWarehouseRequest
|
||||
{
|
||||
Name = _name,
|
||||
Code = _code,
|
||||
Address = _address ?? string.Empty,
|
||||
IsDefault = _isDefault
|
||||
};
|
||||
|
||||
var response = await InventoryContract.CreateWarehouseAsync(request);
|
||||
|
||||
if (response.Id > 0)
|
||||
{
|
||||
Snackbar.Add($"انبار '{_name}' با شناسه {response.Id} ایجاد شد", Severity.Success);
|
||||
MudDialog.Close(DialogResult.Ok(response.Id));
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add("خطا در ایجاد انبار", Severity.Error);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSubmitting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.Inventory
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudForm @ref="_form" @bind-IsValid="_formValid">
|
||||
<MudStack Spacing="4">
|
||||
<MudAlert Severity="Severity.Info">
|
||||
<MudStack Spacing="1">
|
||||
<MudText>محصول: <strong>@Item.ProductTitle</strong></MudText>
|
||||
<MudText>انبار: <strong>@Item.WarehouseName</strong></MudText>
|
||||
</MudStack>
|
||||
</MudAlert>
|
||||
|
||||
<MudNumericField T="int" @bind-Value="_lowStockThreshold"
|
||||
Label="حد هشدار کمموجودی"
|
||||
Variant="Variant.Outlined"
|
||||
Required="true"
|
||||
RequiredError="این فیلد الزامی است"
|
||||
Min="0"
|
||||
HelperText="وقتی موجودی کمتر از این عدد شود، هشدار داده میشود" />
|
||||
|
||||
<MudNumericField T="int" @bind-Value="_reorderPoint"
|
||||
Label="نقطه سفارش مجدد"
|
||||
Variant="Variant.Outlined"
|
||||
Required="true"
|
||||
RequiredError="این فیلد الزامی است"
|
||||
Min="0"
|
||||
HelperText="وقتی موجودی به این عدد برسد، باید سفارش داده شود" />
|
||||
|
||||
<MudNumericField T="int" @bind-Value="_maxStockLevel"
|
||||
Label="حداکثر موجودی مجاز"
|
||||
Variant="Variant.Outlined"
|
||||
Required="true"
|
||||
RequiredError="این فیلد الزامی است"
|
||||
Min="1"
|
||||
HelperText="حداکثر تعدادی که در انبار نگهداری میشود" />
|
||||
|
||||
<MudDivider />
|
||||
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||
مقادیر فعلی:
|
||||
حد هشدار: @Item.LowStockThreshold |
|
||||
نقطه سفارش: @Item.ReorderPoint |
|
||||
حداکثر: @Item.MaxStockLevel
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel">انصراف</MudButton>
|
||||
<MudButton Color="Color.Primary"
|
||||
Variant="Variant.Filled"
|
||||
OnClick="Submit"
|
||||
Disabled="@(!_formValid || _isSubmitting)">
|
||||
@if (_isSubmitting)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
|
||||
}
|
||||
ذخیره تنظیمات
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
[Inject] public InventoryContract.InventoryContractClient InventoryContract { get; set; } = default!;
|
||||
|
||||
[Parameter] public InventoryItemDto Item { get; set; } = default!;
|
||||
|
||||
private MudForm? _form;
|
||||
private bool _formValid;
|
||||
private bool _isSubmitting;
|
||||
|
||||
private int _lowStockThreshold;
|
||||
private int _reorderPoint;
|
||||
private int _maxStockLevel;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_lowStockThreshold = Item.LowStockThreshold;
|
||||
_reorderPoint = Item.ReorderPoint;
|
||||
_maxStockLevel = Item.MaxStockLevel;
|
||||
}
|
||||
|
||||
private void Cancel() => MudDialog.Cancel();
|
||||
|
||||
private async Task Submit()
|
||||
{
|
||||
if (_form != null)
|
||||
{
|
||||
await _form.Validate();
|
||||
if (!_formValid) return;
|
||||
}
|
||||
|
||||
// Validation
|
||||
if (_lowStockThreshold > _reorderPoint)
|
||||
{
|
||||
Snackbar.Add("حد هشدار نمیتواند از نقطه سفارش بیشتر باشد", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_reorderPoint > _maxStockLevel)
|
||||
{
|
||||
Snackbar.Add("نقطه سفارش نمیتواند از حداکثر موجودی بیشتر باشد", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
_isSubmitting = true;
|
||||
try
|
||||
{
|
||||
var request = new UpdateInventorySettingsRequest
|
||||
{
|
||||
Id = Item.Id,
|
||||
LowStockThreshold = _lowStockThreshold,
|
||||
ReorderPoint = _reorderPoint,
|
||||
MaxStockLevel = _maxStockLevel
|
||||
};
|
||||
|
||||
await InventoryContract.UpdateInventorySettingsAsync(request);
|
||||
|
||||
Snackbar.Add("تنظیمات با موفقیت ذخیره شد", Severity.Success);
|
||||
MudDialog.Close(DialogResult.Ok(true));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSubmitting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
@using CMSMicroservice.Protobuf.Protos.Inventory
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudForm @ref="_form" @bind-IsValid="_formValid">
|
||||
<MudStack Spacing="4">
|
||||
<MudAlert Severity="Severity.Warning">
|
||||
<MudStack Spacing="1">
|
||||
<MudText>محصول: <strong>@Item.ProductTitle</strong></MudText>
|
||||
<MudText>موجودی فعلی: <strong>@Item.Quantity</strong> عدد</MudText>
|
||||
</MudStack>
|
||||
</MudAlert>
|
||||
|
||||
<MudNumericField T="int" @bind-Value="_quantity"
|
||||
Label="تعداد خسارت دیده"
|
||||
Variant="Variant.Outlined"
|
||||
Required="true"
|
||||
RequiredError="تعداد الزامی است"
|
||||
Min="1"
|
||||
Max="@Item.Quantity"
|
||||
HelperText="@($"حداکثر: {Item.Quantity} عدد")" />
|
||||
|
||||
<MudTextField T="string" @bind-Value="_reason"
|
||||
Label="دلیل خسارت"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="2"
|
||||
Required="true"
|
||||
RequiredError="دلیل خسارت الزامی است"
|
||||
Placeholder="مثال: آسیب در حمل و نقل، منقضی شده، ..." />
|
||||
|
||||
<MudTextField T="string" @bind-Value="_referenceNumber"
|
||||
Label="شماره مرجع"
|
||||
Variant="Variant.Outlined"
|
||||
HelperText="شماره گزارش یا سند داخلی" />
|
||||
|
||||
<MudSelect T="StockMovementType" @bind-Value="_lossType"
|
||||
Label="نوع خسارت"
|
||||
Variant="Variant.Outlined"
|
||||
Required="true"
|
||||
RequiredError="نوع خسارت الزامی است">
|
||||
<MudSelectItem T="StockMovementType" Value="StockMovementType.Loss">خسارت عمومی</MudSelectItem>
|
||||
<MudSelectItem T="StockMovementType" Value="StockMovementType.Damaged">آسیبدیده</MudSelectItem>
|
||||
<MudSelectItem T="StockMovementType" Value="StockMovementType.Expired">منقضی شده</MudSelectItem>
|
||||
</MudSelect>
|
||||
|
||||
@if (_quantity > 0 && _quantity <= Item.Quantity)
|
||||
{
|
||||
<MudAlert Severity="Severity.Error">
|
||||
<MudText>
|
||||
<strong>@_quantity</strong> عدد از موجودی کسر خواهد شد.
|
||||
موجودی جدید: <strong>@(Item.Quantity - _quantity)</strong> عدد
|
||||
</MudText>
|
||||
</MudAlert>
|
||||
}
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel">انصراف</MudButton>
|
||||
<MudButton Color="Color.Error"
|
||||
Variant="Variant.Filled"
|
||||
OnClick="Submit"
|
||||
Disabled="@(!_formValid || _isSubmitting)">
|
||||
@if (_isSubmitting)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
|
||||
}
|
||||
ثبت خسارت
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
[Inject] public InventoryContract.InventoryContractClient InventoryContract { get; set; } = default!;
|
||||
|
||||
[Parameter] public InventoryItemDto Item { get; set; } = default!;
|
||||
|
||||
private MudForm? _form;
|
||||
private bool _formValid;
|
||||
private bool _isSubmitting;
|
||||
|
||||
private int _quantity = 1;
|
||||
private string _reason = string.Empty;
|
||||
private string _referenceNumber = string.Empty;
|
||||
private StockMovementType _lossType = StockMovementType.Loss;
|
||||
|
||||
private void Cancel() => MudDialog.Cancel();
|
||||
|
||||
private async Task Submit()
|
||||
{
|
||||
if (_form != null)
|
||||
{
|
||||
await _form.Validate();
|
||||
if (!_formValid) return;
|
||||
}
|
||||
|
||||
if (_quantity > Item.Quantity)
|
||||
{
|
||||
Snackbar.Add("تعداد خسارت نمیتواند از موجودی فعلی بیشتر باشد", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
_isSubmitting = true;
|
||||
try
|
||||
{
|
||||
var productId = Item.ProductType == ProductType.RegularProduct
|
||||
? (Item.ProductId ?? 0)
|
||||
: (Item.DiscountProductId ?? 0);
|
||||
|
||||
var request = new RecordLossRequest
|
||||
{
|
||||
ProductId = productId,
|
||||
ProductType = Item.ProductType,
|
||||
Quantity = _quantity,
|
||||
LossType = _lossType,
|
||||
Reason = _reason ?? string.Empty,
|
||||
ReferenceNumber = _referenceNumber ?? string.Empty
|
||||
};
|
||||
|
||||
await InventoryContract.RecordLossAsync(request);
|
||||
|
||||
Snackbar.Add("خسارت با موفقیت ثبت شد", Severity.Success);
|
||||
MudDialog.Close(DialogResult.Ok(true));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSubmitting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
@attribute [Route(RouteConstance.Inventory)]
|
||||
@attribute [Authorize]
|
||||
|
||||
@using CMSMicroservice.Protobuf.Protos.Inventory
|
||||
@using BackOffice.Pages.Inventory.Components
|
||||
|
||||
<div>
|
||||
<MudText Typo="Typo.h4" Class="mb-4">مدیریت انبار</MudText>
|
||||
|
||||
@* Filters Section *@
|
||||
<MudPaper Class="pa-4 mb-4" Elevation="2">
|
||||
<MudGrid>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect T="long?" @bind-Value="_filterWarehouseId"
|
||||
Label="انبار"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Clearable="true">
|
||||
@foreach (var wh in _warehouses)
|
||||
{
|
||||
<MudSelectItem T="long?" Value="@((long?)wh.Id)">@wh.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudSelect T="ProductType" @bind-Value="_filterProductType"
|
||||
Label="نوع محصول"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense">
|
||||
<MudSelectItem T="ProductType" Value="ProductType.Unspecified">همه</MudSelectItem>
|
||||
<MudSelectItem T="ProductType" Value="ProductType.RegularProduct">محصول عادی</MudSelectItem>
|
||||
<MudSelectItem T="ProductType" Value="ProductType.DiscountProduct">محصول تخفیفی</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudTextField T="string" @bind-Value="_searchTerm"
|
||||
Label="جستجو"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Adornment="Adornment.End"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||
Clearable="true" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3" Class="d-flex align-end">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
OnClick="ApplyFilter"
|
||||
StartIcon="@Icons.Material.Filled.FilterAlt"
|
||||
Class="mr-2">
|
||||
اعمال فیلتر
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Secondary"
|
||||
OnClick="ClearFilter"
|
||||
StartIcon="@Icons.Material.Filled.Clear">
|
||||
پاک کردن
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
||||
@* Stats Cards *@
|
||||
<MudGrid Class="mb-4">
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudCard>
|
||||
<MudCardContent>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.caption">کل اقلام انبار</MudText>
|
||||
<MudText Typo="Typo.h5">@_totalCount</MudText>
|
||||
</MudStack>
|
||||
<MudIcon Icon="@Icons.Material.Filled.Inventory" Color="Color.Primary" Size="Size.Large" />
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudCard>
|
||||
<MudCardContent>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.caption">محصولات کمموجود</MudText>
|
||||
<MudText Typo="Typo.h5" Color="Color.Warning">@_lowStockCount</MudText>
|
||||
</MudStack>
|
||||
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Warning" Size="Size.Large" />
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
@* Data Grid *@
|
||||
<MudDataGrid T="InventoryItemDto"
|
||||
ServerData="@(new Func<GridState<InventoryItemDto>, Task<GridData<InventoryItemDto>>>(ServerReload))"
|
||||
Hover="true"
|
||||
@ref="_gridData"
|
||||
Height="calc(100vh - 240px)">
|
||||
<ToolBarContent>
|
||||
<MudText Typo="Typo.h6">لیست موجودی انبار</MudText>
|
||||
<MudSpacer />
|
||||
<MudStack Row="true" Spacing="2">
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Info"
|
||||
OnClick="ExportToExcel"
|
||||
StartIcon="@Icons.Material.Filled.FileDownload">
|
||||
خروجی Excel
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
OnClick="OpenAddStockDialog"
|
||||
StartIcon="@Icons.Material.Filled.Add">
|
||||
ورود کالا
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</ToolBarContent>
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
|
||||
<PropertyColumn Property="x => x.ProductTitle" Title="نام محصول">
|
||||
<CellTemplate>
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.body2"><strong>@context.Item.ProductTitle</strong></MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||
@GetProductTypeLabel(context.Item.ProductType)
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.WarehouseName" Title="انبار">
|
||||
<CellTemplate>
|
||||
<MudChip T="string" Color="Color.Info" Size="Size.Small">
|
||||
@context.Item.WarehouseName
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.Quantity" Title="موجودی">
|
||||
<CellTemplate>
|
||||
<MudText Color="@(context.Item.IsLowStock ? Color.Error : Color.Default)">
|
||||
<strong>@context.Item.Quantity</strong>
|
||||
</MudText>
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.ReservedQuantity" Title="رزرو شده">
|
||||
<CellTemplate>
|
||||
<MudText Color="Color.Warning">@context.Item.ReservedQuantity</MudText>
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.AvailableQuantity" Title="قابل فروش">
|
||||
<CellTemplate>
|
||||
<MudText Color="Color.Success"><strong>@context.Item.AvailableQuantity</strong></MudText>
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.LowStockThreshold" Title="حد هشدار" />
|
||||
|
||||
<PropertyColumn Property="x => x.IsLowStock" Title="وضعیت">
|
||||
<CellTemplate>
|
||||
@if (context.Item.IsLowStock)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Error" Size="Size.Small">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Warning" Size="Size.Small" Class="mr-1" />
|
||||
کمموجود
|
||||
</MudChip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChip T="string" Color="Color.Success" Size="Size.Small">
|
||||
موجود
|
||||
</MudChip>
|
||||
}
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
|
||||
<TemplateColumn Title="عملیات" Sortable="false">
|
||||
<CellTemplate>
|
||||
<MudStack Row="true" Spacing="1">
|
||||
<MudTooltip Text="ورود کالا">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.AddCircle"
|
||||
Size="Size.Small"
|
||||
Color="Color.Success"
|
||||
OnClick="@(() => OpenAddStockDialogForItem(context.Item))" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="تنظیم موجودی">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Size="Size.Small"
|
||||
Color="Color.Primary"
|
||||
OnClick="@(() => OpenAdjustStockDialog(context.Item))" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="ثبت خسارت">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.RemoveCircle"
|
||||
Size="Size.Small"
|
||||
Color="Color.Error"
|
||||
OnClick="@(() => OpenRecordLossDialog(context.Item))" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="تنظیمات">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Settings"
|
||||
Size="Size.Small"
|
||||
Color="Color.Secondary"
|
||||
OnClick="@(() => OpenSettingsDialog(context.Item))" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="تاریخچه تغییرات">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.History"
|
||||
Size="Size.Small"
|
||||
Color="Color.Info"
|
||||
OnClick="@(() => ViewMovements(context.Item))" />
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="InventoryItemDto"
|
||||
PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</div>
|
||||
@@ -0,0 +1,316 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using CMSMicroservice.Protobuf.Protos.Inventory;
|
||||
using BackOffice.Pages.Inventory.Components;
|
||||
using BackOffice.Common.Utilities;
|
||||
using Microsoft.JSInterop;
|
||||
using System.Text;
|
||||
|
||||
namespace BackOffice.Pages.Inventory;
|
||||
|
||||
public partial class InventoryMainPage
|
||||
{
|
||||
[Inject] public InventoryContract.InventoryContractClient InventoryContract { get; set; } = default!;
|
||||
[Inject] public IJSRuntime JsRuntime { get; set; } = default!;
|
||||
|
||||
private MudDataGrid<InventoryItemDto>? _gridData;
|
||||
private List<WarehouseDto> _warehouses = new();
|
||||
|
||||
// Filter fields
|
||||
private long? _filterWarehouseId;
|
||||
private ProductType _filterProductType = ProductType.Unspecified;
|
||||
private string _searchTerm = string.Empty;
|
||||
|
||||
// Stats
|
||||
private int _totalCount;
|
||||
private int _lowStockCount;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadWarehouses();
|
||||
await LoadLowStockCount();
|
||||
}
|
||||
|
||||
private async Task LoadWarehouses()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await InventoryContract.GetAllWarehousesAsync(new GetAllWarehousesRequest
|
||||
{
|
||||
IsActive = true
|
||||
});
|
||||
_warehouses = response.Warehouses.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری انبارها: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadLowStockCount()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await InventoryContract.GetLowStockItemsAsync(new GetLowStockItemsRequest
|
||||
{
|
||||
Page = 1,
|
||||
PageSize = 100 // Get up to 100 low stock items to count
|
||||
});
|
||||
_lowStockCount = response.TotalCount;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_lowStockCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<GridData<InventoryItemDto>> ServerReload(GridState<InventoryItemDto> state)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new GetAllInventoryItemsRequest
|
||||
{
|
||||
Page = state.Page + 1,
|
||||
PageSize = state.PageSize,
|
||||
ProductType = _filterProductType,
|
||||
Search = _searchTerm ?? string.Empty
|
||||
};
|
||||
|
||||
if (_filterWarehouseId.HasValue)
|
||||
{
|
||||
request.WarehouseId = _filterWarehouseId.Value;
|
||||
}
|
||||
|
||||
var result = await InventoryContract.GetAllInventoryItemsAsync(request);
|
||||
|
||||
if (result?.Items != null)
|
||||
{
|
||||
_totalCount = result.TotalCount;
|
||||
return new GridData<InventoryItemDto>
|
||||
{
|
||||
Items = result.Items.ToList(),
|
||||
TotalItems = result.TotalCount
|
||||
};
|
||||
}
|
||||
|
||||
return new GridData<InventoryItemDto>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری دادهها: {ex.Message}", Severity.Error);
|
||||
return new GridData<InventoryItemDto>();
|
||||
}
|
||||
}
|
||||
|
||||
private string GetProductTypeLabel(ProductType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
ProductType.RegularProduct => "محصول عادی",
|
||||
ProductType.DiscountProduct => "محصول تخفیفی",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task ApplyFilter()
|
||||
{
|
||||
if (_gridData != null)
|
||||
{
|
||||
await _gridData.ReloadServerData();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ClearFilter()
|
||||
{
|
||||
_filterWarehouseId = null;
|
||||
_filterProductType = ProductType.Unspecified;
|
||||
_searchTerm = string.Empty;
|
||||
await ApplyFilter();
|
||||
}
|
||||
|
||||
private async Task OpenAddStockDialog()
|
||||
{
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
["Warehouses"] = _warehouses
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<AddStockDialog>("ورود کالا به انبار", parameters, new DialogOptions
|
||||
{
|
||||
CloseButton = true,
|
||||
MaxWidth = MaxWidth.Medium,
|
||||
FullWidth = true
|
||||
});
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result!.Canceled)
|
||||
{
|
||||
Snackbar.Add("موجودی با موفقیت اضافه شد", Severity.Success);
|
||||
await ApplyFilter();
|
||||
await LoadLowStockCount();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenAddStockDialogForItem(InventoryItemDto item)
|
||||
{
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
["Warehouses"] = _warehouses,
|
||||
["ProductIdParam"] = item.ProductType == ProductType.RegularProduct ? item.ProductId : item.DiscountProductId,
|
||||
["ProductTypeParam"] = item.ProductType,
|
||||
["ProductName"] = item.ProductTitle,
|
||||
["WarehouseIdParam"] = item.WarehouseId
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<AddStockDialog>($"ورود کالا - {item.ProductTitle}", parameters, new DialogOptions
|
||||
{
|
||||
CloseButton = true,
|
||||
MaxWidth = MaxWidth.Medium,
|
||||
FullWidth = true
|
||||
});
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result!.Canceled)
|
||||
{
|
||||
Snackbar.Add("موجودی با موفقیت اضافه شد", Severity.Success);
|
||||
await ApplyFilter();
|
||||
await LoadLowStockCount();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenAdjustStockDialog(InventoryItemDto item)
|
||||
{
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
["Item"] = item
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<AdjustStockDialog>($"تنظیم موجودی - {item.ProductTitle}", parameters, new DialogOptions
|
||||
{
|
||||
CloseButton = true,
|
||||
MaxWidth = MaxWidth.Small,
|
||||
FullWidth = true
|
||||
});
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result!.Canceled)
|
||||
{
|
||||
Snackbar.Add("موجودی با موفقیت تنظیم شد", Severity.Success);
|
||||
await ApplyFilter();
|
||||
await LoadLowStockCount();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenRecordLossDialog(InventoryItemDto item)
|
||||
{
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
["Item"] = item
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<RecordLossDialog>($"ثبت خسارت - {item.ProductTitle}", parameters, new DialogOptions
|
||||
{
|
||||
CloseButton = true,
|
||||
MaxWidth = MaxWidth.Small,
|
||||
FullWidth = true
|
||||
});
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result!.Canceled)
|
||||
{
|
||||
Snackbar.Add("خسارت با موفقیت ثبت شد", Severity.Warning);
|
||||
await ApplyFilter();
|
||||
await LoadLowStockCount();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenSettingsDialog(InventoryItemDto item)
|
||||
{
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
["Item"] = item
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<InventorySettingsDialog>($"تنظیمات - {item.ProductTitle}", parameters, new DialogOptions
|
||||
{
|
||||
CloseButton = true,
|
||||
MaxWidth = MaxWidth.Small,
|
||||
FullWidth = true
|
||||
});
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result!.Canceled)
|
||||
{
|
||||
Snackbar.Add("تنظیمات با موفقیت ذخیره شد", Severity.Success);
|
||||
await ApplyFilter();
|
||||
}
|
||||
}
|
||||
|
||||
private void ViewMovements(InventoryItemDto item)
|
||||
{
|
||||
Navigation.NavigateTo($"{RouteConstance.InventoryMovements}?itemId={item.Id}&productName={item.ProductTitle}");
|
||||
}
|
||||
|
||||
private async Task ExportToExcel()
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new GetAllInventoryItemsRequest
|
||||
{
|
||||
Page = 1,
|
||||
PageSize = 1000,
|
||||
ProductType = _filterProductType,
|
||||
Search = _searchTerm ?? string.Empty
|
||||
};
|
||||
|
||||
if (_filterWarehouseId.HasValue)
|
||||
{
|
||||
request.WarehouseId = _filterWarehouseId.Value;
|
||||
}
|
||||
|
||||
var result = await InventoryContract.GetAllInventoryItemsAsync(request);
|
||||
|
||||
if (result?.Items == null || !result.Items.Any())
|
||||
{
|
||||
Snackbar.Add("دادهای برای خروجی وجود ندارد.", Severity.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("شناسه,محصول,انبار,موجودی,رزرو,قابل فروش,حد هشدار,وضعیت");
|
||||
|
||||
foreach (var i in result.Items)
|
||||
{
|
||||
sb.AppendLine(string.Join(",",
|
||||
i.Id,
|
||||
EscapeCsv(i.ProductTitle),
|
||||
EscapeCsv(i.WarehouseName),
|
||||
i.Quantity,
|
||||
i.ReservedQuantity,
|
||||
i.AvailableQuantity,
|
||||
i.LowStockThreshold,
|
||||
i.IsLowStock ? "کمبود" : "عادی"));
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
var filename = $"inventory-{DateTime.Now:yyyyMMddHHmmss}.csv";
|
||||
|
||||
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
|
||||
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private static string EscapeCsv(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return "";
|
||||
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
|
||||
return $"\"{value.Replace("\"", "\"\"")}\"";
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
@attribute [Route(RouteConstance.InventoryLowStock)]
|
||||
@attribute [Authorize]
|
||||
|
||||
@using CMSMicroservice.Protobuf.Protos.Inventory
|
||||
|
||||
<div>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-4">
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.h4">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Warning" Class="mr-2" />
|
||||
محصولات کمموجود
|
||||
</MudText>
|
||||
<MudText Typo="Typo.subtitle1" Color="Color.Secondary">
|
||||
لیست محصولاتی که موجودی آنها کمتر از حد هشدار است
|
||||
</MudText>
|
||||
</MudStack>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Secondary"
|
||||
StartIcon="@Icons.Material.Filled.ArrowBack"
|
||||
OnClick="GoBack">
|
||||
بازگشت به انبار
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
@* Filters Section *@
|
||||
<MudPaper Class="pa-4 mb-4" Elevation="2">
|
||||
<MudGrid>
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudSelect T="long?" @bind-Value="_filterWarehouseId"
|
||||
Label="انبار"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense"
|
||||
Clearable="true">
|
||||
@foreach (var wh in _warehouses)
|
||||
{
|
||||
<MudSelectItem T="long?" Value="@((long?)wh.Id)">@wh.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudSelect T="ProductType" @bind-Value="_filterProductType"
|
||||
Label="نوع محصول"
|
||||
Variant="Variant.Outlined"
|
||||
Margin="Margin.Dense">
|
||||
<MudSelectItem T="ProductType" Value="ProductType.Unspecified">همه</MudSelectItem>
|
||||
<MudSelectItem T="ProductType" Value="ProductType.RegularProduct">محصول عادی</MudSelectItem>
|
||||
<MudSelectItem T="ProductType" Value="ProductType.DiscountProduct">محصول تخفیفی</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="4" Class="d-flex align-end">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
OnClick="LoadData"
|
||||
StartIcon="@Icons.Material.Filled.Refresh"
|
||||
Class="mr-2">
|
||||
بارگذاری
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
||||
@* Stats *@
|
||||
<MudAlert Severity="Severity.Warning" Class="mb-4">
|
||||
<MudText>
|
||||
<strong>@_items.Count</strong> محصول کمموجود شناسایی شده است.
|
||||
لطفاً برای جلوگیری از کمبود موجودی، اقدام به سفارش مجدد کنید.
|
||||
</MudText>
|
||||
</MudAlert>
|
||||
|
||||
@if (_isLoading)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" Color="Color.Primary" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudDataGrid T="InventoryItemDto" Items="_items" Hover="true" Height="calc(100vh - 240px)">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Id" Title="شناسه" />
|
||||
|
||||
<PropertyColumn Property="x => x.ProductTitle" Title="نام محصول">
|
||||
<CellTemplate>
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.body2"><strong>@context.Item.ProductTitle</strong></MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||
@GetProductTypeLabel(context.Item.ProductType)
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.WarehouseName" Title="انبار">
|
||||
<CellTemplate>
|
||||
<MudChip T="string" Color="Color.Info" Size="Size.Small">
|
||||
@context.Item.WarehouseName
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.Quantity" Title="موجودی فعلی">
|
||||
<CellTemplate>
|
||||
<MudText Color="Color.Error"><strong>@context.Item.Quantity</strong></MudText>
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.AvailableQuantity" Title="قابل فروش">
|
||||
<CellTemplate>
|
||||
<MudText Color="Color.Warning">@context.Item.AvailableQuantity</MudText>
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.LowStockThreshold" Title="حد هشدار" />
|
||||
|
||||
<PropertyColumn Property="x => x.ReorderPoint" Title="نقطه سفارش" />
|
||||
|
||||
<TemplateColumn Title="کمبود">
|
||||
<CellTemplate>
|
||||
@{
|
||||
var shortage = context.Item.LowStockThreshold - context.Item.Quantity;
|
||||
}
|
||||
<MudChip T="string" Color="Color.Error" Size="Size.Small">
|
||||
@shortage عدد
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
|
||||
<TemplateColumn Title="عملیات" Sortable="false">
|
||||
<CellTemplate>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Success"
|
||||
Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="@(() => OpenAddStockDialog(context.Item))">
|
||||
ورود کالا
|
||||
</MudButton>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<NoRecordsContent>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
|
||||
</NoRecordsContent>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="InventoryItemDto"
|
||||
PageSizeOptions="@(new int[] { 20, 50, 100 })"
|
||||
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
|
||||
RowsPerPageString="تعداد در صفحه" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,121 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
using CMSMicroservice.Protobuf.Protos.Inventory;
|
||||
using BackOffice.Pages.Inventory.Components;
|
||||
using BackOffice.Common.Utilities;
|
||||
|
||||
namespace BackOffice.Pages.Inventory;
|
||||
|
||||
public partial class LowStockPage
|
||||
{
|
||||
[Inject] public InventoryContract.InventoryContractClient InventoryContract { get; set; } = default!;
|
||||
|
||||
private List<InventoryItemDto> _items = new();
|
||||
private List<WarehouseDto> _warehouses = new();
|
||||
private bool _isLoading = true;
|
||||
|
||||
// Filter fields
|
||||
private long? _filterWarehouseId;
|
||||
private ProductType _filterProductType = ProductType.Unspecified;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadWarehouses();
|
||||
await LoadData();
|
||||
}
|
||||
|
||||
private async Task LoadWarehouses()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await InventoryContract.GetAllWarehousesAsync(new GetAllWarehousesRequest
|
||||
{
|
||||
IsActive = true
|
||||
});
|
||||
_warehouses = response.Warehouses.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری انبارها: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadData()
|
||||
{
|
||||
_isLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
var request = new GetLowStockItemsRequest
|
||||
{
|
||||
Page = 1,
|
||||
PageSize = 200, // Get up to 200 low stock items
|
||||
ProductType = _filterProductType
|
||||
};
|
||||
|
||||
if (_filterWarehouseId.HasValue)
|
||||
{
|
||||
request.WarehouseId = _filterWarehouseId.Value;
|
||||
}
|
||||
|
||||
var response = await InventoryContract.GetLowStockItemsAsync(request);
|
||||
_items = response.Items.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری دادهها: {ex.Message}", Severity.Error);
|
||||
_items = new();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private string GetProductTypeLabel(ProductType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
ProductType.RegularProduct => "محصول عادی",
|
||||
ProductType.DiscountProduct => "محصول تخفیفی",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task OpenAddStockDialog(InventoryItemDto item)
|
||||
{
|
||||
var productId = item.ProductType == ProductType.RegularProduct
|
||||
? (item.ProductId ?? 0)
|
||||
: (item.DiscountProductId ?? 0);
|
||||
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
["Warehouses"] = _warehouses,
|
||||
["ProductIdParam"] = productId,
|
||||
["ProductTypeParam"] = item.ProductType,
|
||||
["ProductName"] = item.ProductTitle,
|
||||
["WarehouseIdParam"] = item.WarehouseId
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<AddStockDialog>($"ورود کالا - {item.ProductTitle}", parameters, new DialogOptions
|
||||
{
|
||||
CloseButton = true,
|
||||
MaxWidth = MaxWidth.Medium,
|
||||
FullWidth = true
|
||||
});
|
||||
|
||||
var result = await dialog.Result;
|
||||
if (!result!.Canceled)
|
||||
{
|
||||
Snackbar.Add("موجودی با موفقیت اضافه شد", Severity.Success);
|
||||
await LoadData();
|
||||
}
|
||||
}
|
||||
|
||||
private void GoBack()
|
||||
{
|
||||
Navigation.NavigateTo(RouteConstance.Inventory);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user