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
+164
View File
@@ -0,0 +1,164 @@
# فیچر فعالسازی (پرداخت) دستی باشگاه مشتریان
## خلاصه
این فیچر امکان فعالسازی دستی عضویت باشگاه مشتریان را برای ادمین فراهم می‌کند. ادمین می‌تواند کاربر را انتخاب کرده، تصویر فیش پرداخت را آپلود کند و عضویت را فعال کند.
## تاریخ: 2 ژانویه 2026
---
## تغییرات انجام شده
### 1. CMS (Backend)
#### Entity - `ManualPayment.cs`
- اضافه شدن فیلد `ImageDocumentId` برای ذخیره شناسه سند در FMS
```csharp
public long? ImageDocumentId { get; set; }
```
#### Proto - `manualpayment.proto`
- اضافه شدن `image_document_id` به `CreateManualPaymentRequest` (فیلد 7)
- اضافه شدن `image_document_id` به `ManualPaymentModel` (فیلد 21)
#### Command - `CreateManualPaymentCommand.cs`
- اضافه شدن پراپرتی `ImageDocumentId`
#### Handler - `CreateManualPaymentCommandHandler.cs`
- ذخیره `ImageDocumentId` از request در entity
---
### 2. BFF (Backend For Frontend)
#### Proto - `manualpayment.proto`
- اضافه شدن `image_document_id` به `ManualPaymentModel` (فیلد 21)
- اضافه شدن `FileUploadModel` برای آپلود فایل
- اضافه شدن `image_file` به `CreateManualPaymentRequest`
#### Command - `CreateManualPaymentCommand.cs`
- اضافه شدن `FileUploadDto` برای دریافت فایل از فرانت
#### Handler - `CreateManualPaymentCommandHandler.cs`
- اتصال به FMS برای آپلود فایل
- استخراج `ImagePath` و `ImageDocumentId` از پاسخ FMS
- ارسال هر دو به CMS
```csharp
var fileInfo = await _context.FileInfos.CreateNewFileInfoAsync(new()
{
Directory = "Images/ManualPayments",
IsBase64 = false,
MIME = request.ImageFile.Mime,
FileName = request.ImageFile.FileName,
File = ByteString.CopyFrom(request.ImageFile.File)
}, cancellationToken: cancellationToken);
if (fileInfo != null)
{
if (!string.IsNullOrWhiteSpace(fileInfo.File))
grpcRequest.ImagePath = fileInfo.File;
if (fileInfo.Id > 0)
grpcRequest.ImageDocumentId = fileInfo.Id;
}
```
#### Query Handler - `GetManualPaymentsQueryHandler.cs`
- اضافه شدن mapping برای `ImagePath` و `ImageDocumentId`
#### DTO - `GetManualPaymentsResponseDto.cs`
- اضافه شدن فیلدهای:
```csharp
public string? ImagePath { get; set; }
public long? ImageDocumentId { get; set; }
```
#### Mapping - `ManualPaymentProfile.cs`
- اضافه شدن mapping برای `FileUploadModel -> FileUploadDto`
---
### 3. Frontend (Blazor)
#### `ManualPaymentDialog.razor`
- استفاده از `UserAutoComplete` برای انتخاب کاربر
- استفاده از `MudFileUpload` برای آپلود تصویر فیش
- استفاده از `MudSelect` برای انتخاب نوع پرداخت
- پیش‌نمایش تصویر قبل از ارسال
- تغییر عنوان‌ها از "پرداخت دستی" به "فعالسازی دستی"
#### `ManualPaymentDialog.razor.cs`
- هندل کردن انتخاب فایل با `IBrowserFile`
- تبدیل فایل به Base64 برای پیش‌نمایش
- ایجاد `FileUploadModel` برای ارسال به BFF
- مقادیر پیش‌فرض:
- `Type = 1` (واریز نقدی)
- `Description = "عضویت دستی باشگاه مشتریان"`
#### `ManualPayments.razor`
- تغییر عنوان صفحه به "فعالسازی (پرداخت) دستی"
- تغییر دکمه به "ثبت فعالسازی دستی جدید"
#### `NavMenu.razor`
- حذف آیتم منوی "پرداخت دستی عضویت"
- تغییر نام "پرداخت‌های دستی" به "فعالسازی (پرداخت) دستی"
#### حذف شده
- `ManualMembershipPayment.razor` و `ManualMembershipPayment.razor.cs`
---
## مقادیر ثابت
```csharp
// مبلغ پایه پکیج: 56 میلیون ریال
SystemConstants.BasePackageAmount = 56_000_000;
// شارژ کیف پول:
// - مجموع شارژ: 56M ریال
```
---
## فلو کامل
1. ادمین کاربر را با `UserAutoComplete` انتخاب می‌کند
2. نوع پرداخت را انتخاب می‌کند (پیش‌فرض: واریز نقدی)
3. توضیحات را وارد می‌کند (پیش‌فرض: عضویت دستی باشگاه مشتریان)
4. تصویر فیش را آپلود می‌کند (اختیاری)
5. دکمه ثبت را می‌زند
6. Frontend فایل را به BFF ارسال می‌کند
7. BFF فایل را به FMS آپلود می‌کند
8. FMS مسیر فایل (`File`) و شناسه سند (`Id`) را برمی‌گرداند
9. BFF هر دو را به CMS ارسال می‌کند
10. CMS تراکنش، پرداخت دستی و لاگ کیف پول را ثبت می‌کند
11. کیف پول کاربر شارژ می‌شود
---
## فایل‌های تغییر یافته
### CMS
- `CMSMicroservice.Domain/Entities/Payment/ManualPayment.cs`
- `CMSMicroservice.Application/ManualPaymentCQ/Commands/CreateManualPayment/CreateManualPaymentCommand.cs`
- `CMSMicroservice.Application/ManualPaymentCQ/Commands/CreateManualPayment/CreateManualPaymentCommandHandler.cs`
- `Protos/manualpayment.proto`
### BFF
- `BackOffice.BFF.Application/ManualPaymentCQ/Commands/CreateManualPayment/CreateManualPaymentCommand.cs`
- `BackOffice.BFF.Application/ManualPaymentCQ/Commands/CreateManualPayment/CreateManualPaymentCommandHandler.cs`
- `BackOffice.BFF.Application/ManualPaymentCQ/Queries/GetManualPayments/GetManualPaymentsQueryHandler.cs`
- `BackOffice.BFF.Application/ManualPaymentCQ/Queries/GetManualPayments/GetManualPaymentsResponseDto.cs`
- `BackOffice.BFF.Application/ManualPaymentCQ/ManualPaymentProfile.cs`
- `Protobufs/BackOffice.BFF.ManualPayment.Protobuf/Protos/manualpayment.proto`
### Frontend
- `BackOffice/Pages/Payment/Components/ManualPaymentDialog.razor`
- `BackOffice/Pages/Payment/Components/ManualPaymentDialog.razor.cs`
- `BackOffice/Pages/Payment/ManualPayments.razor`
- `BackOffice/Shared/NavMenu.razor`
### حذف شده
- `BackOffice.Main/Pages/Payment/ManualMembershipPayment.razor`
- `BackOffice.Main/Pages/Payment/ManualMembershipPayment.razor.cs`
+69 -3
View File
@@ -1,12 +1,78 @@
# کارهای باقیمانده - BackOffice
> آخرین بروزرسانی: December 20, 2025
> آخرین بروزرسانی: January 1, 2026
## وضعیت کلی
**Build Status**: ✅ SUCCESS (0 Errors)
**Enabled Modules**: 10+ ماژول کامل
**Remaining Tasks**: فقط Backend Implementation
**Enabled Modules**: 12+ ماژول کامل
**System Status**: **PRODUCTION READY** 🚀
---
## ✅ کارهای انجام شده - Session January 1, 2026
### فعال‌سازی ماژول‌های فروشگاه تخفیفی (DiscountShop Frontend)
**وضعیت**: ✅ COMPLETED - همه چیز فعال و build موفق
**فایل اصلی تغییر یافته**:
- `BackOffice/Common/Configure/ConfigureService.cs`
**تغییرات**:
#### 1. Using Statements فعال شدند:
```csharp
// 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 Foursat.BackOffice.BFF.PublicMessage.Protobuf;
// Application Services
using BackOffice.Services.DiscountProduct;
using BackOffice.Services.DiscountCategory;
using BackOffice.Services.DiscountOrder;
using BackOffice.Services.PublicMessage;
using BackOffice.Services.Tag;
```
#### 2. gRPC Clients فعال شدند:
```csharp
// Discount Shop Services
services.AddTransient(sp => new DiscountProductContract.DiscountProductContractClient(...));
services.AddTransient(sp => new DiscountCategoryContract.DiscountCategoryContractClient(...));
services.AddTransient(sp => new DiscountOrderContract.DiscountOrderContractClient(...));
// Public Message Service
services.AddTransient(sp => new PublicMessageContract.PublicMessageContractClient(...));
// Tag Management Services
services.AddTransient(sp => new TagContract.TagContractClient(...));
services.AddTransient(sp => new ProductTagContract.ProductTagContractClient(...));
```
#### 3. Application Services فعال شدند:
```csharp
services.AddScoped<IDiscountProductService, DiscountProductService>();
services.AddScoped<IDiscountCategoryService, DiscountCategoryService>();
services.AddScoped<IDiscountOrderService, DiscountOrderService>();
services.AddScoped<IPublicMessageService, PublicMessageService>();
services.AddScoped<ITagService, TagService>();
```
### صفحات فعال شده:
| صفحه | Route | توضیحات |
|------|-------|---------|
| مدیریت محصولات تخفیفی | `/discount-products` | CRUD + گالری تصاویر |
| مدیریت دسته‌بندی‌ها | `/discount-categories` | CRUD + سلسله‌مراتب |
| مدیریت سفارشات | `/discount-orders` | مشاهده + تغییر وضعیت |
| گزارش فروش | `/sales-reports` | آمار و نمودار |
| مدیریت تگ‌ها | `/tags` | CRUD تگ‌ها |
| پیام‌های عمومی | `/public-messages` | CRUD + انتشار |
---
@@ -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>