From b9171d548b50798bea66daf9c2be5ee1ecb0a733 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Tue, 17 Feb 2026 02:11:49 +0330 Subject: [PATCH 1/5] =?UTF-8?q?docs:=20=D9=81=D8=A7=D8=B2=20=DB=B1=DB=B5.?= =?UTF-8?q?=DB=B4-=DB=B1=DB=B5.=DB=B6=20=E2=80=94=20K8S=5FSERVER,=20appset?= =?UTF-8?q?tings.Production,=20nginx=20image=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/BACKOFFICE-CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/BACKOFFICE-CHANGELOG.md b/docs/BACKOFFICE-CHANGELOG.md index f4528fb..7670aa9 100644 --- a/docs/BACKOFFICE-CHANGELOG.md +++ b/docs/BACKOFFICE-CHANGELOG.md @@ -25,6 +25,24 @@ - **تغییر:** `Foursat.CMSMicroservice.Protobuf` → `0.0.179` - **فایل:** `BackOffice.csproj` +### ۱۵.۴ — فیکس K8S_SERVER در prod-deploy.yml ✅ + +- **مشکل:** `K8S_SERVER` هنوز `194.5.195.53` (staging) بود +- **رفع:** تغییر به `45.149.79.127` (production) +- **فیکس اضافی:** `kubectl rollout restart` → `kubectl set image` (تا ایمیج SHA-tagged واقعاً set بشه) + +### ۱۵.۵ — ساخت appsettings.Production.json ✅ + +- **مشکل:** فایل `appsettings.Production.json` وجود نداشت → `GwUrl` fallback به `localhost` +- **رفع:** ساخت `wwwroot/appsettings.Production.json` با `GwUrl=https://cms.kbs1.ir` + +### ۱۵.۶ — فیکس nginx image path در Dockerfile (production branch) ✅ + +- **مشکل:** Dockerfile روی برنچ production از `library/nginx:alpine` استفاده می‌کرد — وجود نداشت +- **ارور:** `manifest for 194.5.195.53:32082/library/nginx:alpine not found` +- **رفع:** تغییر به `194.5.195.53:32082/nginx:alpine` +- **کامیت:** `743403e` (مستقیم روی برنچ production) + --- ## 🏷 فاز ۱۴ — فیکس Docker Build + Deployment (بهمن ۱۴۰۴) From af51c02d3d431320f669a76fe2fb334639fe303f Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Tue, 17 Feb 2026 02:56:07 +0330 Subject: [PATCH 2/5] chore: clean up commented-out GwUrl lines in appsettings --- src/BackOffice/wwwroot/appsettings.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/BackOffice/wwwroot/appsettings.json b/src/BackOffice/wwwroot/appsettings.json index 9e32c79..abfef19 100644 --- a/src/BackOffice/wwwroot/appsettings.json +++ b/src/BackOffice/wwwroot/appsettings.json @@ -1,8 +1,5 @@ { - "GwUrl": "http://localhost:32847", -// "GwUrl": "https://localhost:32846", -// "GwUrl": "https://backoffice-bff.se.kbs1.ir", // OLD - via BFF - // "GwUrl": "https://cms.se.kbs1.ir", // NEW - direct to CMS + "GwUrl": "http://localhost:32847", "Authentication": { "Authority": "https://ids.afrino.co/", "ClientId": "client_backoffice_spa" From 34091337578139a484ccdc987bb67aa5ca4352b8 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Tue, 17 Feb 2026 22:34:59 +0330 Subject: [PATCH 3/5] feat: Add SitePageSettings management UI - ISitePageSettingsService + SitePageSettingsService (gRPC client) - PageSettingsManagementPage: 4 cards for fixed pages - PageSettingsEditDialog: edit title, hero, meta, settingsJson - PageImagesDialog: CRUD for page images (licenses, team, values) - PageImageEditDialog: add/edit single image with upload - Proto NuGet updated to 0.0.180 - NavMenu: added link to page settings --- src/BackOffice/BackOffice.csproj | 2 +- .../Common/Configure/ConfigureService.cs | 3 + .../Components/PageImageEditDialog.razor | 100 +++++++++++++ .../Components/PageImageEditDialog.razor.cs | 112 +++++++++++++++ .../Content/Components/PageImagesDialog.razor | 88 ++++++++++++ .../Components/PageImagesDialog.razor.cs | 133 ++++++++++++++++++ .../Components/PageSettingsEditDialog.razor | 88 ++++++++++++ .../PageSettingsEditDialog.razor.cs | 82 +++++++++++ .../Content/PageSettingsManagementPage.razor | 74 ++++++++++ .../PageSettingsManagementPage.razor.cs | 119 ++++++++++++++++ .../Content/ISitePageSettingsService.cs | 96 +++++++++++++ .../Content/SitePageSettingsService.cs | 131 +++++++++++++++++ src/BackOffice/Shared/NavMenu.razor | 4 +- 13 files changed, 1029 insertions(+), 3 deletions(-) create mode 100644 src/BackOffice/Pages/Content/Components/PageImageEditDialog.razor create mode 100644 src/BackOffice/Pages/Content/Components/PageImageEditDialog.razor.cs create mode 100644 src/BackOffice/Pages/Content/Components/PageImagesDialog.razor create mode 100644 src/BackOffice/Pages/Content/Components/PageImagesDialog.razor.cs create mode 100644 src/BackOffice/Pages/Content/Components/PageSettingsEditDialog.razor create mode 100644 src/BackOffice/Pages/Content/Components/PageSettingsEditDialog.razor.cs create mode 100644 src/BackOffice/Pages/Content/PageSettingsManagementPage.razor create mode 100644 src/BackOffice/Pages/Content/PageSettingsManagementPage.razor.cs create mode 100644 src/BackOffice/Services/Content/ISitePageSettingsService.cs create mode 100644 src/BackOffice/Services/Content/SitePageSettingsService.cs diff --git a/src/BackOffice/BackOffice.csproj b/src/BackOffice/BackOffice.csproj index 4511f4e..1e007e9 100644 --- a/src/BackOffice/BackOffice.csproj +++ b/src/BackOffice/BackOffice.csproj @@ -138,7 +138,7 @@ - + diff --git a/src/BackOffice/Common/Configure/ConfigureService.cs b/src/BackOffice/Common/Configure/ConfigureService.cs index ee4b7b3..70ee6b6 100644 --- a/src/BackOffice/Common/Configure/ConfigureService.cs +++ b/src/BackOffice/Common/Configure/ConfigureService.cs @@ -27,6 +27,7 @@ using CMSMicroservice.Protobuf.Protos.BlogPost; using CMSMicroservice.Protobuf.Protos.BlogCategory; using CMSMicroservice.Protobuf.Protos.BlogPostImage; using CMSMicroservice.Protobuf.Protos.SitePage; +using CMSMicroservice.Protobuf.Protos.SitePageSettings; using CMSMicroservice.Protobuf.Protos.UserContract; using CMSMicroservice.Protobuf.Protos.UserWallet; using CMSMicroservice.Protobuf.Protos.UserWalletChangeLog; @@ -91,6 +92,7 @@ public static class ConfigureServices services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } @@ -158,6 +160,7 @@ public static class ConfigureServices services.AddTransient(sp => new BlogCategoryContract.BlogCategoryContractClient(sp.GetRequiredService())); services.AddTransient(sp => new BlogPostImageContract.BlogPostImageContractClient(sp.GetRequiredService())); services.AddTransient(sp => new SitePageContract.SitePageContractClient(sp.GetRequiredService())); + services.AddTransient(sp => new SitePageSettingsContract.SitePageSettingsContractClient(sp.GetRequiredService())); // User Contracts Service services.AddTransient(sp => new UserContractContract.UserContractContractClient(sp.GetRequiredService())); diff --git a/src/BackOffice/Pages/Content/Components/PageImageEditDialog.razor b/src/BackOffice/Pages/Content/Components/PageImageEditDialog.razor new file mode 100644 index 0000000..5882f8f --- /dev/null +++ b/src/BackOffice/Pages/Content/Components/PageImageEditDialog.razor @@ -0,0 +1,100 @@ +@using BackOffice.Services.Content +@using Microsoft.AspNetCore.Components.Forms +@using BackOffice.Common.BaseComponents + + + + + + + + @foreach (var group in GetAvailableGroups()) + { + @group.Value + } + + + + + + + + + + + + + + + تصویر + + @if (!string.IsNullOrWhiteSpace(_imagePreview)) + { + + } + else if (!string.IsNullOrEmpty(ExistingImagePath)) + { + + } + else + { + + تصویری انتخاب نشده است + + } + + + + + انتخاب تصویر + + + + @if (context != null) + { + @context.Name + } + + + + + + + فعال + + + + + + + @(IsNew ? "افزودن" : "ذخیره تغییرات") + + + انصراف + + + diff --git a/src/BackOffice/Pages/Content/Components/PageImageEditDialog.razor.cs b/src/BackOffice/Pages/Content/Components/PageImageEditDialog.razor.cs new file mode 100644 index 0000000..38937df --- /dev/null +++ b/src/BackOffice/Pages/Content/Components/PageImageEditDialog.razor.cs @@ -0,0 +1,112 @@ +using BackOffice.Services.Content; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Forms; +using MudBlazor; + +namespace BackOffice.Pages.Content.Components; + +public partial class PageImageEditDialog +{ + [CascadingParameter] IMudDialogInstance DialogInstance { get; set; } = default!; + [Inject] public ISitePageSettingsService PageSettingsService { get; set; } = default!; + + [Parameter] public SavePageImageDto Model { get; set; } = new(); + [Parameter] public string PageKey { get; set; } = string.Empty; + [Parameter] public bool IsNew { get; set; } + [Parameter] public string? ExistingImagePath { get; set; } + + private MudForm? _form; + private bool _isValid = true; + private bool _loading; + + private IBrowserFile? _imageFile; + private byte[]? _imageBuffer; + private string? _imagePreview; + private readonly long _maxAllowedSize = (1024 * 1024) * 5; + + private async Task OnImageSelected(IBrowserFile? file) + { + _imageFile = file; + if (file != null) + { + using var ms = new MemoryStream(); + await file.OpenReadStream(_maxAllowedSize).CopyToAsync(ms); + _imageBuffer = ms.ToArray(); + _imagePreview = $"data:{file.ContentType};base64," + Convert.ToBase64String(_imageBuffer); + StateHasChanged(); + } + } + + private async Task SubmitAsync() + { + if (_form != null) + { + await _form.Validate(); + if (!_isValid) return; + } + + _loading = true; + + if (_imageFile != null && _imageBuffer != null) + { + Model.ImageFile = _imageBuffer; + Model.ImageMime = _imageFile.ContentType; + Model.ImageFileName = Path.GetFileNameWithoutExtension(_imageFile.Name); + } + else if (IsNew && _imageBuffer == null) + { + Snackbar.Add("لطفاً یک تصویر انتخاب کنید", Severity.Warning); + _loading = false; + return; + } + + try + { + var result = await PageSettingsService.SaveImageAsync(Model); + if (result.Success) + { + DialogInstance.Close(DialogResult.Ok(true)); + } + else + { + Snackbar.Add("خطا در ذخیره تصویر", Severity.Error); + } + } + catch (Exception ex) + { + Snackbar.Add($"خطا: {ex.Message}", Severity.Error); + } + finally + { + _loading = false; + } + } + + private Dictionary GetAvailableGroups() => PageKey switch + { + "about" => new Dictionary + { + { "team", "اعضای تیم" }, + { "values", "ارزش‌ها" } + }, + "licenses" => new Dictionary + { + { "licenses", "مجوزها" } + }, + "landing" => new Dictionary + { + { "values", "ارزش‌ها" } + }, + _ => new Dictionary + { + { "values", "ارزش‌ها" }, + { "team", "اعضای تیم" }, + { "licenses", "مجوزها" } + } + }; + + private void Cancel() + { + DialogInstance.Cancel(); + } +} diff --git a/src/BackOffice/Pages/Content/Components/PageImagesDialog.razor b/src/BackOffice/Pages/Content/Components/PageImagesDialog.razor new file mode 100644 index 0000000..943caba --- /dev/null +++ b/src/BackOffice/Pages/Content/Components/PageImagesDialog.razor @@ -0,0 +1,88 @@ +@using BackOffice.Services.Content +@using BackOffice.Common.BaseComponents + + + + + + تصاویر صفحه «@PageTitle» + + افزودن تصویر + + + + @if (!Images.Any()) + { + + تصویری ثبت نشده است. + + } + else + { + + + + + @if (!string.IsNullOrWhiteSpace(context.Item.ThumbnailPath)) + { + + } + else if (!string.IsNullOrWhiteSpace(context.Item.ImagePath)) + { + + } + else + { + + } + + + + + + + + + @(context.Item.IsActive ? "فعال" : "غیرفعال") + + + + + + + + + + + + + + + + + + } + + + + + بستن + + + diff --git a/src/BackOffice/Pages/Content/Components/PageImagesDialog.razor.cs b/src/BackOffice/Pages/Content/Components/PageImagesDialog.razor.cs new file mode 100644 index 0000000..4ba787c --- /dev/null +++ b/src/BackOffice/Pages/Content/Components/PageImagesDialog.razor.cs @@ -0,0 +1,133 @@ +using BackOffice.Services.Content; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace BackOffice.Pages.Content.Components; + +public partial class PageImagesDialog +{ + [CascadingParameter] IMudDialogInstance DialogInstance { get; set; } = default!; + [Inject] public ISitePageSettingsService PageSettingsService { get; set; } = default!; + + [Parameter] public long PageId { get; set; } + [Parameter] public string PageKey { get; set; } = string.Empty; + [Parameter] public string PageTitle { get; set; } = string.Empty; + [Parameter] public List Images { get; set; } = new(); + + private bool _hasChanges; + + private async Task AddImage() + { + var dto = new SavePageImageDto + { + SitePageSettingsId = PageId, + ImageGroup = GetDefaultImageGroup(), + IsActive = true, + SortOrder = Images.Count + 1 + }; + + var parameters = new DialogParameters + { + { nameof(PageImageEditDialog.Model), dto }, + { nameof(PageImageEditDialog.PageKey), PageKey }, + { nameof(PageImageEditDialog.IsNew), true } + }; + + var dialog = await DialogService.ShowAsync( + "افزودن تصویر", parameters, + new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true }); + + var result = await dialog.Result; + if (result is { Canceled: false }) + { + Snackbar.Add("تصویر با موفقیت اضافه شد", Severity.Success); + _hasChanges = true; + await ReloadImages(); + } + } + + private async Task EditImage(PageImageItemDto item) + { + var dto = new SavePageImageDto + { + Id = item.Id, + SitePageSettingsId = PageId, + ImageGroup = item.ImageGroup, + Title = item.Title, + Subtitle = item.Subtitle, + Description = item.Description, + LinkUrl = item.LinkUrl, + IconName = item.IconName, + SortOrder = item.SortOrder, + IsActive = item.IsActive + }; + + var parameters = new DialogParameters + { + { nameof(PageImageEditDialog.Model), dto }, + { nameof(PageImageEditDialog.PageKey), PageKey }, + { nameof(PageImageEditDialog.IsNew), false }, + { nameof(PageImageEditDialog.ExistingImagePath), item.ImagePath } + }; + + var dialog = await DialogService.ShowAsync( + "ویرایش تصویر", parameters, + new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true }); + + var result = await dialog.Result; + if (result is { Canceled: false }) + { + Snackbar.Add("تصویر با موفقیت ویرایش شد", Severity.Success); + _hasChanges = true; + await ReloadImages(); + } + } + + private async Task DeleteImage(PageImageItemDto item) + { + var confirm = await DialogService.ShowMessageBox( + "تایید حذف", + $"آیا از حذف تصویر «{item.Title ?? item.ImageGroup}» اطمینان دارید؟", + yesText: "بله، حذف شود", + cancelText: "انصراف"); + + if (confirm != true) return; + + try + { + await PageSettingsService.DeleteImageAsync(item.Id); + Snackbar.Add("تصویر با موفقیت حذف شد", Severity.Success); + _hasChanges = true; + await ReloadImages(); + } + catch (Exception ex) + { + Snackbar.Add($"خطا در حذف تصویر: {ex.Message}", Severity.Error); + } + } + + private async Task ReloadImages() + { + var details = await PageSettingsService.GetByKeyAsync(PageKey); + if (details != null) + { + Images = details.Images; + StateHasChanged(); + } + } + + private string GetDefaultImageGroup() => PageKey switch + { + "about" => "team", + "licenses" => "licenses", + _ => "values" + }; + + private void Close() + { + if (_hasChanges) + DialogInstance.Close(DialogResult.Ok(true)); + else + DialogInstance.Cancel(); + } +} diff --git a/src/BackOffice/Pages/Content/Components/PageSettingsEditDialog.razor b/src/BackOffice/Pages/Content/Components/PageSettingsEditDialog.razor new file mode 100644 index 0000000..899e7be --- /dev/null +++ b/src/BackOffice/Pages/Content/Components/PageSettingsEditDialog.razor @@ -0,0 +1,88 @@ +@using BackOffice.Services.Content +@using Microsoft.AspNetCore.Components.Forms +@using BackOffice.Common.BaseComponents + + + + + + + + + + + + + + تصویر هیرو + + @if (!string.IsNullOrWhiteSpace(_imagePreview)) + { + + } + else if (!string.IsNullOrEmpty(ExistingHeroImagePath)) + { + + } + else + { + + تصویری انتخاب نشده است + + } + + + + + انتخاب تصویر هیرو + + + + @if (context != null) + { + @context.Name + } + + + + + + + تنظیمات اختصاصی (JSON) + + + + + فعال + + + + + + + ذخیره تغییرات + + + انصراف + + + diff --git a/src/BackOffice/Pages/Content/Components/PageSettingsEditDialog.razor.cs b/src/BackOffice/Pages/Content/Components/PageSettingsEditDialog.razor.cs new file mode 100644 index 0000000..028a351 --- /dev/null +++ b/src/BackOffice/Pages/Content/Components/PageSettingsEditDialog.razor.cs @@ -0,0 +1,82 @@ +using BackOffice.Services.Content; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Forms; +using MudBlazor; + +namespace BackOffice.Pages.Content.Components; + +public partial class PageSettingsEditDialog +{ + [CascadingParameter] IMudDialogInstance DialogInstance { get; set; } = default!; + [Inject] public ISitePageSettingsService PageSettingsService { get; set; } = default!; + + [Parameter] public SavePageSettingsDto Model { get; set; } = new(); + [Parameter] public string? ExistingHeroImagePath { get; set; } + [Parameter] public string PageTitle { get; set; } = string.Empty; + + private MudForm? _form; + private bool _isValid = true; + private bool _loading; + + private IBrowserFile? _imageFile; + private byte[]? _imageBuffer; + private string? _imagePreview; + private readonly long _maxAllowedSize = (1024 * 1024) * 5; + + private async Task OnImageSelected(IBrowserFile? file) + { + _imageFile = file; + if (file != null) + { + using var ms = new MemoryStream(); + await file.OpenReadStream(_maxAllowedSize).CopyToAsync(ms); + _imageBuffer = ms.ToArray(); + _imagePreview = $"data:{file.ContentType};base64," + Convert.ToBase64String(_imageBuffer); + StateHasChanged(); + } + } + + private async Task SubmitAsync() + { + if (_form != null) + { + await _form.Validate(); + if (!_isValid) return; + } + + _loading = true; + + if (_imageFile != null && _imageBuffer != null) + { + Model.ImageFile = _imageBuffer; + Model.ImageMime = _imageFile.ContentType; + Model.ImageFileName = Path.GetFileNameWithoutExtension(_imageFile.Name); + } + + try + { + var result = await PageSettingsService.SaveSettingsAsync(Model); + if (result.Success) + { + DialogInstance.Close(DialogResult.Ok(true)); + } + else + { + Snackbar.Add("خطا در ذخیره تنظیمات", Severity.Error); + } + } + catch (Exception ex) + { + Snackbar.Add($"خطا: {ex.Message}", Severity.Error); + } + finally + { + _loading = false; + } + } + + private void Cancel() + { + DialogInstance.Cancel(); + } +} diff --git a/src/BackOffice/Pages/Content/PageSettingsManagementPage.razor b/src/BackOffice/Pages/Content/PageSettingsManagementPage.razor new file mode 100644 index 0000000..8a99432 --- /dev/null +++ b/src/BackOffice/Pages/Content/PageSettingsManagementPage.razor @@ -0,0 +1,74 @@ +@page "/content/page-settings" + +@using BackOffice.Services.Content + +@inject ISitePageSettingsService PageSettingsService + +
+ + مدیریت تنظیمات صفحات + تنظیمات ۴ صفحه ثابت سایت: صفحه اصلی، درباره ما، تماس با ما و مجوزها + + @if (_loading) + { + + } + else + { + + @foreach (var item in _pages) + { + + + + + + + + @item.Title + @item.PageKey + + + + + + @(item.IsActive ? "فعال" : "غیرفعال") + + + + + + + + @item.ImageCount تصویر + + + آخرین ویرایش: @item.LastModified.MiladiToJalaliWithTime() + + + + + + ویرایش + + + تصاویر + + + + + } + + } + +
diff --git a/src/BackOffice/Pages/Content/PageSettingsManagementPage.razor.cs b/src/BackOffice/Pages/Content/PageSettingsManagementPage.razor.cs new file mode 100644 index 0000000..6c77e93 --- /dev/null +++ b/src/BackOffice/Pages/Content/PageSettingsManagementPage.razor.cs @@ -0,0 +1,119 @@ +using BackOffice.Services.Content; +using BackOffice.Pages.Content.Components; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace BackOffice.Pages.Content; + +public partial class PageSettingsManagementPage +{ + private List _pages = new(); + private bool _loading = true; + + protected override async Task OnInitializedAsync() + { + await LoadPagesAsync(); + } + + private async Task LoadPagesAsync() + { + _loading = true; + try + { + _pages = await PageSettingsService.GetAllAsync(); + } + catch (Exception ex) + { + Snackbar.Add($"خطا در دریافت صفحات: {ex.Message}", Severity.Error); + } + finally + { + _loading = false; + } + } + + private async Task EditPageSettings(PageSettingsSummaryDto summary) + { + var details = await PageSettingsService.GetByKeyAsync(summary.PageKey); + if (details == null) + { + Snackbar.Add("خطا در دریافت تنظیمات صفحه", Severity.Error); + return; + } + + var dto = new SavePageSettingsDto + { + PageKey = details.PageKey, + Title = details.Title, + MetaDescription = details.MetaDescription, + HeroTitle = details.HeroTitle, + HeroSubtitle = details.HeroSubtitle, + IsActive = details.IsActive, + SettingsJson = details.SettingsJson + }; + + var parameters = new DialogParameters + { + { nameof(PageSettingsEditDialog.Model), dto }, + { nameof(PageSettingsEditDialog.ExistingHeroImagePath), details.HeroImagePath }, + { nameof(PageSettingsEditDialog.PageTitle), GetPageDisplayName(summary.PageKey) } + }; + + var dialog = await DialogService.ShowAsync( + $"ویرایش تنظیمات «{GetPageDisplayName(summary.PageKey)}»", parameters, + new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true }); + + var result = await dialog.Result; + if (result is { Canceled: false }) + { + Snackbar.Add("تنظیمات صفحه با موفقیت ذخیره شد", Severity.Success); + await LoadPagesAsync(); + } + } + + private async Task ManageImages(PageSettingsSummaryDto summary) + { + var details = await PageSettingsService.GetByKeyAsync(summary.PageKey); + if (details == null) + { + Snackbar.Add("خطا در دریافت اطلاعات صفحه", Severity.Error); + return; + } + + var parameters = new DialogParameters + { + { nameof(PageImagesDialog.PageId), details.Id }, + { nameof(PageImagesDialog.PageKey), details.PageKey }, + { nameof(PageImagesDialog.PageTitle), GetPageDisplayName(summary.PageKey) }, + { nameof(PageImagesDialog.Images), details.Images } + }; + + var dialog = await DialogService.ShowAsync( + $"مدیریت تصاویر «{GetPageDisplayName(summary.PageKey)}»", parameters, + new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Large, FullWidth = true }); + + var result = await dialog.Result; + if (result is { Canceled: false }) + { + await LoadPagesAsync(); + } + } + + private static string GetPageIcon(string pageKey) => pageKey switch + { + "landing" => Icons.Material.Filled.Home, + "about" => Icons.Material.Filled.Info, + "contact" => Icons.Material.Filled.ContactMail, + "licenses" => Icons.Material.Filled.VerifiedUser, + _ => Icons.Material.Filled.Article + }; + + private static string GetPageDisplayName(string pageKey) => pageKey switch + { + "landing" => "صفحه اصلی", + "about" => "درباره ما", + "contact" => "تماس با ما", + "licenses" => "مجوزها", + _ => pageKey + }; +} diff --git a/src/BackOffice/Services/Content/ISitePageSettingsService.cs b/src/BackOffice/Services/Content/ISitePageSettingsService.cs new file mode 100644 index 0000000..dca372c --- /dev/null +++ b/src/BackOffice/Services/Content/ISitePageSettingsService.cs @@ -0,0 +1,96 @@ +namespace BackOffice.Services.Content; + +public interface ISitePageSettingsService +{ + Task> GetAllAsync(); + Task GetByKeyAsync(string pageKey); + Task SaveSettingsAsync(SavePageSettingsDto dto); + Task SaveImageAsync(SavePageImageDto dto); + Task DeleteImageAsync(long id); +} + +// ── Summary DTO (لیست صفحات) ── +public class PageSettingsSummaryDto +{ + public long Id { get; set; } + public string PageKey { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public bool IsActive { get; set; } + public int ImageCount { get; set; } + public DateTime LastModified { get; set; } +} + +// ── Details DTO (تنظیمات کامل + تصاویر) ── +public class PageSettingsDetailsDto +{ + public long Id { get; set; } + public string PageKey { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string? MetaDescription { get; set; } + public string? HeroTitle { get; set; } + public string? HeroSubtitle { get; set; } + public string? HeroImagePath { get; set; } + public bool IsActive { get; set; } + public string? SettingsJson { get; set; } + public List Images { get; set; } = new(); +} + +// ── Image Item DTO ── +public class PageImageItemDto +{ + public long Id { get; set; } + public string ImageGroup { get; set; } = string.Empty; + public string? Title { get; set; } + public string? Subtitle { get; set; } + public string? Description { get; set; } + public string ImagePath { get; set; } = string.Empty; + public string? ThumbnailPath { get; set; } + public string? LinkUrl { get; set; } + public string? IconName { get; set; } + public int SortOrder { get; set; } + public bool IsActive { get; set; } +} + +// ── Save Settings DTO ── +public class SavePageSettingsDto +{ + public string PageKey { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string? MetaDescription { get; set; } + public string? HeroTitle { get; set; } + public string? HeroSubtitle { get; set; } + public bool IsActive { get; set; } = true; + public string? SettingsJson { get; set; } + + // Hero image upload + public byte[]? ImageFile { get; set; } + public string? ImageMime { get; set; } + public string? ImageFileName { get; set; } +} + +// ── Save Image DTO ── +public class SavePageImageDto +{ + public long Id { get; set; } // 0 = جدید + public long SitePageSettingsId { get; set; } + public string ImageGroup { get; set; } = string.Empty; + public string? Title { get; set; } + public string? Subtitle { get; set; } + public string? Description { get; set; } + public string? LinkUrl { get; set; } + public string? IconName { get; set; } + public int SortOrder { get; set; } + public bool IsActive { get; set; } = true; + + // Image upload + public byte[]? ImageFile { get; set; } + public string? ImageMime { get; set; } + public string? ImageFileName { get; set; } +} + +// ── Save Result ── +public class SaveResultDto +{ + public long Id { get; set; } + public bool Success { get; set; } +} diff --git a/src/BackOffice/Services/Content/SitePageSettingsService.cs b/src/BackOffice/Services/Content/SitePageSettingsService.cs new file mode 100644 index 0000000..40f94f4 --- /dev/null +++ b/src/BackOffice/Services/Content/SitePageSettingsService.cs @@ -0,0 +1,131 @@ +using CMSMicroservice.Protobuf.Protos.SitePageSettings; + +namespace BackOffice.Services.Content; + +public class SitePageSettingsService : ISitePageSettingsService +{ + private readonly SitePageSettingsContract.SitePageSettingsContractClient _client; + + public SitePageSettingsService(SitePageSettingsContract.SitePageSettingsContractClient client) + { + _client = client; + } + + public async Task> GetAllAsync() + { + var response = await _client.GetAllPageSettingsAsync(new GetAllPageSettingsRequest()); + + return response.Pages.Select(m => new PageSettingsSummaryDto + { + Id = m.Id, + PageKey = m.PageKey, + Title = m.Title, + IsActive = m.IsActive, + ImageCount = m.ImageCount, + LastModified = m.LastModified?.ToDateTime() ?? DateTime.MinValue + }).ToList(); + } + + public async Task GetByKeyAsync(string pageKey) + { + var response = await _client.GetPageSettingsAsync( + new GetPageSettingsRequest { PageKey = pageKey }); + + if (response == null || response.Id <= 0) return null; + + var dto = new PageSettingsDetailsDto + { + Id = response.Id, + PageKey = response.PageKey, + Title = response.Title, + MetaDescription = response.MetaDescription, + HeroTitle = response.HeroTitle, + HeroSubtitle = response.HeroSubtitle, + HeroImagePath = response.HeroImagePath, + IsActive = response.IsActive, + SettingsJson = response.SettingsJson + }; + + foreach (var img in response.Images) + { + dto.Images.Add(new PageImageItemDto + { + Id = img.Id, + ImageGroup = img.ImageGroup, + Title = img.Title, + Subtitle = img.Subtitle, + Description = img.Description, + ImagePath = img.ImagePath, + ThumbnailPath = img.ThumbnailPath, + LinkUrl = img.LinkUrl, + IconName = img.IconName, + SortOrder = img.SortOrder, + IsActive = img.IsActive + }); + } + + return dto; + } + + public async Task SaveSettingsAsync(SavePageSettingsDto dto) + { + var request = new SavePageSettingsRequest + { + PageKey = dto.PageKey, + Title = dto.Title, + MetaDescription = dto.MetaDescription ?? string.Empty, + HeroTitle = dto.HeroTitle ?? string.Empty, + HeroSubtitle = dto.HeroSubtitle ?? string.Empty, + IsActive = dto.IsActive, + SettingsJson = dto.SettingsJson ?? string.Empty + }; + + if (dto.ImageFile != null && !string.IsNullOrEmpty(dto.ImageMime) && !string.IsNullOrEmpty(dto.ImageFileName)) + { + request.HeroImageFile = new PageSettingsImageFile + { + File = Google.Protobuf.ByteString.CopyFrom(dto.ImageFile), + Mime = dto.ImageMime, + FileName = dto.ImageFileName + }; + } + + var response = await _client.SavePageSettingsAsync(request); + return new SaveResultDto { Id = response.Id, Success = response.Success }; + } + + public async Task SaveImageAsync(SavePageImageDto dto) + { + var request = new SavePageImageRequest + { + Id = dto.Id, + SitePageSettingsId = dto.SitePageSettingsId, + ImageGroup = dto.ImageGroup, + Title = dto.Title ?? string.Empty, + Subtitle = dto.Subtitle ?? string.Empty, + Description = dto.Description ?? string.Empty, + LinkUrl = dto.LinkUrl ?? string.Empty, + IconName = dto.IconName ?? string.Empty, + SortOrder = dto.SortOrder, + IsActive = dto.IsActive + }; + + if (dto.ImageFile != null && !string.IsNullOrEmpty(dto.ImageMime) && !string.IsNullOrEmpty(dto.ImageFileName)) + { + request.ImageFile = new PageSettingsImageFile + { + File = Google.Protobuf.ByteString.CopyFrom(dto.ImageFile), + Mime = dto.ImageMime, + FileName = dto.ImageFileName + }; + } + + var response = await _client.SavePageImageAsync(request); + return new SaveResultDto { Id = response.Id, Success = response.Success }; + } + + public async Task DeleteImageAsync(long id) + { + await _client.DeletePageImageAsync(new DeletePageImageRequest { Id = id }); + } +} diff --git a/src/BackOffice/Shared/NavMenu.razor b/src/BackOffice/Shared/NavMenu.razor index 44f964a..6a61a93 100644 --- a/src/BackOffice/Shared/NavMenu.razor +++ b/src/BackOffice/Shared/NavMenu.razor @@ -231,9 +231,9 @@ @if (CanManageSitePages) { - صفحات سایت + تنظیمات صفحات } From 6147499f9c3d385dae24ed036cad7b66935fa8fe Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Tue, 17 Feb 2026 23:50:34 +0330 Subject: [PATCH 4/5] feat: replace raw JSON editor with Shopify-style typed settings editors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create PageSettingsModels.cs: typed DTOs for landing, about, contact, licenses - LandingSettingsEditor: accordion sections for hero buttons, trust badges, steps, features, stats, testimonials, FAQs, CTA — all with add/remove list management - AboutSettingsEditor: vision/mission text + icon fields + section titles - ContactSettingsEditor: address, phone, email, working hours + social media links - LicensesSettingsEditor: page description + display style selector - PageSettingsEditDialog: auto-switch editor by PageKey, JSON↔Model serialize/deserialize - Fallback to raw JSON textarea for unknown page keys --- .../Components/AboutSettingsEditor.razor | 66 ++++ .../Components/ContactSettingsEditor.razor | 68 ++++ .../Components/LandingSettingsEditor.razor | 332 ++++++++++++++++++ .../Components/LicensesSettingsEditor.razor | 32 ++ .../Components/PageSettingsEditDialog.razor | 35 +- .../PageSettingsEditDialog.razor.cs | 69 ++++ .../SettingsModels/PageSettingsModels.cs | 108 ++++++ 7 files changed, 704 insertions(+), 6 deletions(-) create mode 100644 src/BackOffice/Pages/Content/Components/AboutSettingsEditor.razor create mode 100644 src/BackOffice/Pages/Content/Components/ContactSettingsEditor.razor create mode 100644 src/BackOffice/Pages/Content/Components/LandingSettingsEditor.razor create mode 100644 src/BackOffice/Pages/Content/Components/LicensesSettingsEditor.razor create mode 100644 src/BackOffice/Pages/Content/Components/SettingsModels/PageSettingsModels.cs diff --git a/src/BackOffice/Pages/Content/Components/AboutSettingsEditor.razor b/src/BackOffice/Pages/Content/Components/AboutSettingsEditor.razor new file mode 100644 index 0000000..762f208 --- /dev/null +++ b/src/BackOffice/Pages/Content/Components/AboutSettingsEditor.razor @@ -0,0 +1,66 @@ +@using BackOffice.Pages.Content.Components.SettingsModels + + + + @* ── Vision ── *@ + + + + + + + + + @* ── Mission ── *@ + + + + + + + + + @* ── Section Titles ── *@ + + + + + + + + @* ── Hint ── *@ + + آیتم‌های ارزش‌ها و اعضای تیم از بخش «مدیریت تصاویر» قابل ویرایش هستند. + + + + +@code { + [Parameter] public AboutSettingsModel Model { get; set; } = new(); +} diff --git a/src/BackOffice/Pages/Content/Components/ContactSettingsEditor.razor b/src/BackOffice/Pages/Content/Components/ContactSettingsEditor.razor new file mode 100644 index 0000000..15a2bb3 --- /dev/null +++ b/src/BackOffice/Pages/Content/Components/ContactSettingsEditor.razor @@ -0,0 +1,68 @@ +@using BackOffice.Pages.Content.Components.SettingsModels + + + + @* ── Contact Info ── *@ + + + + + + + + + + + + + + + + @* ── Social Media ── *@ + + + + + + + + + + + +@code { + [Parameter] public ContactSettingsModel Model { get; set; } = new(); +} diff --git a/src/BackOffice/Pages/Content/Components/LandingSettingsEditor.razor b/src/BackOffice/Pages/Content/Components/LandingSettingsEditor.razor new file mode 100644 index 0000000..12bd702 --- /dev/null +++ b/src/BackOffice/Pages/Content/Components/LandingSettingsEditor.razor @@ -0,0 +1,332 @@ +@using BackOffice.Pages.Content.Components.SettingsModels + + + + @* ── Hero Buttons ── *@ + + + + + + + + @* ── Trust Badges ── *@ + + + @for (var i = 0; i < Model.TrustBadges.Count; i++) + { + var index = i; + + + @(index + 1) + + + + + + } + + افزودن نشان + + + + + @* ── Steps ── *@ + + + + @for (var i = 0; i < Model.Steps.Count; i++) + { + var index = i; + + + + + @(index + 1) + + + + + + + + } + + افزودن مرحله + + + + + @* ── Features ── *@ + + + + @for (var i = 0; i < Model.Features.Count; i++) + { + var index = i; + + + + @(index + 1) + + + + + + + + } + + افزودن ویژگی + + + + + @* ── Stats ── *@ + + + + @for (var i = 0; i < Model.Stats.Count; i++) + { + var index = i; + + + @(index + 1) + + + + + آبی + بنفش + سبز + زرد + قرمز + آبی روشن + + + + + } + + افزودن آمار + + + + + @* ── Testimonials ── *@ + + + + @for (var i = 0; i < Model.Testimonials.Count; i++) + { + var index = i; + + + + @(index + 1) + + + + + + + + } + + افزودن نظر + + + + + @* ── FAQs ── *@ + + + + @for (var i = 0; i < Model.Faqs.Count; i++) + { + var index = i; + + + + @(index + 1) + + + + + + + } + + افزودن سوال + + + + + @* ── CTA Section ── *@ + + + + + + + + + @* ── Blog Section ── *@ + + + + + نمایش آخرین مطالب بلاگ + + + + + + +@code { + [Parameter] public LandingSettingsModel Model { get; set; } = new(); + + private bool _featuredBlogEnabled + { + get => Model.FeaturedBlogEnabled ?? true; + set => Model.FeaturedBlogEnabled = value; + } +} diff --git a/src/BackOffice/Pages/Content/Components/LicensesSettingsEditor.razor b/src/BackOffice/Pages/Content/Components/LicensesSettingsEditor.razor new file mode 100644 index 0000000..7426aa1 --- /dev/null +++ b/src/BackOffice/Pages/Content/Components/LicensesSettingsEditor.razor @@ -0,0 +1,32 @@ +@using BackOffice.Pages.Content.Components.SettingsModels + + + + @* ── Page Settings ── *@ + + + + + گرید (Grid) + لیست (List) + اسلایدر (Carousel) + + + + + @* ── Hint ── *@ + + تصاویر مجوزها از بخش «مدیریت تصاویر» قابل افزودن و ویرایش هستند. + + + + +@code { + [Parameter] public LicensesSettingsModel Model { get; set; } = new(); +} diff --git a/src/BackOffice/Pages/Content/Components/PageSettingsEditDialog.razor b/src/BackOffice/Pages/Content/Components/PageSettingsEditDialog.razor index 899e7be..8ab2e3b 100644 --- a/src/BackOffice/Pages/Content/Components/PageSettingsEditDialog.razor +++ b/src/BackOffice/Pages/Content/Components/PageSettingsEditDialog.razor @@ -1,4 +1,5 @@ @using BackOffice.Services.Content +@using BackOffice.Pages.Content.Components.SettingsModels @using Microsoft.AspNetCore.Components.Forms @using BackOffice.Common.BaseComponents @@ -63,12 +64,34 @@ - تنظیمات اختصاصی (JSON) - + + + تنظیمات اختصاصی «@PageTitle» + + + @* ── Typed Settings Editors ── *@ + @switch (Model.PageKey) + { + case "landing": + + break; + case "about": + + break; + case "contact": + + break; + case "licenses": + + break; + default: + + break; + } diff --git a/src/BackOffice/Pages/Content/Components/PageSettingsEditDialog.razor.cs b/src/BackOffice/Pages/Content/Components/PageSettingsEditDialog.razor.cs index 028a351..9095065 100644 --- a/src/BackOffice/Pages/Content/Components/PageSettingsEditDialog.razor.cs +++ b/src/BackOffice/Pages/Content/Components/PageSettingsEditDialog.razor.cs @@ -1,3 +1,5 @@ +using System.Text.Json; +using BackOffice.Pages.Content.Components.SettingsModels; using BackOffice.Services.Content; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Forms; @@ -23,6 +25,70 @@ public partial class PageSettingsEditDialog private string? _imagePreview; private readonly long _maxAllowedSize = (1024 * 1024) * 5; + // ── Typed settings models ── + private LandingSettingsModel _landingSettings = new(); + private AboutSettingsModel _aboutSettings = new(); + private ContactSettingsModel _contactSettings = new(); + private LicensesSettingsModel _licensesSettings = new(); + + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true, + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + protected override void OnParametersSet() + { + DeserializeSettings(); + } + + /// + /// Parse existing SettingsJson into the typed model based on PageKey + /// + private void DeserializeSettings() + { + if (string.IsNullOrWhiteSpace(Model.SettingsJson)) return; + + try + { + switch (Model.PageKey) + { + case "landing": + _landingSettings = JsonSerializer.Deserialize(Model.SettingsJson, JsonOptions) ?? new(); + break; + case "about": + _aboutSettings = JsonSerializer.Deserialize(Model.SettingsJson, JsonOptions) ?? new(); + break; + case "contact": + _contactSettings = JsonSerializer.Deserialize(Model.SettingsJson, JsonOptions) ?? new(); + break; + case "licenses": + _licensesSettings = JsonSerializer.Deserialize(Model.SettingsJson, JsonOptions) ?? new(); + break; + } + } + catch + { + // If JSON is malformed, start with empty model + } + } + + /// + /// Serialize the typed model back to JSON before saving + /// + private void SerializeSettings() + { + Model.SettingsJson = Model.PageKey switch + { + "landing" => JsonSerializer.Serialize(_landingSettings, JsonOptions), + "about" => JsonSerializer.Serialize(_aboutSettings, JsonOptions), + "contact" => JsonSerializer.Serialize(_contactSettings, JsonOptions), + "licenses" => JsonSerializer.Serialize(_licensesSettings, JsonOptions), + _ => Model.SettingsJson // keep raw JSON for unknown pages + }; + } + private async Task OnImageSelected(IBrowserFile? file) { _imageFile = file; @@ -46,6 +112,9 @@ public partial class PageSettingsEditDialog _loading = true; + // Serialize typed model → JSON before saving + SerializeSettings(); + if (_imageFile != null && _imageBuffer != null) { Model.ImageFile = _imageBuffer; diff --git a/src/BackOffice/Pages/Content/Components/SettingsModels/PageSettingsModels.cs b/src/BackOffice/Pages/Content/Components/SettingsModels/PageSettingsModels.cs new file mode 100644 index 0000000..a5f2ae2 --- /dev/null +++ b/src/BackOffice/Pages/Content/Components/SettingsModels/PageSettingsModels.cs @@ -0,0 +1,108 @@ +namespace BackOffice.Pages.Content.Components.SettingsModels; + +// ══════════════════════════════════════════════════════════════ +// LANDING PAGE +// ══════════════════════════════════════════════════════════════ + +public class LandingSettingsModel +{ + public string? HeroButtonPrimaryText { get; set; } + public string? HeroButtonSecondaryText { get; set; } + public List TrustBadges { get; set; } = new(); + public string? StepsTitle { get; set; } + public List Steps { get; set; } = new(); + public string? FeaturesTitle { get; set; } + public List Features { get; set; } = new(); + public string? StatsTitle { get; set; } + public List Stats { get; set; } = new(); + public string? TestimonialsTitle { get; set; } + public List Testimonials { get; set; } = new(); + public string? FaqTitle { get; set; } + public List Faqs { get; set; } = new(); + public string? CtaTitle { get; set; } + public string? CtaDescription { get; set; } + public string? CtaButtonText { get; set; } + public bool? FeaturedBlogEnabled { get; set; } +} + +public class TrustBadgeModel +{ + public string? IconName { get; set; } + public string? Text { get; set; } +} + +public class StepModel +{ + public string? Title { get; set; } + public string? Description { get; set; } +} + +public class FeatureModel +{ + public string? IconName { get; set; } + public string? Title { get; set; } + public string? Description { get; set; } +} + +public class StatModel +{ + public string? Label { get; set; } + public double Value { get; set; } + public string? Suffix { get; set; } + public string? Color { get; set; } +} + +public class TestimonialModel +{ + public string? Quote { get; set; } + public string? Name { get; set; } + public string? Role { get; set; } +} + +public class FaqModel +{ + public string? Question { get; set; } + public string? Answer { get; set; } +} + +// ══════════════════════════════════════════════════════════════ +// ABOUT PAGE +// ══════════════════════════════════════════════════════════════ + +public class AboutSettingsModel +{ + public string? VisionTitle { get; set; } + public string? VisionText { get; set; } + public string? VisionIcon { get; set; } + public string? MissionTitle { get; set; } + public string? MissionText { get; set; } + public string? MissionIcon { get; set; } + public string? ValuesTitle { get; set; } + public string? TeamTitle { get; set; } +} + +// ══════════════════════════════════════════════════════════════ +// CONTACT PAGE +// ══════════════════════════════════════════════════════════════ + +public class ContactSettingsModel +{ + public string? Address { get; set; } + public string? Phone { get; set; } + public string? Email { get; set; } + public string? WorkingHours { get; set; } + public string? TelegramUrl { get; set; } + public string? InstagramUrl { get; set; } + public string? LinkedinUrl { get; set; } + public string? WhatsappUrl { get; set; } +} + +// ══════════════════════════════════════════════════════════════ +// LICENSES PAGE +// ══════════════════════════════════════════════════════════════ + +public class LicensesSettingsModel +{ + public string? PageDescription { get; set; } + public string? DisplayStyle { get; set; } +} From c9affe62bd4a32b4ced9c82878d98af116286efd Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Wed, 18 Feb 2026 00:43:12 +0330 Subject: [PATCH 5/5] feat: smart product autocomplete for inventory dialogs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DiscountProductsAutoComplete: new autocomplete component for discount products with search/debounce (mirrors ProductsAutoComplete pattern) - AddStockDialog: replace manual product ID input with smart autocomplete - When 'regular product' selected → shows ProductsAutoComplete - When 'discount product' selected → shows DiscountProductsAutoComplete - Admin never has to type product IDs manually - Product type selector controls which autocomplete is shown - Validates product selection before submit --- .../DiscountProductsAutoComplete.razor | 14 ++++ .../DiscountProductsAutoComplete.razor.cs | 83 +++++++++++++++++++ .../Inventory/Components/AddStockDialog.razor | 39 ++++++--- 3 files changed, 126 insertions(+), 10 deletions(-) create mode 100644 src/BackOffice/Pages/AutoComplete/DiscountProductsAutoComplete.razor create mode 100644 src/BackOffice/Pages/AutoComplete/DiscountProductsAutoComplete.razor.cs diff --git a/src/BackOffice/Pages/AutoComplete/DiscountProductsAutoComplete.razor b/src/BackOffice/Pages/AutoComplete/DiscountProductsAutoComplete.razor new file mode 100644 index 0000000..70b2886 --- /dev/null +++ b/src/BackOffice/Pages/AutoComplete/DiscountProductsAutoComplete.razor @@ -0,0 +1,14 @@ +@using CMSMicroservice.Protobuf.Protos.DiscountProduct + + diff --git a/src/BackOffice/Pages/AutoComplete/DiscountProductsAutoComplete.razor.cs b/src/BackOffice/Pages/AutoComplete/DiscountProductsAutoComplete.razor.cs new file mode 100644 index 0000000..3853fed --- /dev/null +++ b/src/BackOffice/Pages/AutoComplete/DiscountProductsAutoComplete.razor.cs @@ -0,0 +1,83 @@ +using CMSMicroservice.Protobuf.Protos.DiscountProduct; +using Microsoft.AspNetCore.Components; + +namespace BackOffice.Pages.AutoComplete; + +public partial class DiscountProductsAutoComplete +{ + [Inject] public DiscountProductContract.DiscountProductContractClient DiscountProductService { get; set; } = default!; + + [Parameter] public long? Value { get; set; } + [Parameter] public string? Label { get; set; } = "محصول تخفیفی"; + [Parameter] public EventCallback ValueChanged { get; set; } + + private List _items = new(); + private DiscountProductDto? _item; + + protected override async void OnParametersSet() + { + await base.OnParametersSetAsync(); + + if (Value.HasValue && Value.Value > 0 && (_item == null || _item.Id != Value.Value)) + { + try + { + var getAll = await DiscountProductService.GetDiscountProductsAsync(new GetDiscountProductsRequest + { + PageNumber = 1, + PageSize = 50 + }); + + if (getAll?.Models != null) + { + _item = getAll.Models.FirstOrDefault(x => x.Id == Value.Value); + StateHasChanged(); + } + } + catch + { + // Silently handle + } + } + } + + private async Task OnSelected(DiscountProductDto? model) + { + if (model == null) + { + _item = null; + await ValueChanged.InvokeAsync(null); + } + else + { + _item = model; + await ValueChanged.InvokeAsync(model.Id); + } + } + + private async Task> Search(string value, CancellationToken token) + { + var request = new GetDiscountProductsRequest + { + PageNumber = 1, + PageSize = 9 + }; + + if (!string.IsNullOrWhiteSpace(value)) + { + request.SearchQuery = value; + } + + try + { + var getAll = await DiscountProductService.GetDiscountProductsAsync(request); + _items = getAll?.Models?.ToList() ?? new List(); + } + catch + { + _items = new List(); + } + + return _items; + } +} diff --git a/src/BackOffice/Pages/Inventory/Components/AddStockDialog.razor b/src/BackOffice/Pages/Inventory/Components/AddStockDialog.razor index 08e7f37..c51c21e 100644 --- a/src/BackOffice/Pages/Inventory/Components/AddStockDialog.razor +++ b/src/BackOffice/Pages/Inventory/Components/AddStockDialog.razor @@ -1,6 +1,7 @@ @using CMSMicroservice.Protobuf.Protos.Inventory @using Google.Protobuf.WellKnownTypes @using InventoryProductType = CMSMicroservice.Protobuf.Protos.Inventory.ProductType +@using BackOffice.Pages.AutoComplete @@ -19,12 +20,16 @@ - + @if (_productType == InventoryProductType.RegularProduct) + { + + } + else + { + + } } else @@ -103,7 +108,8 @@ private bool _isSubmitting; private InventoryProductType _productType = InventoryProductType.RegularProduct; - private long _productId; + private long? _productId; + private long? _discountProductId; private long? _warehouseId; private int _quantity = 1; private string _referenceNumber = string.Empty; @@ -111,9 +117,12 @@ protected override void OnInitialized() { - if (ProductIdParam.HasValue) + if (ProductIdParam.HasValue && ProductTypeParam.HasValue) { - _productId = ProductIdParam.Value; + if (ProductTypeParam.Value == InventoryProductType.DiscountProduct) + _discountProductId = ProductIdParam.Value; + else + _productId = ProductIdParam.Value; } if (ProductTypeParam.HasValue) { @@ -144,9 +153,19 @@ _isSubmitting = true; try { + var selectedProductId = ProductIdParam + ?? (_productType == InventoryProductType.DiscountProduct ? _discountProductId : _productId); + + if (!selectedProductId.HasValue || selectedProductId.Value <= 0) + { + Snackbar.Add("لطفاً یک محصول انتخاب کنید", Severity.Warning); + _isSubmitting = false; + return; + } + var request = new AddStockRequest { - ProductId = ProductIdParam ?? _productId, + ProductId = selectedProductId.Value, ProductType = ProductTypeParam ?? _productType, Quantity = _quantity, ReferenceNumber = _referenceNumber ?? string.Empty,