From 313a714a997447ce449ddfae68663741d25cc02365bb8d0ef103ab4aea515f9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= <93217193+Daniel-Garmig@users.noreply.github.com> Date: Fri, 4 Sep 2026 20:38:00 +0200 Subject: [PATCH] feat StayExport - Stay Export Pages - Initial implementation --- src/Somnia.GuestLog/Components/App.razor | 1 + .../Components/Layout/NavMenu.razor | 3 + .../Pages/StayExport/StayExportList.razor | 177 ++++++++++++++++++ .../Pages/StayExport/StayExportNew.razor | 166 ++++++++++++++++ .../Components/Pages/_Imports.razor | 3 +- src/Somnia.GuestLog/GuestConsts.cs | 3 + src/Somnia.GuestLog/Program.cs | 1 + .../Resources/LanguageLocale.resx | 37 +++- .../Entity/IStayExportFileEntityService.cs | 2 +- .../Entity/StayExportFileEntityService.cs | 9 +- src/Somnia.GuestLog/Utils/QueryExtensions.cs | 5 + .../StayExportParamsDTOValidator.cs | 23 +++ src/Somnia.GuestLog/wwwroot/js/App.js | 13 ++ 13 files changed, 434 insertions(+), 9 deletions(-) create mode 100644 src/Somnia.GuestLog/Components/Pages/StayExport/StayExportList.razor create mode 100644 src/Somnia.GuestLog/Components/Pages/StayExport/StayExportNew.razor create mode 100644 src/Somnia.GuestLog/Validators/StayExportParamsDTOValidator.cs create mode 100644 src/Somnia.GuestLog/wwwroot/js/App.js diff --git a/src/Somnia.GuestLog/Components/App.razor b/src/Somnia.GuestLog/Components/App.razor index aa3de14..f90fa34 100644 --- a/src/Somnia.GuestLog/Components/App.razor +++ b/src/Somnia.GuestLog/Components/App.razor @@ -23,6 +23,7 @@ + diff --git a/src/Somnia.GuestLog/Components/Layout/NavMenu.razor b/src/Somnia.GuestLog/Components/Layout/NavMenu.razor index 6969cad..24364a9 100644 --- a/src/Somnia.GuestLog/Components/Layout/NavMenu.razor +++ b/src/Somnia.GuestLog/Components/Layout/NavMenu.razor @@ -13,4 +13,7 @@ @LanguageLocale.navMenu_StayEntryList + + @LanguageLocale.navMenu_StayExport + diff --git a/src/Somnia.GuestLog/Components/Pages/StayExport/StayExportList.razor b/src/Somnia.GuestLog/Components/Pages/StayExport/StayExportList.razor new file mode 100644 index 0000000..97ca1aa --- /dev/null +++ b/src/Somnia.GuestLog/Components/Pages/StayExport/StayExportList.razor @@ -0,0 +1,177 @@ +@attribute [Route($"{GuestConsts.Page_StayExport_List}")] + +@using FluentStorage; +@using FluentStorage.Storage; +@using System.Text.Json; +@using Microsoft.Extensions.Options; +@using Somnia.GuestLog.Models.DTOs; +@using Somnia.GuestLog.Models.Configuration; +@using Somnia.GuestLog.Services; + +@inject IStayExportFileEntityService stayExportEntityService +@inject NavigationManager Navigation +@inject IUserMessageService userMessage +@inject IDialogService DialogService +@inject IDataListService dataListService; +@inject ILocalizerService Localizer; + +@inject IOptions exportConfig +@inject IJSRuntime JS + + +Somnia - Stay Export Files + + + @LanguageLocale.page_StayExportList_pageTitle + + + @LanguageLocale.general_btnSearch + + + @LanguageLocale.page_StayExportList_btnNewExport + + + +@* + FILTER + *@ + + + + @LanguageLocale.page_StayExportList_exportList + + + + + + + + + + + + + + + + + + +@code { + private bool _dataLoading = true; + private const int resultLimit = 200; + + private IEnumerable stayExportList = []; + + private MudDataGrid _dataGrid = null!; + private ConfirmDeleteMsgBox _confirmDeleteMsgBox = null!; + + + protected override async Task OnInitializedAsync() + { + await LoadDataAsync(showMsg: false); + } + + private async Task LoadDataAsync(bool showMsg = true) + { + _dataLoading = true; + + stayExportList = await stayExportEntityService.GetDtoListAsync(limit: resultLimit); + + if (showMsg) + { + userMessage.ShowMessage("Data loaded from DB", MessageKind.Info); + } + _dataLoading = false; + } + + + #region Event Handlers + + private async Task BtnSearchDataAsync() + { + await LoadDataAsync(); + } + + private void BtnNewExport() + { + Navigation.NavigateTo(GuestConsts.Page_StayExport_New); + } + + + private async Task ShowExportParams(StayExportFileDTO dto) + { + string jsonParams = JsonSerializer.Serialize(dto.ExportParams, options: new() { WriteIndented = true }); + + await DialogService.ShowMessageBoxAsync("Details", jsonParams); + } + + private async Task BtnDownloadFile(StayExportFileDTO item) + { + if(item.ExportResult == Enums.Models.StayExportFileResult.NoData) + { + userMessage.ShowMessage("A Export without data can't be downloaded!", MessageKind.Warning); + return; + } + + if (string.IsNullOrEmpty(item.ExportedPath)) + { + userMessage.ShowMessage("File can't be downloaded: no path found!", MessageKind.Warning); + return; + } + + IStore store = StorageFactory.FromConnectionString(exportConfig.Value.ExportStorageConnectionString); + + using Stream objectStream = new MemoryStream(); + await store.GetObject(item.ExportedPath, objectStream); + objectStream.Seek(0, SeekOrigin.Begin); + + using var streamRef = new DotNetStreamReference(stream: objectStream); + + var fileName = Path.GetFileName(item.ExportedPath); + await JS.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef); + } + + private async Task BtnDeleteItem(StayExportFileDTO item) + { + userMessage.ShowMessage("Not Implemented!", MessageKind.Normal); + @* bool? confirmDelete = await _confirmDeleteMsgBox.MsgBox.ShowAsync(); + + if (confirmDelete is null || confirmDelete.Value == false) + { + return; + } + + Result deleteResult = await stayExportEntityService.Delete(item); + if (!deleteResult.IsSuccess) + { + string errorMsg = $"Error deleting Stay {item.Id}"; + userMessage.ShowResultWithMessage(deleteResult, errorMsg); + return; + } + + userMessage.ShowMessage($"Stay {item.Id} deleted!", MessageKind.Success); + await LoadDataAsync(); *@ + } + + #endregion + +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/Components/Pages/StayExport/StayExportNew.razor b/src/Somnia.GuestLog/Components/Pages/StayExport/StayExportNew.razor new file mode 100644 index 0000000..6d1ee8c --- /dev/null +++ b/src/Somnia.GuestLog/Components/Pages/StayExport/StayExportNew.razor @@ -0,0 +1,166 @@ +@attribute [Route($"{GuestConsts.Page_StayExport_New}")] + +@using FluentValidation; +@using FluentStorage; +@using FluentStorage.Storage; +@using Microsoft.Extensions.Options; +@using Somnia.GuestLog.Models.DTOs; +@using Somnia.GuestLog.Models.Configuration; +@using Somnia.GuestLog.Enums.Models; +@using Somnia.GuestLog.Services; +@using Somnia.GuestLog.Validators; +@using Somnia.GuestLog.Services.StayExport; + + +@inject IUserMessageService userMessage +@inject IDialogService DialogService +@inject NavigationManager Navigation +@inject IValidator ExportParamsValidator; +@inject ILocalizerService Localizer; +@inject IStayExportServiceFactory ExportServiceFactory; + +@inject IOptions exportConfig +@inject IJSRuntime JS + + +Somnia - Stay Export - New + + + + @LanguageLocale.page_StayExportNew_pageTitle + + + + @LanguageLocale.general_btnCreate + + + @LanguageLocale.general_btnReturn + + + + + + + + @foreach (StayExportFormat option in Enum.GetValues()) + { + + @Localizer.GetEnumValue(option) + + } + + + @foreach (StayExportParamType option in Enum.GetValues()) + { + + @Localizer.GetEnumValue(option) + + } + + + + @if (_showPeriodFields) + { + + + + + + + } + + + +@if (_isExported && exportedFile != null) +{ + + + File Exported! + + + Download Exported File + + +} + +@code { + MudForm _form = null!; + + StayExportFormat? exportFormat = null; + StayExportParamsDTO exportParams = new(); + StayExportFileDTO? exportedFile = null; + + bool _showPeriodFields => exportParams.ExportType == StayExportParamType.PeriodDateIn; + bool _isExported => exportedFile != null; + + + private void BtnReturn() + { + Navigation.NavigateTo(GuestConsts.Page_StayExport_List); + } + + private async Task BtnCreateExport() + { + await _form.ValidateAsync(); + if (!_form.IsValid || exportFormat == null) + { + userMessage.ShowMessage("Invalid Form!", MessageKind.Warning); + return; + } + + var loadingDialog = await DialogService.ShowAsync(); + + var exportService = ExportServiceFactory.GetService(exportFormat.Value); + var exportResult = await exportService.ExportStaysByParams(exportParams); + + exportedFile = exportResult.Value; + userMessage.ShowMessage( + $"Stay exported: {exportResult.ResultCode} with export result: {exportedFile?.ExportResult} to '{exportedFile?.ExportedPath ?? "???"}'", + MessageKind.Info); + + loadingDialog.Close(); + + @* Result createResult = await stayService.CreateEntryAsync(model); + if (!createResult.IsSuccess) + { + userMessage.ShowResultWithMessage(createResult, $"Error creating StayEntry"); + return; + } *@ + } + + private async Task BtnDownloadExportedFile() + { + Debug.Assert(exportedFile != null); + Debug.Assert(exportConfig.Value.ExportStorageConnectionString != null); + + if (string.IsNullOrEmpty(exportedFile.ExportedPath)) + { + userMessage.ShowMessage("Exported File can't be downloaded as no file was saved", MessageKind.Warning); + return; + } + + IStore store = StorageFactory.FromConnectionString(exportConfig.Value.ExportStorageConnectionString); + + using Stream objectStream = new MemoryStream(); + await store.GetObject(exportedFile.ExportedPath, objectStream); + objectStream.Seek(0, SeekOrigin.Begin); + + var fileName = Path.GetFileName(exportedFile.ExportedPath); + + using var streamRef = new DotNetStreamReference(stream: objectStream); + + await JS.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef); + } + +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/Components/Pages/_Imports.razor b/src/Somnia.GuestLog/Components/Pages/_Imports.razor index be91a40..abb77d5 100644 --- a/src/Somnia.GuestLog/Components/Pages/_Imports.razor +++ b/src/Somnia.GuestLog/Components/Pages/_Imports.razor @@ -1,2 +1,3 @@ @using Somnia.GuestLog.Components.Controls -@using Somnia.GuestLog.Services.Entity; \ No newline at end of file +@using Somnia.GuestLog.Services.Entity; +@using System.Diagnostics; \ No newline at end of file diff --git a/src/Somnia.GuestLog/GuestConsts.cs b/src/Somnia.GuestLog/GuestConsts.cs index af7a4e2..601e684 100644 --- a/src/Somnia.GuestLog/GuestConsts.cs +++ b/src/Somnia.GuestLog/GuestConsts.cs @@ -13,6 +13,9 @@ public static class GuestConsts public const string Page_StayEntry_Detail = $"/stayEntries/d/{Page_StayEntry_Detail_Param_Id}"; public const string Page_StayEntry_Detail_Param_Id = "{StayId:long}"; + public const string Page_StayExport_List = "/stayExport"; + public const string Page_StayExport_New = "/stayExport/new"; + public const string QueryParam_ReturnUrl = "returnUrl"; diff --git a/src/Somnia.GuestLog/Program.cs b/src/Somnia.GuestLog/Program.cs index da4d9b1..16f4aac 100644 --- a/src/Somnia.GuestLog/Program.cs +++ b/src/Somnia.GuestLog/Program.cs @@ -55,6 +55,7 @@ builder.Services.AddHttpClient(); builder.Services.AddSingleton, GuestDocumentDTOValidator>(); builder.Services.AddSingleton, GuestDocIdDTOValidator>(); builder.Services.AddSingleton, StayEntryDTOValidator>(); +builder.Services.AddSingleton, StayExportParamsDTOValidator>(); // Add Entity Services builder.Services.AddSingleton(); diff --git a/src/Somnia.GuestLog/Resources/LanguageLocale.resx b/src/Somnia.GuestLog/Resources/LanguageLocale.resx index 818e6c5..92b2211 100644 --- a/src/Somnia.GuestLog/Resources/LanguageLocale.resx +++ b/src/Somnia.GuestLog/Resources/LanguageLocale.resx @@ -149,6 +149,17 @@ Creation Time Update Time + Id + Export Format + Export Params + Export Result + Export Path + Export Time + + Export Type + Date From + Date To + TMP Actions @@ -177,8 +188,8 @@ State Country Zip Code - - + + OK Unknown Error @@ -187,10 +198,21 @@ Guest (ID: '{0}') is in a stay. Guest (ID: '{0}') is already in one stay during the period: {1} - + + OK + No Data + Export Error + File Error + + GuestLog JSON + Spain Parte Viajeros + + Stay 'Date In' Period + Guests Stays + Stay Export Guest Documents @@ -205,7 +227,14 @@ New Stay New Stay - + + Stay Export Files + Export List + New Export + + New Stay Export + + Document Details Guest Details Contact Details diff --git a/src/Somnia.GuestLog/Services/Entity/IStayExportFileEntityService.cs b/src/Somnia.GuestLog/Services/Entity/IStayExportFileEntityService.cs index 6b9f116..1f9ecac 100644 --- a/src/Somnia.GuestLog/Services/Entity/IStayExportFileEntityService.cs +++ b/src/Somnia.GuestLog/Services/Entity/IStayExportFileEntityService.cs @@ -9,5 +9,5 @@ public interface IStayExportFileEntityService public Task GetDtoByIdAsync(long id); //public Task> GetDtoListByFilterAsync(StayExportFileFilterDTO filter, int limit = 0); - public Task> GetDtoListAsync(); + public Task> GetDtoListAsync(int limit = 0); } \ No newline at end of file diff --git a/src/Somnia.GuestLog/Services/Entity/StayExportFileEntityService.cs b/src/Somnia.GuestLog/Services/Entity/StayExportFileEntityService.cs index e1f3219..99456a1 100644 --- a/src/Somnia.GuestLog/Services/Entity/StayExportFileEntityService.cs +++ b/src/Somnia.GuestLog/Services/Entity/StayExportFileEntityService.cs @@ -4,6 +4,7 @@ using Somnia.GuestLog.Infrastructure; using Somnia.GuestLog.Models; using Somnia.GuestLog.Models.DTOs; using Somnia.GuestLog.Models.Entities; +using Somnia.GuestLog.Utils; namespace Somnia.GuestLog.Services.Entity; @@ -69,13 +70,15 @@ public class StayExportFileEntityService( return dbItem is not null ? GetDtoFromDb(dbItem) : null; } - public async Task> GetDtoListAsync() + public async Task> GetDtoListAsync(int limit = 0) { using var dbContext = contextFactory.CreateDbContext(); - var dbList = await dbContext.StayExportFiles + IQueryable query = dbContext.StayExportFiles .OrderByDescending(e => e.ExportTime) - .ToListAsync(); + .WithLimit(limit); + + var dbList = await query.ToListAsync(); return dbList.Select(e => GetDtoFromDb(e)); } diff --git a/src/Somnia.GuestLog/Utils/QueryExtensions.cs b/src/Somnia.GuestLog/Utils/QueryExtensions.cs index e512d17..cddbaee 100644 --- a/src/Somnia.GuestLog/Utils/QueryExtensions.cs +++ b/src/Somnia.GuestLog/Utils/QueryExtensions.cs @@ -8,4 +8,9 @@ public static class QueryExtensions { return condition ? source.Where(predicate) : source; } + + public static IQueryable WithLimit(this IQueryable source, int limit) + { + return limit > 0 ? source.Take(limit) : source; + } } diff --git a/src/Somnia.GuestLog/Validators/StayExportParamsDTOValidator.cs b/src/Somnia.GuestLog/Validators/StayExportParamsDTOValidator.cs new file mode 100644 index 0000000..4245963 --- /dev/null +++ b/src/Somnia.GuestLog/Validators/StayExportParamsDTOValidator.cs @@ -0,0 +1,23 @@ +using FluentValidation; +using Somnia.GuestLog.Enums.Models; +using Somnia.GuestLog.Models.DTOs; + +namespace Somnia.GuestLog.Validators; + +public class StayExportParamsDTOValidator : ValueValidator +{ + public StayExportParamsDTOValidator() + { + RuleFor(e => e.ExportType) + .NotEmpty(); + + RuleFor(e => e.ExportPeriodStart) + .NotEmpty() + .When(e => e.ExportType == StayExportParamType.PeriodDateIn); + + RuleFor(e => e.ExportPeriodEnd) + .NotEmpty() + .When(e => e.ExportType == StayExportParamType.PeriodDateIn); + + } +} diff --git a/src/Somnia.GuestLog/wwwroot/js/App.js b/src/Somnia.GuestLog/wwwroot/js/App.js new file mode 100644 index 0000000..64522b2 --- /dev/null +++ b/src/Somnia.GuestLog/wwwroot/js/App.js @@ -0,0 +1,13 @@ + +// Function to download files from Blazor pages +window.downloadFileFromStream = async (fileName, contentStreamReference) => { + const arrayBuffer = await contentStreamReference.arrayBuffer(); + const blob = new Blob([arrayBuffer]); + const url = URL.createObjectURL(blob); + const anchorElement = document.createElement('a'); + anchorElement.href = url; + anchorElement.download = fileName ?? ''; + anchorElement.click(); + anchorElement.remove(); + URL.revokeObjectURL(url); +}