diff --git a/docs/MANUAL-ACTIVATION-FEATURE.md b/docs/MANUAL-ACTIVATION-FEATURE.md new file mode 100644 index 0000000..a431acd --- /dev/null +++ b/docs/MANUAL-ACTIVATION-FEATURE.md @@ -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` diff --git a/docs/REMAINING-TASKS.md b/docs/REMAINING-TASKS.md index c694c51..1cc630b 100644 --- a/docs/REMAINING-TASKS.md +++ b/docs/REMAINING-TASKS.md @@ -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(); +services.AddScoped(); +services.AddScoped(); +services.AddScoped(); +services.AddScoped(); +``` + +### صفحات فعال شده: + +| صفحه | Route | توضیحات | +|------|-------|---------| +| مدیریت محصولات تخفیفی | `/discount-products` | CRUD + گالری تصاویر | +| مدیریت دسته‌بندی‌ها | `/discount-categories` | CRUD + سلسله‌مراتب | +| مدیریت سفارشات | `/discount-orders` | مشاهده + تغییر وضعیت | +| گزارش فروش | `/sales-reports` | آمار و نمودار | +| مدیریت تگ‌ها | `/tags` | CRUD تگ‌ها | +| پیام‌های عمومی | `/public-messages` | CRUD + انتشار | --- diff --git a/src/BackOffice.Main/Pages/Payment/ManualMembershipPayment.razor b/src/BackOffice.Main/Pages/Payment/ManualMembershipPayment.razor deleted file mode 100644 index 18e7710..0000000 --- a/src/BackOffice.Main/Pages/Payment/ManualMembershipPayment.razor +++ /dev/null @@ -1,131 +0,0 @@ -@page "/payment/membership" -@using BackOffice.BFF.ManualPayment.Protobuf -@using BackOffice.Main.Pages.AutoComplete -@attribute [Authorize(Roles = "Admin,SuperAdmin")] - -پرداخت دستی عضویت - - - - - - - - پرداخت دستی عضویت - - - - - - - - - - - - - - - - - - - - - - - - - - - - @if (_showResult) - { - - @_resultMessage - شماره تراکنش: @_transactionId - شماره سفارش: @_orderId - موجودی جدید کیف پول: @_newWalletBalance.ToString("N0") ریال - - } - - - - - @if (_isProcessing) - { - - در حال ثبت... - } - else - { - ثبت پرداخت - } - - - - پاک کردن فرم - - - - - - - - - - - راهنما - - - - - این صفحه برای ثبت پرداخت‌های دستی عضویت استفاده می‌شود. پس از ثبت: - - - مبلغ به کیف پول کاربر (Balance و DiscountBalance) اضافه می‌شود - - - لاگ تغییرات کیف پول ثبت می‌شود - - - تراکنش با وضعیت موفق ثبت می‌شود - - - سفارش خرید پکیج عضویت ثبت می‌شود - - - - - diff --git a/src/BackOffice.Main/Pages/Payment/ManualMembershipPayment.razor.cs b/src/BackOffice.Main/Pages/Payment/ManualMembershipPayment.razor.cs deleted file mode 100644 index 0f4b079..0000000 --- a/src/BackOffice.Main/Pages/Payment/ManualMembershipPayment.razor.cs +++ /dev/null @@ -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; - } -} diff --git a/src/BackOffice/BackOffice.csproj b/src/BackOffice/BackOffice.csproj index 5dbab5f..51e5f02 100644 --- a/src/BackOffice/BackOffice.csproj +++ b/src/BackOffice/BackOffice.csproj @@ -117,25 +117,27 @@ - - - - + + + + - + - + - + + + diff --git a/src/BackOffice/Common/Configure/ConfigureService.cs b/src/BackOffice/Common/Configure/ConfigureService.cs index f2f9d62..43f0e43 100644 --- a/src/BackOffice/Common/Configure/ConfigureService.cs +++ b/src/BackOffice/Common/Configure/ConfigureService.cs @@ -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(); services.AddScoped(); - // TODO: Re-enable when proto projects are created - // services.AddScoped(); - // services.AddScoped(); - // services.AddScoped(); - // services.AddScoped(); - // services.AddScoped(); + + // Discount Shop Services + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); return services; } @@ -120,18 +120,23 @@ public static class ConfigureServices services.AddTransient(sp => new HealthContract.HealthContractClient(sp.GetRequiredService())); services.AddTransient(sp => new AppVersionContract.AppVersionContractClient(sp.GetRequiredService())); - // TODO: Re-enable when proto projects are created // Discount Shop Services - // services.AddTransient(sp => new DiscountProductsContract.DiscountProductsContractClient(sp.GetRequiredService())); - // services.AddTransient(sp => new DiscountCategoriesContract.DiscountCategoriesContractClient(sp.GetRequiredService())); - // services.AddTransient(sp => new DiscountOrdersContract.DiscountOrdersContractClient(sp.GetRequiredService())); + services.AddTransient(sp => new DiscountProductContract.DiscountProductContractClient(sp.GetRequiredService())); + services.AddTransient(sp => new DiscountCategoryContract.DiscountCategoryContractClient(sp.GetRequiredService())); + services.AddTransient(sp => new DiscountOrderContract.DiscountOrderContractClient(sp.GetRequiredService())); // Public Message Service - // services.AddTransient(sp => new PublicMessagesContract.PublicMessagesContractClient(sp.GetRequiredService())); + services.AddTransient(sp => new PublicMessageContract.PublicMessageContractClient(sp.GetRequiredService())); // Tag Management Services - // services.AddTransient(sp => new TagContract.TagContractClient(sp.GetRequiredService())); - // services.AddTransient(sp => new ProductTagContract.ProductTagContractClient(sp.GetRequiredService())); + services.AddTransient(sp => new TagContract.TagContractClient(sp.GetRequiredService())); + services.AddTransient(sp => new ProductTagContract.ProductTagContractClient(sp.GetRequiredService())); + + // Manual Payment Service + services.AddTransient(sp => new ManualPaymentContract.ManualPaymentContractClient(sp.GetRequiredService())); + + // Inventory Management Service + services.AddTransient(sp => new InventoryBFFContract.InventoryBFFContractClient(sp.GetRequiredService())); return services; } diff --git a/src/BackOffice/Common/Utilities/RouteConstance.cs b/src/BackOffice/Common/Utilities/RouteConstance.cs index 97568b6..1bdefa8 100644 --- a/src/BackOffice/Common/Utilities/RouteConstance.cs +++ b/src/BackOffice/Common/Utilities/RouteConstance.cs @@ -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/"; } diff --git a/src/BackOffice/Pages/Commission/Components/PayoutDetailsDialog.razor b/src/BackOffice/Pages/Commission/Components/PayoutDetailsDialog.razor index 061c9bb..bb2c5c1 100644 --- a/src/BackOffice/Pages/Commission/Components/PayoutDetailsDialog.razor +++ b/src/BackOffice/Pages/Commission/Components/PayoutDetailsDialog.razor @@ -23,12 +23,12 @@ - جزئیات Payout + جزئیات پرداخت - شناسه Payout: + شناسه پرداخت: @Payout.Id diff --git a/src/BackOffice/Pages/Commission/Dashboard.razor b/src/BackOffice/Pages/Commission/Dashboard.razor index 6df9550..6e8afdf 100644 --- a/src/BackOffice/Pages/Commission/Dashboard.razor +++ b/src/BackOffice/Pages/Commission/Dashboard.razor @@ -229,7 +229,7 @@ Color="Color.Primary" StartIcon="@Icons.Material.Filled.Payments" Href="/commission/payouts"> - مشاهده Payout ها + مشاهده پرداخت‌ها @context.Id @context.UserFullName (@context.UserNationalCode) @context.Amount.ToString("N0") - @context.PaymentDate.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm") + @context.PaymentDate.ToDateTime().MiladiToJalali() } diff --git a/src/BackOffice/Pages/Inventory/Components/AddStockDialog.razor b/src/BackOffice/Pages/Inventory/Components/AddStockDialog.razor new file mode 100644 index 0000000..5f59e3e --- /dev/null +++ b/src/BackOffice/Pages/Inventory/Components/AddStockDialog.razor @@ -0,0 +1,178 @@ +@using Foursat.BackOffice.BFF.Inventory.Protos +@using Google.Protobuf.WellKnownTypes +@using InventoryProductType = Foursat.BackOffice.BFF.Inventory.Protos.ProductType + + + + + + @if (ProductIdParam == null) + { + + + محصول عادی + محصول تخفیفی + + + + + + } + else + { + + + محصول: @ProductName + + + } + + + + @foreach (var wh in Warehouses) + { + @wh.Name + } + + + + + + + + + + + + + + + + + + + انصراف + + @if (_isSubmitting) + { + + } + ثبت موجودی + + + + +@code { + [CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!; + [Inject] public InventoryBFFContract.InventoryBFFContractClient InventoryContract { get; set; } = default!; + + [Parameter] public List 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; + } + } +} diff --git a/src/BackOffice/Pages/Inventory/Components/AdjustStockDialog.razor b/src/BackOffice/Pages/Inventory/Components/AdjustStockDialog.razor new file mode 100644 index 0000000..0132af3 --- /dev/null +++ b/src/BackOffice/Pages/Inventory/Components/AdjustStockDialog.razor @@ -0,0 +1,142 @@ +@using Foursat.BackOffice.BFF.Inventory.Protos + + + + + + + + محصول: @Item.ProductName + موجودی فعلی: @Item.Quantity عدد + رزرو شده: @Item.ReservedQuantity عدد + قابل فروش: @Item.AvailableQuantity عدد + + + + + + + + @if (_newQuantity != Item.Quantity) + { + + @GetDifferenceAlertText() + + } + + + + + انصراف + + @if (_isSubmitting) + { + + } + ذخیره تغییرات + + + + +@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; + } + } +} diff --git a/src/BackOffice/Pages/Inventory/Components/CreateWarehouseDialog.razor b/src/BackOffice/Pages/Inventory/Components/CreateWarehouseDialog.razor new file mode 100644 index 0000000..9ceb185 --- /dev/null +++ b/src/BackOffice/Pages/Inventory/Components/CreateWarehouseDialog.razor @@ -0,0 +1,112 @@ +@using Foursat.BackOffice.BFF.Inventory.Protos + + + + + + + + + + + + + + @if (_isDefault) + { + + این انبار به عنوان انبار پیش‌فرض برای ورود کالا تنظیم می‌شود. + + } + + + + + انصراف + + @if (_isSubmitting) + { + + } + ایجاد انبار + + + + +@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; + } + } +} diff --git a/src/BackOffice/Pages/Inventory/Components/InventorySettingsDialog.razor b/src/BackOffice/Pages/Inventory/Components/InventorySettingsDialog.razor new file mode 100644 index 0000000..d450db5 --- /dev/null +++ b/src/BackOffice/Pages/Inventory/Components/InventorySettingsDialog.razor @@ -0,0 +1,133 @@ +@using Foursat.BackOffice.BFF.Inventory.Protos + + + + + + + + محصول: @Item.ProductName + انبار: @Item.WarehouseName + + + + + + + + + + + + + مقادیر فعلی: + حد هشدار: @Item.LowStockThreshold | + نقطه سفارش: @Item.ReorderPoint | + حداکثر: @Item.MaxStockLevel + + + + + + انصراف + + @if (_isSubmitting) + { + + } + ذخیره تنظیمات + + + + +@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; + } + } +} diff --git a/src/BackOffice/Pages/Inventory/Components/RecordLossDialog.razor b/src/BackOffice/Pages/Inventory/Components/RecordLossDialog.razor new file mode 100644 index 0000000..16979f0 --- /dev/null +++ b/src/BackOffice/Pages/Inventory/Components/RecordLossDialog.razor @@ -0,0 +1,123 @@ +@using Foursat.BackOffice.BFF.Inventory.Protos + + + + + + + + محصول: @Item.ProductName + موجودی فعلی: @Item.Quantity عدد + + + + + + + + + + @if (_quantity > 0 && _quantity <= Item.Quantity) + { + + + @_quantity عدد از موجودی کسر خواهد شد. + موجودی جدید: @(Item.Quantity - _quantity) عدد + + + } + + + + + انصراف + + @if (_isSubmitting) + { + + } + ثبت خسارت + + + + +@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; + } + } +} diff --git a/src/BackOffice/Pages/Inventory/InventoryMainPage.razor b/src/BackOffice/Pages/Inventory/InventoryMainPage.razor new file mode 100644 index 0000000..e1f950e --- /dev/null +++ b/src/BackOffice/Pages/Inventory/InventoryMainPage.razor @@ -0,0 +1,216 @@ +@attribute [Route(RouteConstance.Inventory)] +@attribute [Authorize] + +@using Foursat.BackOffice.BFF.Inventory.Protos +@using BackOffice.Pages.Inventory.Components + + + مدیریت انبار + + @* Filters Section *@ + + + + + @foreach (var wh in _warehouses) + { + @wh.Name + } + + + + + همه + محصول عادی + محصول تخفیفی + + + + + + + + اعمال فیلتر + + + پاک کردن + + + + + + @* Stats Cards *@ + + + + + + + کل اقلام انبار + @_totalCount + + + + + + + + + + + + محصولات کم‌موجود + @_lowStockCount + + + + + + + + + @* Data Grid *@ + + + لیست موجودی انبار + + + + ورود کالا + + + + + + + + + + @context.Item.ProductName + + @GetProductTypeLabel(context.Item.ProductType) + + + + + + + + + @context.Item.WarehouseName + + + + + + + + @context.Item.Quantity + + + + + + + @context.Item.ReservedQuantity + + + + + + @context.Item.AvailableQuantity + + + + + + + + @if (context.Item.IsLowStock) + { + + + کم‌موجود + + } + else + { + + موجود + + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/BackOffice/Pages/Inventory/InventoryMainPage.razor.cs b/src/BackOffice/Pages/Inventory/InventoryMainPage.razor.cs new file mode 100644 index 0000000..155fe0a --- /dev/null +++ b/src/BackOffice/Pages/Inventory/InventoryMainPage.razor.cs @@ -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? _gridData; + private List _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> ServerReload(GridState 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 + { + Items = result.Items.ToList(), + TotalItems = result.TotalCount + }; + } + + return new GridData(); + } + catch (Exception ex) + { + Snackbar.Add($"خطا در بارگذاری داده‌ها: {ex.Message}", Severity.Error); + return new GridData(); + } + } + + 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("ورود کالا به انبار", 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($"ورود کالا - {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($"تنظیم موجودی - {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($"ثبت خسارت - {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($"تنظیمات - {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}"); + } +} diff --git a/src/BackOffice/Pages/Inventory/LowStockPage.razor b/src/BackOffice/Pages/Inventory/LowStockPage.razor new file mode 100644 index 0000000..e99e3ec --- /dev/null +++ b/src/BackOffice/Pages/Inventory/LowStockPage.razor @@ -0,0 +1,146 @@ +@attribute [Route(RouteConstance.InventoryLowStock)] +@attribute [Authorize] + +@using Foursat.BackOffice.BFF.Inventory.Protos + + + + + + + محصولات کم‌موجود + + + لیست محصولاتی که موجودی آنها کمتر از حد هشدار است + + + + بازگشت به انبار + + + + @* Filters Section *@ + + + + + @foreach (var wh in _warehouses) + { + @wh.Name + } + + + + + همه + محصول عادی + محصول تخفیفی + + + + + بارگذاری + + + + + + @* Stats *@ + + + @_items.Count محصول کم‌موجود شناسایی شده است. + لطفاً برای جلوگیری از کمبود موجودی، اقدام به سفارش مجدد کنید. + + + + @if (_isLoading) + { + + } + else + { + + + + + + + + @context.Item.ProductName + + @GetProductTypeLabel(context.Item.ProductType) + + + + + + + + + @context.Item.WarehouseName + + + + + + + @context.Item.Quantity + + + + + + @context.Item.AvailableQuantity + + + + + + + + + + @{ + var shortage = context.Item.LowStockThreshold - context.Item.Quantity; + } + + @shortage عدد + + + + + + + + ورود کالا + + + + + + + + + } + diff --git a/src/BackOffice/Pages/Inventory/LowStockPage.razor.cs b/src/BackOffice/Pages/Inventory/LowStockPage.razor.cs new file mode 100644 index 0000000..726dc88 --- /dev/null +++ b/src/BackOffice/Pages/Inventory/LowStockPage.razor.cs @@ -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 _items = new(); + private List _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($"ورود کالا - {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); + } +} diff --git a/src/BackOffice/Pages/Inventory/MovementsPage.razor b/src/BackOffice/Pages/Inventory/MovementsPage.razor new file mode 100644 index 0000000..92f77c9 --- /dev/null +++ b/src/BackOffice/Pages/Inventory/MovementsPage.razor @@ -0,0 +1,186 @@ +@attribute [Route(RouteConstance.InventoryMovements)] +@attribute [Authorize] + +@using Foursat.BackOffice.BFF.Inventory.Protos +@using Google.Protobuf.WellKnownTypes + + + + + تاریخچه تغییرات موجودی + @if (!string.IsNullOrEmpty(_productName)) + { + محصول: @_productName + } + + + بازگشت به انبار + + + + @* Filters Section *@ + + + + + همه + موجودی اولیه + ورود کالا + برگشت از مشتری + فروش + افزایش موجودی + کاهش موجودی + رزرو + آزادسازی رزرو + خسارت + آسیب‌دیده + منقضی + انتقال به بیرون + انتقال به داخل + + + + + همه + محصول عادی + محصول تخفیفی + + + + + + + + + + + اعمال فیلتر + + + پاک کردن + + + + + + @* Data Grid *@ + + + تغییرات موجودی + + + کل: @_totalCount ردیف + + + + + + + + + + + @context.Item.MovementTypeName + + + + + + + + @(IsIncreaseMovement(context.Item.MovementType) ? "+" : "-")@Math.Abs(context.Item.Quantity) + + + + + + + + + + @if (!string.IsNullOrEmpty(context.Item.ReferenceNumber)) + { + @context.Item.ReferenceNumber + } + else + { + - + } + + + + + + @if (!string.IsNullOrEmpty(context.Item.PerformedByUserName)) + { + @context.Item.PerformedByUserName + } + else + { + سیستم + } + + + + + + @if (context.Item.CreatedAt != null) + { + + @context.Item.CreatedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd HH:mm") + + } + + + + + + @if (!string.IsNullOrEmpty(context.Item.Note)) + { + + + @context.Item.Note + + + } + + + + + + + + diff --git a/src/BackOffice/Pages/Inventory/MovementsPage.razor.cs b/src/BackOffice/Pages/Inventory/MovementsPage.razor.cs new file mode 100644 index 0000000..25a9fb3 --- /dev/null +++ b/src/BackOffice/Pages/Inventory/MovementsPage.razor.cs @@ -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? _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> ServerReload(GridState 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 + { + Items = result.Movements.ToList(), + TotalItems = result.TotalCount + }; + } + + return new GridData(); + } + catch (Exception ex) + { + Snackbar.Add($"خطا در بارگذاری داده‌ها: {ex.Message}", Severity.Error); + return new GridData(); + } + } + + 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); + } +} diff --git a/src/BackOffice/Pages/Inventory/WarehousesPage.razor b/src/BackOffice/Pages/Inventory/WarehousesPage.razor new file mode 100644 index 0000000..0da858c --- /dev/null +++ b/src/BackOffice/Pages/Inventory/WarehousesPage.razor @@ -0,0 +1,123 @@ +@attribute [Route(RouteConstance.InventoryWarehouses)] +@attribute [Authorize] + +@using Foursat.BackOffice.BFF.Inventory.Protos +@using BackOffice.Pages.Inventory.Components + + + + + + + مدیریت انبارها + + + تعریف و مدیریت انبارهای سیستم + + + + + انبار جدید + + + بازگشت به انبار + + + + + @* Filter Toggle *@ + + + + بارگذاری مجدد + + + + @if (_isLoading) + { + + } + else + { + + @foreach (var warehouse in _warehouses) + { + + + + + + + + + + + @warehouse.Name + @if (warehouse.IsDefault) + { + پیش‌فرض + } + + + کد: @warehouse.Code + + + + + @(warehouse.IsActive ? "فعال" : "غیرفعال") + + + + + + + + + @(string.IsNullOrEmpty(warehouse.Address) ? "آدرس ثبت نشده" : warehouse.Address) + + + + + + ایجاد: @(warehouse.Created?.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd") ?? "-") + + + + + + + مشاهده موجودی + + + + + } + + @if (_warehouses.Count == 0) + { + + + هیچ انباری یافت نشد. برای شروع، یک انبار جدید ایجاد کنید. + + + } + + } + diff --git a/src/BackOffice/Pages/Inventory/WarehousesPage.razor.cs b/src/BackOffice/Pages/Inventory/WarehousesPage.razor.cs new file mode 100644 index 0000000..f6949dc --- /dev/null +++ b/src/BackOffice/Pages/Inventory/WarehousesPage.razor.cs @@ -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 _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("ایجاد انبار جدید", 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); + } +} diff --git a/src/BackOffice/Pages/Network/UserNetworkInfo.razor b/src/BackOffice/Pages/Network/UserNetworkInfo.razor index 424b406..e8c4357 100644 --- a/src/BackOffice/Pages/Network/UserNetworkInfo.razor +++ b/src/BackOffice/Pages/Network/UserNetworkInfo.razor @@ -366,7 +366,7 @@ Color="Color.Info" StartIcon="@Icons.Material.Filled.Money" OnClick="@(() => NavigationManager.NavigateTo($"/commission/payouts?userId={UserId}"))"> - Payout های کاربر + پرداخت‌های کاربر @if (Mode == ManualPaymentDialogMode.Create) { - ثبت پرداخت دستی جدید + ثبت فعالسازی دستی جدید - + - + + واریز نقدی + شارژ کیف پول تخفیف + شارژ کیف پول شبکه + تسویه حساب + اصلاح خطا + بازپرداخت + سایر + + Required="true" + RequiredError="توضیحات الزامی است" + @bind-Value="_description" /> + + تصویر فیش پرداخت (اختیاری) + + + + انتخاب تصویر + + + + + @if (!string.IsNullOrWhiteSpace(_imagePreview)) + { + + + + + } } else if (Model is not null) { - جزئیات پرداخت دستی + جزئیات فعالسازی دستی شناسه: @Model.Id کاربر: @Model.UserFullName (@Model.UserId) مبلغ: @Model.Amount.ToString("N0") ریال @@ -52,6 +92,16 @@ شماره مرجع: @Model.ReferenceNumber } + @if (!string.IsNullOrWhiteSpace(Model.ImagePath)) + { + تصویر فیش: + + } + توضیحات: @Model.Description @if (!string.IsNullOrWhiteSpace(Model.RejectionReason)) @@ -79,7 +129,11 @@ @if (Mode == ManualPaymentDialogMode.Create) { - + + @if (_isSubmitting) + { + + } ثبت } diff --git a/src/BackOffice/Pages/Payment/Components/ManualPaymentDialog.razor.cs b/src/BackOffice/Pages/Payment/Components/ManualPaymentDialog.razor.cs index ff5b72d..b90b4c2 100644 --- a/src/BackOffice/Pages/Payment/Components/ManualPaymentDialog.razor.cs +++ b/src/BackOffice/Pages/Payment/Components/ManualPaymentDialog.razor.cs @@ -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() diff --git a/src/BackOffice/Pages/Payment/ManualPayments.razor b/src/BackOffice/Pages/Payment/ManualPayments.razor index 679ce03..d5ad02f 100644 --- a/src/BackOffice/Pages/Payment/ManualPayments.razor +++ b/src/BackOffice/Pages/Payment/ManualPayments.razor @@ -45,11 +45,11 @@ - پرداخت‌های دستی + فعالسازی (پرداخت) دستی - ثبت پرداخت دستی جدید + ثبت فعالسازی دستی جدید diff --git a/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor b/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor index cbc6602..83f3624 100644 --- a/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor +++ b/src/BackOffice/Pages/UserOrder/UserOrderMainPage.razor @@ -77,7 +77,7 @@ - + @* جمع سفارش‌ها در بازه فعلی @@ -107,7 +107,7 @@ } - + *@ - + @* @(context.Item.VatAmount > 0 ? context.Item.VatAmount.ToString("N0") : "-") @@ -136,7 +136,7 @@ @(context.Item.VatPercentage > 0 ? $"{context.Item.VatPercentage:0}٪" : "-") - + *@ diff --git a/src/BackOffice/Shared/NavMenu.razor b/src/BackOffice/Shared/NavMenu.razor index 6b33e94..ea9a6ac 100644 --- a/src/BackOffice/Shared/NavMenu.razor +++ b/src/BackOffice/Shared/NavMenu.razor @@ -81,7 +81,16 @@ آمار باشگاه - + @if (CanManagePayments) + { + + + فعالسازی (پرداخت) دستی + + + } مدیریت @@ -106,7 +115,7 @@ ویرایش دسته‌جمعی @@ -129,6 +138,29 @@ مدیریت تگ‌ها + + + + موجودی انبار + + + محصولات کم‌موجود + + + مدیریت انبارها + + + تاریخچه تغییرات + + } @if (CanViewOrders) @@ -158,21 +190,7 @@ } - @if (CanManagePayments) - { - - - پرداخت‌های دستی - - - پرداخت دستی عضویت - - - } +