feat: Implement Inventory Management Pages
Build and Deploy / build (push) Successful in 2m18s

- Added InventoryMainPage for managing inventory items with filtering options.
- Created LowStockPage to display low stock items with filtering capabilities.
- Developed MovementsPage to track inventory movements with detailed filtering.
- Introduced WarehousesPage for managing warehouses, including creation and viewing inventory.
- Enhanced backend integration with InventoryBFFContract for data retrieval.
- Implemented user-friendly UI components using MudBlazor for better user experience.
- Added necessary dialog components for adding stock, adjusting inventory, and viewing movements.
This commit is contained in:
masoodafar-web
2026-01-03 08:18:55 +03:30
parent 81ee23e313
commit 2d833605b4
29 changed files with 2416 additions and 317 deletions
@@ -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;
}
}
+9 -7
View File
@@ -117,25 +117,27 @@
<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.DiscountCategory.Protobuf" Version="0.0.4"/>
<PackageReference Include="Foursat.BackOffice.BFF.DiscountOrder.Protobuf" Version="0.0.4"/>
<PackageReference Include="Foursat.BackOffice.BFF.DiscountProduct.Protobuf" Version="0.0.4"/>
<PackageReference Include="Foursat.BackOffice.BFF.DiscountShoppingCart.Protobuf" Version="0.0.4"/>
<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.ManualPayment.Protobuf" Version="0.0.14"/>
<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.ProductTag.Protobuf" Version="0.0.4"/>
<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.Tag.Protobuf" Version="0.0.5"/>
<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"/>
<PackageReference Include="Foursat.BackOffice.BFF.Inventory.Protobuf" Version="0.0.1"/>
<!-- 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" />-->
@@ -14,19 +14,21 @@ using Foursat.BackOffice.BFF.NetworkMembership.Protos;
using Foursat.BackOffice.BFF.Health.Protobuf;
using BackOffice.BFF.Configuration.Protobuf.Protos.AppVersion;
// TODO: Create these proto projects - temporarily disabled
// using BackOffice.BFF.DiscountProduct.Protobuf.Protos.DiscountProduct;
// 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 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.ManualPayment.Protobuf;
using Foursat.BackOffice.BFF.PublicMessage.Protobuf;
using Foursat.BackOffice.BFF.Inventory.Protos;
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 Blazored.LocalStorage;
using Grpc.Core;
using Grpc.Core.Interceptors;
@@ -37,9 +39,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 +69,13 @@ 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>();
return services;
}
@@ -120,18 +120,23 @@ 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 InventoryBFFContract.InventoryBFFContractClient(sp.GetRequiredService<CallInvoker>()));
return services;
}
@@ -17,4 +17,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/";
}
@@ -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>
@@ -229,7 +229,7 @@
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Payments"
Href="/commission/payouts">
مشاهده Payout ها
مشاهده پرداخت‌ها
</MudButton>
<MudButton Variant="Variant.Filled"
Color="Color.Secondary"
+1 -1
View File
@@ -99,7 +99,7 @@
<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>
<MudTd>@context.PaymentDate.ToDateTime().MiladiToJalali()</MudTd>
</RowTemplate>
</MudTable>
}
@@ -0,0 +1,178 @@
@using Foursat.BackOffice.BFF.Inventory.Protos
@using Google.Protobuf.WellKnownTypes
@using InventoryProductType = Foursat.BackOffice.BFF.Inventory.Protos.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 Value="InventoryProductType.Regular">محصول عادی</MudSelectItem>
<MudSelectItem Value="InventoryProductType.Discount">محصول تخفیفی</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 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 InventoryBFFContract.InventoryBFFContractClient 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.Regular;
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);
if (response.Success)
{
Snackbar.Add($"موجودی جدید: {response.NewQuantity} عدد", Severity.Success);
MudDialog.Close(DialogResult.Ok(true));
}
else
{
Snackbar.Add(response.Message ?? "خطا در ثبت موجودی", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_isSubmitting = false;
}
}
}
@@ -0,0 +1,142 @@
@using Foursat.BackOffice.BFF.Inventory.Protos
<MudDialog>
<DialogContent>
<MudForm @ref="_form" @bind-IsValid="_formValid">
<MudStack Spacing="4">
<MudAlert Severity="Severity.Info">
<MudStack Spacing="1">
<MudText>محصول: <strong>@Item.ProductName</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 InventoryBFFContract.InventoryBFFContractClient 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.Regular
? (Item.ProductId ?? 0)
: (Item.DiscountProductId ?? 0);
var request = new AdjustStockRequest
{
ProductId = productId,
ProductType = Item.ProductType,
NewQuantity = _newQuantity,
Note = _note ?? string.Empty
};
var response = await InventoryContract.AdjustStockAsync(request);
if (response.Success)
{
Snackbar.Add(response.Message ?? "موجودی با موفقیت تنظیم شد", Severity.Success);
MudDialog.Close(DialogResult.Ok(true));
}
else
{
Snackbar.Add(response.Message ?? "خطا در تنظیم موجودی", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_isSubmitting = false;
}
}
}
@@ -0,0 +1,112 @@
@using Foursat.BackOffice.BFF.Inventory.Protos
<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 InventoryBFFContract.InventoryBFFContractClient 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 Foursat.BackOffice.BFF.Inventory.Protos
<MudDialog>
<DialogContent>
<MudForm @ref="_form" @bind-IsValid="_formValid">
<MudStack Spacing="4">
<MudAlert Severity="Severity.Info">
<MudStack Spacing="1">
<MudText>محصول: <strong>@Item.ProductName</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 InventoryBFFContract.InventoryBFFContractClient 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
{
InventoryItemId = 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,123 @@
@using Foursat.BackOffice.BFF.Inventory.Protos
<MudDialog>
<DialogContent>
<MudForm @ref="_form" @bind-IsValid="_formValid">
<MudStack Spacing="4">
<MudAlert Severity="Severity.Warning">
<MudStack Spacing="1">
<MudText>محصول: <strong>@Item.ProductName</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="شماره گزارش یا سند داخلی" />
@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 InventoryBFFContract.InventoryBFFContractClient 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 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.Regular
? (Item.ProductId ?? 0)
: (Item.DiscountProductId ?? 0);
var request = new RecordLossRequest
{
ProductId = productId,
ProductType = Item.ProductType,
Quantity = _quantity,
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,216 @@
@attribute [Route(RouteConstance.Inventory)]
@attribute [Authorize]
@using Foursat.BackOffice.BFF.Inventory.Protos
@using BackOffice.Pages.Inventory.Components
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
<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 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 Value="ProductType.Unspecified">همه</MudSelectItem>
<MudSelectItem Value="ProductType.Regular">محصول عادی</MudSelectItem>
<MudSelectItem Value="ProductType.Discount">محصول تخفیفی</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="60vh">
<ToolBarContent>
<MudText Typo="Typo.h6">لیست موجودی انبار</MudText>
<MudSpacer />
<MudStack Row="true" Spacing="2">
<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.ProductName" Title="نام محصول">
<CellTemplate>
<MudStack Spacing="0">
<MudText Typo="Typo.body2"><strong>@context.Item.ProductName</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>
<PagerContent>
<MudDataGridPager T="InventoryItemDto"
PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد سطرهای صفحه" />
</PagerContent>
</MudDataGrid>
</MudContainer>
@@ -0,0 +1,250 @@
using Microsoft.AspNetCore.Components;
using MudBlazor;
using Google.Protobuf.WellKnownTypes;
using Foursat.BackOffice.BFF.Inventory.Protos;
using BackOffice.Pages.Inventory.Components;
using BackOffice.Common.Utilities;
namespace BackOffice.Pages.Inventory;
public partial class InventoryMainPage
{
[Inject] public InventoryBFFContract.InventoryBFFContractClient InventoryContract { 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
{
ActiveOnly = 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
{
Count = 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
{
PageIndex = state.Page + 1,
PageSize = state.PageSize,
ProductType = _filterProductType,
SearchTerm = _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.Regular => "محصول عادی",
ProductType.Discount => "محصول تخفیفی",
_ => "نامشخص"
};
}
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.Regular ? item.ProductId : item.DiscountProductId,
["ProductTypeParam"] = item.ProductType,
["ProductName"] = item.ProductName,
["WarehouseIdParam"] = item.WarehouseId
};
var dialog = await DialogService.ShowAsync<AddStockDialog>($"ورود کالا - {item.ProductName}", 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.ProductName}", 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.ProductName}", 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.ProductName}", 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.ProductName}");
}
}
@@ -0,0 +1,146 @@
@attribute [Route(RouteConstance.InventoryLowStock)]
@attribute [Authorize]
@using Foursat.BackOffice.BFF.Inventory.Protos
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
<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 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 Value="ProductType.Unspecified">همه</MudSelectItem>
<MudSelectItem Value="ProductType.Regular">محصول عادی</MudSelectItem>
<MudSelectItem Value="ProductType.Discount">محصول تخفیفی</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="LowStockItemDto" Items="_items" Hover="true" Height="60vh">
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<PropertyColumn Property="x => x.ProductName" Title="نام محصول">
<CellTemplate>
<MudStack Spacing="0">
<MudText Typo="Typo.body2"><strong>@context.Item.ProductName</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>
<PagerContent>
<MudDataGridPager T="LowStockItemDto"
PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد سطرهای صفحه" />
</PagerContent>
</MudDataGrid>
}
</MudContainer>
@@ -0,0 +1,120 @@
using Microsoft.AspNetCore.Components;
using MudBlazor;
using Foursat.BackOffice.BFF.Inventory.Protos;
using BackOffice.Pages.Inventory.Components;
using BackOffice.Common.Utilities;
namespace BackOffice.Pages.Inventory;
public partial class LowStockPage
{
[Inject] public InventoryBFFContract.InventoryBFFContractClient InventoryContract { get; set; } = default!;
private List<LowStockItemDto> _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
{
ActiveOnly = 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
{
Count = 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.Regular => "محصول عادی",
ProductType.Discount => "محصول تخفیفی",
_ => "نامشخص"
};
}
private async Task OpenAddStockDialog(LowStockItemDto item)
{
var productId = item.ProductType == ProductType.Regular
? (item.ProductId ?? 0)
: (item.DiscountProductId ?? 0);
var parameters = new DialogParameters
{
["Warehouses"] = _warehouses,
["ProductIdParam"] = productId,
["ProductTypeParam"] = item.ProductType,
["ProductName"] = item.ProductName,
["WarehouseIdParam"] = item.WarehouseId
};
var dialog = await DialogService.ShowAsync<AddStockDialog>($"ورود کالا - {item.ProductName}", 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);
}
}
@@ -0,0 +1,186 @@
@attribute [Route(RouteConstance.InventoryMovements)]
@attribute [Authorize]
@using Foursat.BackOffice.BFF.Inventory.Protos
@using Google.Protobuf.WellKnownTypes
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-4">
<MudStack Spacing="0">
<MudText Typo="Typo.h4">تاریخچه تغییرات موجودی</MudText>
@if (!string.IsNullOrEmpty(_productName))
{
<MudText Typo="Typo.subtitle1" Color="Color.Secondary">محصول: @_productName</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="3">
<MudSelect T="StockMovementType" @bind-Value="_filterMovementType"
Label="نوع تغییر"
Variant="Variant.Outlined"
Margin="Margin.Dense">
<MudSelectItem Value="StockMovementType.Unspecified">همه</MudSelectItem>
<MudSelectItem Value="StockMovementType.InitialStock">موجودی اولیه</MudSelectItem>
<MudSelectItem Value="StockMovementType.Restock">ورود کالا</MudSelectItem>
<MudSelectItem Value="StockMovementType.Return">برگشت از مشتری</MudSelectItem>
<MudSelectItem Value="StockMovementType.Sale">فروش</MudSelectItem>
<MudSelectItem Value="StockMovementType.AdjustmentIncrease">افزایش موجودی</MudSelectItem>
<MudSelectItem Value="StockMovementType.AdjustmentDecrease">کاهش موجودی</MudSelectItem>
<MudSelectItem Value="StockMovementType.Reserved">رزرو</MudSelectItem>
<MudSelectItem Value="StockMovementType.Released">آزادسازی رزرو</MudSelectItem>
<MudSelectItem Value="StockMovementType.Loss">خسارت</MudSelectItem>
<MudSelectItem Value="StockMovementType.Damaged">آسیب‌دیده</MudSelectItem>
<MudSelectItem Value="StockMovementType.Expired">منقضی</MudSelectItem>
<MudSelectItem Value="StockMovementType.TransferOut">انتقال به بیرون</MudSelectItem>
<MudSelectItem Value="StockMovementType.TransferIn">انتقال به داخل</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="ProductType" @bind-Value="_filterProductType"
Label="نوع محصول"
Variant="Variant.Outlined"
Margin="Margin.Dense">
<MudSelectItem Value="ProductType.Unspecified">همه</MudSelectItem>
<MudSelectItem Value="ProductType.Regular">محصول عادی</MudSelectItem>
<MudSelectItem Value="ProductType.Discount">محصول تخفیفی</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudDatePicker @bind-Date="_filterFromDate"
Label="از تاریخ"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Clearable="true" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudDatePicker @bind-Date="_filterToDate"
Label="تا تاریخ"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Clearable="true" />
</MudItem>
<MudItem xs="12" Class="d-flex justify-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>
@* Data Grid *@
<MudDataGrid T="StockMovementDto"
ServerData="@(new Func<GridState<StockMovementDto>, Task<GridData<StockMovementDto>>>(ServerReload))"
Hover="true"
@ref="_gridData"
Height="65vh">
<ToolBarContent>
<MudText Typo="Typo.h6">تغییرات موجودی</MudText>
<MudSpacer />
<MudText Typo="Typo.body2" Color="Color.Secondary">
کل: @_totalCount ردیف
</MudText>
</ToolBarContent>
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<PropertyColumn Property="x => x.ProductName" Title="محصول" />
<PropertyColumn Property="x => x.MovementTypeName" Title="نوع تغییر">
<CellTemplate>
<MudChip T="string"
Color="@GetMovementColor(context.Item.MovementType)"
Size="Size.Small">
@context.Item.MovementTypeName
</MudChip>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.Quantity" Title="تعداد">
<CellTemplate>
<MudText Color="@(IsIncreaseMovement(context.Item.MovementType) ? Color.Success : Color.Error)">
@(IsIncreaseMovement(context.Item.MovementType) ? "+" : "-")@Math.Abs(context.Item.Quantity)
</MudText>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.QuantityBefore" Title="قبل" />
<PropertyColumn Property="x => x.QuantityAfter" Title="بعد" />
<PropertyColumn Property="x => x.ReferenceNumber" Title="شماره مرجع">
<CellTemplate>
@if (!string.IsNullOrEmpty(context.Item.ReferenceNumber))
{
<MudText Typo="Typo.caption">@context.Item.ReferenceNumber</MudText>
}
else
{
<MudText Typo="Typo.caption" Color="Color.Secondary">-</MudText>
}
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.PerformedByUserName" Title="کاربر">
<CellTemplate>
@if (!string.IsNullOrEmpty(context.Item.PerformedByUserName))
{
<MudText Typo="Typo.caption">@context.Item.PerformedByUserName</MudText>
}
else
{
<MudText Typo="Typo.caption" Color="Color.Secondary">سیستم</MudText>
}
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.CreatedAt" Title="تاریخ">
<CellTemplate>
@if (context.Item.CreatedAt != null)
{
<MudText Typo="Typo.caption">
@context.Item.CreatedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm")
</MudText>
}
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.Note" Title="توضیحات">
<CellTemplate>
@if (!string.IsNullOrEmpty(context.Item.Note))
{
<MudTooltip Text="@context.Item.Note">
<MudText Typo="Typo.caption" Style="max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
@context.Item.Note
</MudText>
</MudTooltip>
}
</CellTemplate>
</PropertyColumn>
</Columns>
<PagerContent>
<MudDataGridPager T="StockMovementDto"
PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد سطرهای صفحه" />
</PagerContent>
</MudDataGrid>
</MudContainer>
@@ -0,0 +1,132 @@
using Microsoft.AspNetCore.Components;
using MudBlazor;
using Google.Protobuf.WellKnownTypes;
using Foursat.BackOffice.BFF.Inventory.Protos;
using BackOffice.Common.Utilities;
namespace BackOffice.Pages.Inventory;
public partial class MovementsPage
{
[Inject] public InventoryBFFContract.InventoryBFFContractClient InventoryContract { get; set; } = default!;
[SupplyParameterFromQuery(Name = "itemId")]
public long? ItemId { get; set; }
[SupplyParameterFromQuery(Name = "productName")]
public string? _productName { get; set; }
private MudDataGrid<StockMovementDto>? _gridData;
private int _totalCount;
// Filter fields
private StockMovementType _filterMovementType = StockMovementType.Unspecified;
private ProductType _filterProductType = ProductType.Unspecified;
private DateTime? _filterFromDate;
private DateTime? _filterToDate;
private async Task<GridData<StockMovementDto>> ServerReload(GridState<StockMovementDto> state)
{
try
{
var request = new GetStockMovementsRequest
{
PageIndex = state.Page + 1,
PageSize = state.PageSize,
MovementType = _filterMovementType,
ProductType = _filterProductType
};
if (ItemId.HasValue)
{
request.InventoryItemId = ItemId.Value;
}
if (_filterFromDate.HasValue)
{
request.FromDate = Timestamp.FromDateTime(_filterFromDate.Value.ToUniversalTime());
}
if (_filterToDate.HasValue)
{
request.ToDate = Timestamp.FromDateTime(_filterToDate.Value.ToUniversalTime());
}
var result = await InventoryContract.GetStockMovementsAsync(request);
if (result?.Movements != null)
{
_totalCount = result.TotalCount;
return new GridData<StockMovementDto>
{
Items = result.Movements.ToList(),
TotalItems = result.TotalCount
};
}
return new GridData<StockMovementDto>();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری داده‌ها: {ex.Message}", Severity.Error);
return new GridData<StockMovementDto>();
}
}
private Color GetMovementColor(StockMovementType type)
{
return type switch
{
StockMovementType.InitialStock => Color.Info,
StockMovementType.Restock => Color.Success,
StockMovementType.Return => Color.Success,
StockMovementType.Sale => Color.Primary,
StockMovementType.AdjustmentIncrease => Color.Success,
StockMovementType.AdjustmentDecrease => Color.Warning,
StockMovementType.Reserved => Color.Secondary,
StockMovementType.Released => Color.Tertiary,
StockMovementType.Loss => Color.Error,
StockMovementType.Damaged => Color.Error,
StockMovementType.Expired => Color.Error,
StockMovementType.TransferOut => Color.Warning,
StockMovementType.TransferIn => Color.Info,
_ => Color.Default
};
}
private bool IsIncreaseMovement(StockMovementType type)
{
return type switch
{
StockMovementType.InitialStock => true,
StockMovementType.Restock => true,
StockMovementType.Return => true,
StockMovementType.AdjustmentIncrease => true,
StockMovementType.Released => true,
StockMovementType.TransferIn => true,
_ => false
};
}
private async Task ApplyFilter()
{
if (_gridData != null)
{
await _gridData.ReloadServerData();
}
}
private async Task ClearFilter()
{
_filterMovementType = StockMovementType.Unspecified;
_filterProductType = ProductType.Unspecified;
_filterFromDate = null;
_filterToDate = null;
await ApplyFilter();
}
private void GoBack()
{
Navigation.NavigateTo(RouteConstance.Inventory);
}
}
@@ -0,0 +1,123 @@
@attribute [Route(RouteConstance.InventoryWarehouses)]
@attribute [Authorize]
@using Foursat.BackOffice.BFF.Inventory.Protos
@using BackOffice.Pages.Inventory.Components
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-4">
<MudStack Spacing="0">
<MudText Typo="Typo.h4">
<MudIcon Icon="@Icons.Material.Filled.Warehouse" Color="Color.Primary" Class="mr-2" />
مدیریت انبارها
</MudText>
<MudText Typo="Typo.subtitle1" Color="Color.Secondary">
تعریف و مدیریت انبارهای سیستم
</MudText>
</MudStack>
<MudStack Row="true" Spacing="2">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="OpenCreateWarehouseDialog">
انبار جدید
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.ArrowBack"
OnClick="GoBack">
بازگشت به انبار
</MudButton>
</MudStack>
</MudStack>
@* Filter Toggle *@
<MudPaper Class="pa-4 mb-4" Elevation="2">
<MudSwitch T="bool" @bind-Value="_showActiveOnly"
Color="Color.Primary"
Label="فقط انبارهای فعال"
LabelPosition="LabelPosition.Start" />
<MudButton Variant="Variant.Text"
Color="Color.Primary"
OnClick="LoadWarehouses"
StartIcon="@Icons.Material.Filled.Refresh"
Class="ml-4">
بارگذاری مجدد
</MudButton>
</MudPaper>
@if (_isLoading)
{
<MudProgressLinear Indeterminate="true" Color="Color.Primary" />
}
else
{
<MudGrid>
@foreach (var warehouse in _warehouses)
{
<MudItem xs="12" sm="6" md="4">
<MudCard Elevation="3" Class="@(warehouse.IsDefault ? "border-2 border-solid mud-border-primary" : "")">
<MudCardHeader>
<CardHeaderAvatar>
<MudAvatar Color="@(warehouse.IsActive ? Color.Success : Color.Default)" Size="Size.Large">
<MudIcon Icon="@Icons.Material.Filled.Warehouse" />
</MudAvatar>
</CardHeaderAvatar>
<CardHeaderContent>
<MudText Typo="Typo.h6">
@warehouse.Name
@if (warehouse.IsDefault)
{
<MudChip T="string" Color="Color.Primary" Size="Size.Small" Class="ml-2">پیش‌فرض</MudChip>
}
</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">
کد: @warehouse.Code
</MudText>
</CardHeaderContent>
<CardHeaderActions>
<MudChip T="string" Color="@(warehouse.IsActive ? Color.Success : Color.Error)" Size="Size.Small">
@(warehouse.IsActive ? "فعال" : "غیرفعال")
</MudChip>
</CardHeaderActions>
</MudCardHeader>
<MudCardContent>
<MudStack Spacing="2">
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.LocationOn" Size="Size.Small" Color="Color.Secondary" />
<MudText Typo="Typo.body2">
@(string.IsNullOrEmpty(warehouse.Address) ? "آدرس ثبت نشده" : warehouse.Address)
</MudText>
</MudStack>
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.CalendarToday" Size="Size.Small" Color="Color.Secondary" />
<MudText Typo="Typo.caption" Color="Color.Secondary">
ایجاد: @(warehouse.Created?.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd") ?? "-")
</MudText>
</MudStack>
</MudStack>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Text"
Color="Color.Primary"
Size="Size.Small"
StartIcon="@Icons.Material.Filled.Visibility"
OnClick="@(() => ViewWarehouseInventory(warehouse))">
مشاهده موجودی
</MudButton>
</MudCardActions>
</MudCard>
</MudItem>
}
@if (_warehouses.Count == 0)
{
<MudItem xs="12">
<MudAlert Severity="Severity.Info">
هیچ انباری یافت نشد. برای شروع، یک انبار جدید ایجاد کنید.
</MudAlert>
</MudItem>
}
</MudGrid>
}
</MudContainer>
@@ -0,0 +1,73 @@
using Microsoft.AspNetCore.Components;
using MudBlazor;
using Foursat.BackOffice.BFF.Inventory.Protos;
using BackOffice.Pages.Inventory.Components;
using BackOffice.Common.Utilities;
namespace BackOffice.Pages.Inventory;
public partial class WarehousesPage
{
[Inject] public InventoryBFFContract.InventoryBFFContractClient InventoryContract { get; set; } = default!;
private List<WarehouseDto> _warehouses = new();
private bool _isLoading = true;
private bool _showActiveOnly = true;
protected override async Task OnInitializedAsync()
{
await LoadWarehouses();
}
private async Task LoadWarehouses()
{
_isLoading = true;
StateHasChanged();
try
{
var response = await InventoryContract.GetAllWarehousesAsync(new GetAllWarehousesRequest
{
ActiveOnly = _showActiveOnly
});
_warehouses = response.Warehouses.ToList();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری انبارها: {ex.Message}", Severity.Error);
_warehouses = new();
}
finally
{
_isLoading = false;
StateHasChanged();
}
}
private async Task OpenCreateWarehouseDialog()
{
var dialog = await DialogService.ShowAsync<CreateWarehouseDialog>("ایجاد انبار جدید", new DialogOptions
{
CloseButton = true,
MaxWidth = MaxWidth.Small,
FullWidth = true
});
var result = await dialog.Result;
if (!result!.Canceled)
{
Snackbar.Add("انبار با موفقیت ایجاد شد", Severity.Success);
await LoadWarehouses();
}
}
private void ViewWarehouseInventory(WarehouseDto warehouse)
{
Navigation.NavigateTo($"{RouteConstance.Inventory}?warehouseId={warehouse.Id}");
}
private void GoBack()
{
Navigation.NavigateTo(RouteConstance.Inventory);
}
}
@@ -366,7 +366,7 @@
Color="Color.Info"
StartIcon="@Icons.Material.Filled.Money"
OnClick="@(() => NavigationManager.NavigateTo($"/commission/payouts?userId={UserId}"))">
Payout های کاربر
پرداخت‌های کاربر
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Secondary"
@@ -1,46 +1,86 @@
@using BackOffice.BFF.ManualPayment.Protobuf
@using BackOffice.Pages.Payment.Components
@using BackOffice.Pages.AutoComplete
@using Microsoft.AspNetCore.Components.Forms
<MudDialog>
<DialogContent>
@if (Mode == ManualPaymentDialogMode.Create)
{
<MudStack Spacing="2">
<MudText Typo="Typo.h6">ثبت پرداخت دستی جدید</MudText>
<MudText Typo="Typo.h6">ثبت فعالسازی دستی جدید</MudText>
<MudNumericField T="long"
Label="شناسه کاربر"
Variant="Variant.Outlined"
@bind-Value="_createModel.UserId" />
<UserAutoComplete Label="انتخاب کاربر"
@bind-SelectedUserId="_selectedUserId" />
<MudNumericField T="long"
Label="مبلغ"
Variant="Variant.Outlined"
Adornment="Adornment.End"
AdornmentText="ریال"
AdornmentText="تومان"
Disabled="true"
@bind-Value="_createModel.Amount" />
<MudTextField T="string"
Label="نوع / توضیح نوع"
Variant="Variant.Outlined"
@bind-Value="_createModel.TypeDisplay" />
<MudSelect T="int"
Label="نوع پرداخت"
Variant="Variant.Outlined"
@bind-Value="_selectedType">
<MudSelectItem Value="1">واریز نقدی</MudSelectItem>
<MudSelectItem Value="2">شارژ کیف پول تخفیف</MudSelectItem>
<MudSelectItem Value="3">شارژ کیف پول شبکه</MudSelectItem>
<MudSelectItem Value="4">تسویه حساب</MudSelectItem>
<MudSelectItem Value="5">اصلاح خطا</MudSelectItem>
<MudSelectItem Value="6">بازپرداخت</MudSelectItem>
<MudSelectItem Value="99">سایر</MudSelectItem>
</MudSelect>
<MudTextField T="string"
Label="توضیحات"
Variant="Variant.Outlined"
Lines="3"
@bind-Value="_createModel.Description" />
Required="true"
RequiredError="توضیحات الزامی است"
@bind-Value="_description" />
<MudTextField T="string"
Label="شماره مرجع (اختیاری)"
Variant="Variant.Outlined"
@bind-Value="_createModel.ReferenceNumber" />
<MudText Typo="Typo.subtitle2">تصویر فیش پرداخت (اختیاری)</MudText>
<MudFileUpload T="IBrowserFile"
FilesChanged="OnImageFileSelected"
Accept=".jpg,.jpeg,.png,.gif,.webp"
MaximumFileCount="1">
<ActivatorContent>
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.CloudUpload">
انتخاب تصویر
</MudButton>
</ActivatorContent>
</MudFileUpload>
@if (!string.IsNullOrWhiteSpace(_imagePreview))
{
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudImage Src="@_imagePreview"
Alt="پیش‌نمایش فیش"
Width="150"
Height="150"
ObjectFit="ObjectFit.Cover"
Class="rounded" />
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Color="Color.Error"
OnClick="RemoveImage" />
</MudStack>
}
</MudStack>
}
else if (Model is not null)
{
<MudStack Spacing="2">
<MudText Typo="Typo.h6">جزئیات پرداخت دستی</MudText>
<MudText Typo="Typo.h6">جزئیات فعالسازی دستی</MudText>
<MudText Typo="Typo.body2">شناسه: @Model.Id</MudText>
<MudText Typo="Typo.body2">کاربر: @Model.UserFullName (@Model.UserId)</MudText>
<MudText Typo="Typo.body2">مبلغ: @Model.Amount.ToString("N0") ریال</MudText>
@@ -52,6 +92,16 @@
<MudText Typo="Typo.body2">شماره مرجع: @Model.ReferenceNumber</MudText>
}
@if (!string.IsNullOrWhiteSpace(Model.ImagePath))
{
<MudText Typo="Typo.body2">تصویر فیش:</MudText>
<MudImage Src="@Model.ImagePath"
Alt="تصویر فیش"
Width="200"
ObjectFit="ObjectFit.Contain"
Class="rounded" />
}
<MudText Typo="Typo.body2">توضیحات: @Model.Description</MudText>
@if (!string.IsNullOrWhiteSpace(Model.RejectionReason))
@@ -79,7 +129,11 @@
@if (Mode == ManualPaymentDialogMode.Create)
{
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="CreateAsync">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="CreateAsync" Disabled="@_isSubmitting">
@if (_isSubmitting)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="me-2" />
}
ثبت
</MudButton>
}
@@ -1,5 +1,7 @@
using BackOffice.BFF.ManualPayment.Protobuf;
using Google.Protobuf;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using MudBlazor;
namespace BackOffice.Pages.Payment.Components;
@@ -27,19 +29,81 @@ public partial class ManualPaymentDialog
[Parameter] public ManualPaymentDialogMode Mode { get; set; }
[Parameter] public ManualPaymentModel? Model { get; set; }
private ManualPaymentModel _createModel = new();
private ManualPaymentModel _createModel = new(){
Amount = 56000000,
};
private string? _adminNote;
// User selection
private long? _selectedUserId;
// Type and Description
private int _selectedType = 1; // Default: CashDeposit
private string _description = "عضویت دستی باشگاه مشتریان";
// Image upload
private IBrowserFile? _imageFile;
private string? _imagePreview;
private const long MaxFileSize = 10 * 1024 * 1024; // 10MB
private bool _isSubmitting;
private async Task OnImageFileSelected(IBrowserFile? file)
{
if (file == null)
{
RemoveImage();
return;
}
_imageFile = file;
// Create preview
var buffer = new byte[file.Size];
await file.OpenReadStream(MaxFileSize).ReadAsync(buffer);
_imagePreview = $"data:{file.ContentType};base64,{Convert.ToBase64String(buffer)}";
}
private void RemoveImage()
{
_imageFile = null;
_imagePreview = null;
}
private async Task CreateAsync()
{
if (_isSubmitting) return;
// Validation
if (!_selectedUserId.HasValue || _selectedUserId.Value <= 0)
{
Snackbar.Add("لطفاً کاربر را انتخاب کنید.", Severity.Warning);
return;
}
if (_selectedType <= 0)
{
Snackbar.Add("لطفاً نوع پرداخت را انتخاب کنید.", Severity.Warning);
return;
}
if (string.IsNullOrWhiteSpace(_description))
{
Snackbar.Add("لطفاً توضیحات را وارد کنید.", Severity.Warning);
return;
}
_isSubmitting = true;
StateHasChanged();
try
{
var request = new CreateManualPaymentRequest
{
UserId = _createModel.UserId,
UserId = _selectedUserId.Value,
Amount = _createModel.Amount,
Type = _createModel.Type,
Description = _createModel.Description
Type = _selectedType,
Description = _description
};
if (!string.IsNullOrWhiteSpace(_createModel.ReferenceNumber))
@@ -47,6 +111,20 @@ public partial class ManualPaymentDialog
request.ReferenceNumber = _createModel.ReferenceNumber;
}
// Upload image file
if (_imageFile != null)
{
var buffer = new byte[_imageFile.Size];
await _imageFile.OpenReadStream(MaxFileSize).ReadAsync(buffer);
request.ImageFile = new FileUploadModel
{
File = ByteString.CopyFrom(buffer),
FileName = _imageFile.Name,
Mime = _imageFile.ContentType
};
}
await ManualPaymentClient.CreateManualPaymentAsync(request);
Snackbar.Add("پرداخت دستی با موفقیت ثبت شد.", Severity.Success);
MudDialog.Close(DialogResult.Ok(true));
@@ -55,6 +133,11 @@ public partial class ManualPaymentDialog
{
Snackbar.Add($"خطا در ثبت پرداخت دستی: {ex.Message}", Severity.Error);
}
finally
{
_isSubmitting = false;
StateHasChanged();
}
}
private async Task ApproveAsync()
@@ -45,11 +45,11 @@
<Content>
<MudPaper Class="pa-2 mb-2">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.subtitle1">پرداخت‌های دستی</MudText>
<MudText Typo="Typo.subtitle1">فعالسازی (پرداخت) دستی</MudText>
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
OnClick="OpenCreateDialog">
ثبت پرداخت دستی جدید
ثبت فعالسازی دستی جدید
</MudButton>
</MudStack>
</MudPaper>
@@ -77,7 +77,7 @@
</Filters>
<Content>
<MudGrid Class="mb-2">
<MudItem xs="12" md="4">
@* <MudItem xs="12" md="4">
<MudCard>
<MudCardContent>
<MudText Typo="Typo.subtitle2">جمع سفارش‌ها در بازه فعلی</MudText>
@@ -107,7 +107,7 @@
}
</MudCardContent>
</MudCard>
</MudItem>
</MudItem> *@
</MudGrid>
<MudDataGrid T="DataModel"
@@ -127,7 +127,7 @@
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه"/>
<PropertyColumn Property="x => x.Amount" Title="مبلغ"/>
<PropertyColumn Property="x => x.VatAmount" Title="مبلغ VAT">
@* <PropertyColumn Property="x => x.VatAmount" Title="مبلغ VAT">
<CellTemplate>
@(context.Item.VatAmount > 0 ? context.Item.VatAmount.ToString("N0") : "-")
</CellTemplate>
@@ -136,7 +136,7 @@
<CellTemplate>
@(context.Item.VatPercentage > 0 ? $"{context.Item.VatPercentage:0}٪" : "-")
</CellTemplate>
</PropertyColumn>
</PropertyColumn> *@
<PropertyColumn Property="x => x.UserFullName" Title="نام کاربر"/>
<PropertyColumn Property="x => x.UserNationalCode" Title="کدملی"/>
<TemplateColumn Title="روش پرداخت">
+35 -17
View File
@@ -81,7 +81,16 @@
آمار باشگاه
</MudNavLink>
</MudNavGroup>
@if (CanManagePayments)
{
<MudNavGroup Title="پرداخت‌ها و فعالسازی‌ها" Icon="@Icons.Material.Filled.Payment" Expanded="false">
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/payment/manual-payments"
Icon="@Icons.Material.Filled.Payments">
فعالسازی (پرداخت) دستی
</MudNavLink>
</MudNavGroup>
}
<MudDivider Class="my-2" />
<MudText Class="nav-menu__title" Typo="Typo.subtitle2">مدیریت</MudText>
@@ -106,7 +115,7 @@
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/products/bulk-edit"
Href="@(RouteConstance.ProductsBulkEdit)"
Icon="@Icons.Material.Filled.EditNote">
ویرایش دسته‌جمعی
</MudNavLink>
@@ -129,6 +138,29 @@
مدیریت تگ‌ها
</MudNavLink>
</MudNavGroup>
<MudNavGroup Title="انبارداری" Icon="@Icons.Material.Filled.Warehouse" Expanded="false">
<MudNavLink Match="NavLinkMatch.Prefix"
Href="@(RouteConstance.Inventory)"
Icon="@Icons.Material.Filled.Inventory">
موجودی انبار
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="@(RouteConstance.InventoryLowStock)"
Icon="@Icons.Material.Filled.Warning">
محصولات کم‌موجود
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="@(RouteConstance.InventoryWarehouses)"
Icon="@Icons.Material.Filled.Store">
مدیریت انبارها
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="@(RouteConstance.InventoryMovements)"
Icon="@Icons.Material.Filled.History">
تاریخچه تغییرات
</MudNavLink>
</MudNavGroup>
}
@if (CanViewOrders)
@@ -158,21 +190,7 @@
</MudNavLink>
}
@if (CanManagePayments)
{
<MudNavGroup Title="پرداخت‌ها" Icon="@Icons.Material.Filled.Payment" Expanded="false">
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/payment/manual-payments"
Icon="@Icons.Material.Filled.Payments">
پرداخت‌های دستی
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/payment/membership"
Icon="@Icons.Material.Filled.CardMembership">
پرداخت دستی عضویت
</MudNavLink>
</MudNavGroup>
}
</Authorized>
</AuthorizeView>