From 67845e250fad8c942508e4441d2eb31fc894e794af42c231f9d7b39aae91259f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20Garc=C3=ADa?=
<93217193+Daniel-Garmig@users.noreply.github.com>
Date: Mon, 17 Aug 2026 21:28:26 +0200
Subject: [PATCH] GuestLog - StayEntry detail view and improvements: - Added
detail view for StayEntries. - Added button to delete StayEntries. - Improve
queries of StayEntries using a "query config" record. - Enabled LazyLoading
for all database relations. This will make easier to find cases where
required relations aren't explicitly loaded.
---
Directory.Packages.props | 1 +
.../Controls/ConfirmDeleteMsgBox.razor | 18 +++++
.../Components/Forms/StayEntryForm.razor | 29 +++++---
.../Pages/GuestDocuments/GuestDocList.razor | 16 +----
.../Pages/StayEntries/StayEntryDetail.razor | 70 +++++++++++++++++++
.../Pages/StayEntries/StayEntryList.razor | 35 +++++++++-
src/Somnia.GuestLog/GuestConsts.cs | 2 +
.../Models/DTOs/StayEntryDTO.cs | 2 +-
src/Somnia.GuestLog/Models/EntityMapping.cs | 4 +-
.../QueryConfigs/StayEntryQueryConfig.cs | 34 +++++++++
src/Somnia.GuestLog/Program.cs | 13 +++-
.../Services/IStayEntryService.cs | 7 +-
.../Services/StayEntryService.cs | 41 +++++++----
src/Somnia.GuestLog/Somnia.GuestLog.csproj | 1 +
.../Validators/StayEntryDTOValidator.cs | 1 +
15 files changed, 229 insertions(+), 45 deletions(-)
create mode 100644 src/Somnia.GuestLog/Components/Controls/ConfirmDeleteMsgBox.razor
create mode 100644 src/Somnia.GuestLog/Components/Pages/StayEntries/StayEntryDetail.razor
create mode 100644 src/Somnia.GuestLog/Models/QueryConfigs/StayEntryQueryConfig.cs
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 04460a3..d52b22e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -23,6 +23,7 @@
+
\ No newline at end of file
diff --git a/src/Somnia.GuestLog/Components/Controls/ConfirmDeleteMsgBox.razor b/src/Somnia.GuestLog/Components/Controls/ConfirmDeleteMsgBox.razor
new file mode 100644
index 0000000..7cdec14
--- /dev/null
+++ b/src/Somnia.GuestLog/Components/Controls/ConfirmDeleteMsgBox.razor
@@ -0,0 +1,18 @@
+
+
+ @LanguageLocale.general_confirmDeleteMsg
+
+
+
+ @LanguageLocale.general_btnDelete
+
+
+
+
+@code {
+ public MudMessageBox MsgBox => _msgBox;
+
+ MudMessageBox _msgBox = null!;
+
+}
\ No newline at end of file
diff --git a/src/Somnia.GuestLog/Components/Forms/StayEntryForm.razor b/src/Somnia.GuestLog/Components/Forms/StayEntryForm.razor
index 6fa0c10..f8491b0 100644
--- a/src/Somnia.GuestLog/Components/Forms/StayEntryForm.razor
+++ b/src/Somnia.GuestLog/Components/Forms/StayEntryForm.razor
@@ -1,5 +1,5 @@
+@using System.Diagnostics
@using FluentValidation;
-@using Microsoft.AspNetCore.SignalR
@using Somnia.GuestLog.Models.DTOs;
@using Somnia.GuestLog.Services;
@using Somnia.GuestLog.Validators;
@@ -143,7 +143,7 @@
{
await _form.ValidateAsync();
- if (Model.GuestDocuments.Count == 0)
+ if (Model.GuestDocuments is null || Model.GuestDocuments.Count == 0)
{
snackbar.Add("At least 1 guest is required!", MudBlazor.Severity.Warning);
return false;
@@ -198,12 +198,21 @@
}
//Check if this guest is already added.
- bool alreadyAdded = Model.GuestDocuments.Any(e => e.DocumentType == resultData.DocumentType
- && e.DocumentNum == resultData.DocumentNum);
- if (alreadyAdded)
+ if (Model.GuestDocuments is not null && Model.GuestDocuments.Count > 0)
{
- snackbar.Add("GuestDocument already added!", MudBlazor.Severity.Warning);
- return;
+ bool alreadyAdded = Model.GuestDocuments.Any(e => e.DocumentType == resultData.DocumentType
+ && e.DocumentNum == resultData.DocumentNum);
+ if (alreadyAdded)
+ {
+ snackbar.Add("GuestDocument already added!", MudBlazor.Severity.Warning);
+ return;
+ }
+ }
+
+ //Init List.
+ if (Model.GuestDocuments is null)
+ {
+ Model.GuestDocuments = [];
}
Model.GuestDocuments.Add(resultData);
@@ -211,8 +220,12 @@
private void BtnDeleteGuest(string docType, string docNum)
{
+ Debug.Assert(Model.GuestDocuments is not null);
+
//Filter out given GuestDocument from model.
- Model.GuestDocuments = Model.GuestDocuments.Where(e => !(e.DocumentType == docType && e.DocumentNum == docNum)).ToList();
+ Model.GuestDocuments = Model.GuestDocuments
+ .Where(e => !(e.DocumentType == docType && e.DocumentNum == docNum))
+ .ToList();
}
#endregion
diff --git a/src/Somnia.GuestLog/Components/Pages/GuestDocuments/GuestDocList.razor b/src/Somnia.GuestLog/Components/Pages/GuestDocuments/GuestDocList.razor
index 68eb8cb..762a045 100644
--- a/src/Somnia.GuestLog/Components/Pages/GuestDocuments/GuestDocList.razor
+++ b/src/Somnia.GuestLog/Components/Pages/GuestDocuments/GuestDocList.razor
@@ -69,17 +69,7 @@
-
-
- @LanguageLocale.general_confirmDeleteMsg
-
-
-
- @LanguageLocale.general_btnDelete
-
-
-
+
@code {
@@ -92,7 +82,7 @@
private GuestDocFilterDTO docFilter = new GuestDocFilterDTO();
private MudDataGrid _dataGrid = null!;
- private MudMessageBox _msgBoxConfirmDelete = null!;
+ private ConfirmDeleteMsgBox _msgBoxConfirmDelete = null!;
protected override async Task OnInitializedAsync()
@@ -145,7 +135,7 @@
private async Task BtnDeleteItem(GuestDocumentDTO item)
{
- bool? confirmDelete = await _msgBoxConfirmDelete.ShowAsync();
+ bool? confirmDelete = await _msgBoxConfirmDelete.MsgBox.ShowAsync();
if (confirmDelete is null || confirmDelete.Value == false)
{
diff --git a/src/Somnia.GuestLog/Components/Pages/StayEntries/StayEntryDetail.razor b/src/Somnia.GuestLog/Components/Pages/StayEntries/StayEntryDetail.razor
new file mode 100644
index 0000000..fed7887
--- /dev/null
+++ b/src/Somnia.GuestLog/Components/Pages/StayEntries/StayEntryDetail.razor
@@ -0,0 +1,70 @@
+@attribute [Route($"{GuestConsts.Page_StayEntry_Detail}")]
+
+@using Somnia.GuestLog.Models.DTOs;
+@using Somnia.GuestLog.Services;
+
+@inject ISnackbar Snackbar
+@inject IDialogService DialogService
+@inject NavigationManager Navigation
+@inject IGuestDocumentService guestDocService
+@inject IStayEntryService stayService
+
+
+Somnia - Guest Docs - Details
+
+
+ Stay Entry - "@StayId"
+
+
+ @LanguageLocale.general_btnEdit
+
+
+ @LanguageLocale.general_btnReturn
+
+
+
+@if (stayModel is not null)
+{
+
+}
+
+
+@code {
+
+ [Parameter]
+ public long StayId { get; set; }
+
+ StayEntryDTO? stayModel = null;
+
+ protected override async Task OnInitializedAsync()
+ {
+ @* if (StayId is default)
+ {
+ Snackbar.Add("No Id provided!!", Severity.Error);
+ Navigation.NotFound();
+ return;
+ } *@
+
+ //Get model from db.
+ stayModel = await stayService.GetDtoByIdAsync(StayId, queryConfig: new(IncludeGuests: true));
+ if (stayModel is null)
+ {
+ Snackbar.Add($"Stay not found with ID: '{StayId}'", Severity.Error);
+ Navigation.NotFound();
+ return;
+ }
+ }
+
+ private void BtnEdit()
+ {
+ Snackbar.Add("Not implemented yet!", Severity.Info);
+ }
+
+ private void BtnReturn()
+ {
+ Navigation.NavigateTo(GuestConsts.Page_StayEntry_List);
+ }
+
+}
\ No newline at end of file
diff --git a/src/Somnia.GuestLog/Components/Pages/StayEntries/StayEntryList.razor b/src/Somnia.GuestLog/Components/Pages/StayEntries/StayEntryList.razor
index ea39c8c..7fed61c 100644
--- a/src/Somnia.GuestLog/Components/Pages/StayEntries/StayEntryList.razor
+++ b/src/Somnia.GuestLog/Components/Pages/StayEntries/StayEntryList.razor
@@ -43,7 +43,7 @@
- @*
- *@
+
+
+
@code {
private bool _dataLoading = true;
@@ -67,6 +69,7 @@
private IEnumerable stayEntryList = [];
private MudDataGrid _dataGrid = null!;
+ private ConfirmDeleteMsgBox _confirmDeleteMsgBox = null!;
protected override async Task OnInitializedAsync()
@@ -102,6 +105,34 @@
Navigation.NavigateTo(GuestConsts.Page_StayEntry_New);
}
+
+ private async Task BtnViewDetails(StayEntryDTO item)
+ {
+ string itemId = $"{item.Id ?? -1}";
+ string uri = GuestConsts.Page_StayEntry_Detail.Replace(GuestConsts.Page_StayEntry_Detail_Param_Id, itemId);
+ Navigation.NavigateTo(uri);
+ }
+
+ private async Task BtnDeleteItem(StayEntryDTO item)
+ {
+ bool? confirmDelete = await _confirmDeleteMsgBox.MsgBox.ShowAsync();
+
+ if (confirmDelete is null || confirmDelete.Value == false)
+ {
+ return;
+ }
+
+ bool deleted = await StayEntryService.DeleteEntryAsync(item);
+ if (!deleted)
+ {
+ Snackbar.Add($"Error deleting Stay {item.Id}", Severity.Error);
+ return;
+ }
+
+ Snackbar.Add($"Stay {item.Id} deleted!", Severity.Success);
+ await LoadDataAsync();
+ }
+
#endregion
}
\ No newline at end of file
diff --git a/src/Somnia.GuestLog/GuestConsts.cs b/src/Somnia.GuestLog/GuestConsts.cs
index dea0f0b..ed23428 100644
--- a/src/Somnia.GuestLog/GuestConsts.cs
+++ b/src/Somnia.GuestLog/GuestConsts.cs
@@ -10,6 +10,8 @@ public static class GuestConsts
public const string Page_StayEntry_List = "/stayEntries";
public const string Page_StayEntry_New = "/stayEntries/new";
+ 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 DataList_DocumentTypes = "DocumentType";
diff --git a/src/Somnia.GuestLog/Models/DTOs/StayEntryDTO.cs b/src/Somnia.GuestLog/Models/DTOs/StayEntryDTO.cs
index 94fa01e..90a42aa 100644
--- a/src/Somnia.GuestLog/Models/DTOs/StayEntryDTO.cs
+++ b/src/Somnia.GuestLog/Models/DTOs/StayEntryDTO.cs
@@ -12,7 +12,7 @@ public class StayEntryDTO
public string? PaymentMethod { get; set; }
- public List GuestDocuments { get; set; } = [];
+ public List? GuestDocuments { get; set; } = null;
public DateTime? CreateTime { get; set; }
diff --git a/src/Somnia.GuestLog/Models/EntityMapping.cs b/src/Somnia.GuestLog/Models/EntityMapping.cs
index 332d749..6b0dfee 100644
--- a/src/Somnia.GuestLog/Models/EntityMapping.cs
+++ b/src/Somnia.GuestLog/Models/EntityMapping.cs
@@ -33,7 +33,7 @@ internal static class EntityMapping
};
}
- public static StayEntryDTO MapStayEntryEntityToDTO(StayEntry entity)
+ public static StayEntryDTO MapStayEntryEntityToDTO(StayEntry entity, bool includeGuests)
{
return new()
{
@@ -45,7 +45,7 @@ internal static class EntityMapping
PaymentMethod = entity.PaymentMethod,
CreateTime = entity.CreateTime,
UpdateTime = entity.UpdateTime,
- GuestDocuments = [.. entity.GuestDocuments.Select(e => MapGuestDocumentEntityToDTO(e))],
+ GuestDocuments = includeGuests ? [.. entity.GuestDocuments.Select(e => MapGuestDocumentEntityToDTO(e))] : null,
};
}
diff --git a/src/Somnia.GuestLog/Models/QueryConfigs/StayEntryQueryConfig.cs b/src/Somnia.GuestLog/Models/QueryConfigs/StayEntryQueryConfig.cs
new file mode 100644
index 0000000..e40eefe
--- /dev/null
+++ b/src/Somnia.GuestLog/Models/QueryConfigs/StayEntryQueryConfig.cs
@@ -0,0 +1,34 @@
+using Microsoft.EntityFrameworkCore;
+using Somnia.GuestLog.Models.Entities;
+
+namespace Somnia.GuestLog.Models.QueryConfigs;
+
+public record StayEntryQueryConfig(bool IncludeGuests = false, bool UseSplitQueries = true, bool DisableTracking = false)
+{
+ public readonly static StayEntryQueryConfig DefaultQueryConfig = new();
+}
+
+
+public static class StayEntryQueryExtensions
+{
+ public static IQueryable AddQueryConfig(this IQueryable query, StayEntryQueryConfig? config)
+ {
+ config ??= StayEntryQueryConfig.DefaultQueryConfig;
+
+ if (config.IncludeGuests)
+ {
+ query = query.Include(e => e.GuestDocuments);
+ }
+
+ if (config.UseSplitQueries)
+ {
+ query = query.AsSplitQuery();
+ }
+ if (config.DisableTracking)
+ {
+ query = query.AsNoTracking();
+ }
+
+ return query;
+ }
+}
diff --git a/src/Somnia.GuestLog/Program.cs b/src/Somnia.GuestLog/Program.cs
index 18efcf9..846b599 100644
--- a/src/Somnia.GuestLog/Program.cs
+++ b/src/Somnia.GuestLog/Program.cs
@@ -1,6 +1,7 @@
using FluentValidation;
using Microsoft.AspNetCore.Localization;
using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Diagnostics;
using MudBlazor.Services;
using Somnia.GuestLog.Components;
using Somnia.GuestLog.Infrastructure;
@@ -17,7 +18,17 @@ var connectionStringGuestLog = builder.Configuration.GetConnectionString("DbSomn
?? throw new InvalidOperationException("Connection string 'DbSomniaGuestLog' not found.");
builder.Services.AddDbContextFactory(options =>
{
- options.UseNpgsql(connectionStringGuestLog);
+ options.UseNpgsql(connectionStringGuestLog)
+ .UseLazyLoadingProxies()
+ .ConfigureWarnings(w => w.Log((CoreEventId.NavigationLazyLoading, LogLevel.Warning)));
+
+ // The following options help with debugging, but should
+ // be changed or removed for production.
+ if (builder.Environment.IsDevelopment())
+ {
+ options.EnableSensitiveDataLogging()
+ .EnableDetailedErrors();
+ }
});
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
diff --git a/src/Somnia.GuestLog/Services/IStayEntryService.cs b/src/Somnia.GuestLog/Services/IStayEntryService.cs
index 2cc113f..2c63954 100644
--- a/src/Somnia.GuestLog/Services/IStayEntryService.cs
+++ b/src/Somnia.GuestLog/Services/IStayEntryService.cs
@@ -1,4 +1,5 @@
using Somnia.GuestLog.Models.DTOs;
+using Somnia.GuestLog.Models.QueryConfigs;
namespace Somnia.GuestLog.Services;
@@ -9,11 +10,11 @@ public interface IStayEntryService
public Task DeleteEntryAsync(StayEntryDTO dto);
public Task DeleteEntryAsync(long id);
- public Task GetDtoByIdAsync(long id);
- public Task> GetDtoListAsync();
+ public Task GetDtoByIdAsync(long id, StayEntryQueryConfig? queryConfig = null);
+ public Task> GetDtoListAsync(StayEntryQueryConfig? queryConfig = null);
public Task IsGuestInStayInPeriod(string docType, string docNum, DateTime startTime, DateTime endTime);
- public Task> GetStaysWithGuest(string docType, string docNum);
+ public Task> GetStaysWithGuest(string docType, string docNum, StayEntryQueryConfig? queryConfig = null);
}
\ No newline at end of file
diff --git a/src/Somnia.GuestLog/Services/StayEntryService.cs b/src/Somnia.GuestLog/Services/StayEntryService.cs
index fcd46da..daeed77 100644
--- a/src/Somnia.GuestLog/Services/StayEntryService.cs
+++ b/src/Somnia.GuestLog/Services/StayEntryService.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.Models.QueryConfigs;
namespace Somnia.GuestLog.Services;
@@ -41,7 +42,7 @@ public class StayEntryService(
using var dbContext = contextFactory.CreateDbContext();
//Check and add GuestDocuments
- foreach (GuestDocumentDTO guestDto in dto.GuestDocuments)
+ foreach (GuestDocumentDTO guestDto in dto.GuestDocuments!)
{
//Get it from db.
GuestDocument? dbGuest = await dbContext.GuestDocuments.FindAsync(guestDto.DocumentType, guestDto.DocumentNum)
@@ -111,21 +112,27 @@ public class StayEntryService(
}
}
- public async Task GetDtoByIdAsync(long id)
+ public async Task GetDtoByIdAsync(long id, StayEntryQueryConfig? queryConfig = null)
{
using var dbContext = contextFactory.CreateDbContext();
- StayEntry? dbItem = await dbContext.StayEntries.FindAsync(id);
- return dbItem is not null ? GetDtoFromDb(dbItem) : null;
+ StayEntry? dbItem = await dbContext.StayEntries
+ .Where(e => e.Id == id)
+ .AddQueryConfig(queryConfig)
+ .FirstOrDefaultAsync();
+
+ return dbItem is not null ? GetDtoFromDb(dbItem, queryConfig) : null;
}
- public async Task> GetDtoListAsync()
+ public async Task> GetDtoListAsync(StayEntryQueryConfig? queryConfig = null)
{
using var dbContext = contextFactory.CreateDbContext();
+
var dbList = await dbContext.StayEntries
+ .AddQueryConfig(queryConfig)
.OrderByDescending(e => e.UpdateTime)
.ToListAsync();
- return dbList.Select(GetDtoFromDb);
+ return dbList.Select(e => GetDtoFromDb(e, queryConfig));
}
@@ -133,29 +140,33 @@ public class StayEntryService(
{
using var dbContext = contextFactory.CreateDbContext();
- bool rs = await dbContext.StayEntries.AnyAsync(
- s => s.DateIn < endTime && s.DateOut > startTime
- && s.GuestDocuments.Any(g => g.DocumentType == docType && g.DocumentNum == docNum));
+ bool rs = await dbContext.StayEntries
+ .AnyAsync(s => s.DateIn < endTime && s.DateOut > startTime
+ && s.GuestDocuments.Any(g => g.DocumentType == docType && g.DocumentNum == docNum)
+ );
return rs;
}
- public async Task> GetStaysWithGuest(string docType, string docNum)
+ public async Task> GetStaysWithGuest(string docType, string docNum, StayEntryQueryConfig? queryConfig = null)
{
using var dbContext = contextFactory.CreateDbContext();
- var rs = await dbContext.StayEntries.Where(
- s => s.GuestDocuments.Any(g => g.DocumentType == docType && g.DocumentNum == docNum))
+ var rs = await dbContext.StayEntries
+ .Where(s => s.GuestDocuments.Any(g => g.DocumentType == docType && g.DocumentNum == docNum))
+ .AddQueryConfig(queryConfig)
.ToListAsync();
- return rs.Select(GetDtoFromDb);
+ return rs.Select(e => GetDtoFromDb(e, queryConfig));
}
#region Internal Methods
- private static StayEntryDTO GetDtoFromDb(StayEntry dbItem)
+ private static StayEntryDTO GetDtoFromDb(StayEntry dbItem, StayEntryQueryConfig? queryConfig)
{
- return EntityMapping.MapStayEntryEntityToDTO(dbItem);
+ queryConfig ??= StayEntryQueryConfig.DefaultQueryConfig;
+
+ return EntityMapping.MapStayEntryEntityToDTO(dbItem, queryConfig.IncludeGuests);
}
#endregion
diff --git a/src/Somnia.GuestLog/Somnia.GuestLog.csproj b/src/Somnia.GuestLog/Somnia.GuestLog.csproj
index 7c1e4ab..9dca5d8 100644
--- a/src/Somnia.GuestLog/Somnia.GuestLog.csproj
+++ b/src/Somnia.GuestLog/Somnia.GuestLog.csproj
@@ -24,6 +24,7 @@
+
diff --git a/src/Somnia.GuestLog/Validators/StayEntryDTOValidator.cs b/src/Somnia.GuestLog/Validators/StayEntryDTOValidator.cs
index 678bcf3..4d1f10a 100644
--- a/src/Somnia.GuestLog/Validators/StayEntryDTOValidator.cs
+++ b/src/Somnia.GuestLog/Validators/StayEntryDTOValidator.cs
@@ -26,6 +26,7 @@ public class StayEntryDTOValidator : ValueValidator
.Must(val => val != null && dataListService.IsValueInList(GuestConsts.DataList_PaymentMethods, val));
RuleFor(e => e.GuestDocuments)
+ .NotNull()
.NotEmpty();
}
}