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.
This commit is contained in:
2026-08-17 21:28:26 +02:00
parent 3e0ef34f66
commit 67845e250f
15 changed files with 229 additions and 45 deletions
+1
View File
@@ -23,6 +23,7 @@
</PackageVersion>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Proxies" Version="10.0.7" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
</ItemGroup>
</Project>
@@ -0,0 +1,18 @@
<MudMessageBox @ref="_msgBox" Title="@LanguageLocale.general_confirmTitle"
CancelText="@LanguageLocale.general_btnCancel">
<MessageContent>
@LanguageLocale.general_confirmDeleteMsg
</MessageContent>
<YesButton>
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.DeleteForever">
@LanguageLocale.general_btnDelete
</MudButton>
</YesButton>
</MudMessageBox>
@code {
public MudMessageBox MsgBox => _msgBox;
MudMessageBox _msgBox = null!;
}
@@ -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
@@ -69,17 +69,7 @@
</PagerContent>
</MudDataGrid>
<MudMessageBox @ref="_msgBoxConfirmDelete" Title="@LanguageLocale.general_confirmTitle"
CancelText="@LanguageLocale.general_btnCancel">
<MessageContent>
@LanguageLocale.general_confirmDeleteMsg
</MessageContent>
<YesButton>
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.DeleteForever">
@LanguageLocale.general_btnDelete
</MudButton>
</YesButton>
</MudMessageBox>
<ConfirmDeleteMsgBox @ref="_msgBoxConfirmDelete" />
@code {
@@ -92,7 +82,7 @@
private GuestDocFilterDTO docFilter = new GuestDocFilterDTO();
private MudDataGrid<GuestDocumentDTO> _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)
{
@@ -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
<PageTitle>Somnia - Guest Docs - Details</PageTitle>
<MudStack Row="true" Class="my-4">
<MudText Typo="Typo.h4" GutterBottom="true">Stay Entry - "@StayId"</MudText>
<MudSpacer />
<MudButton Variant="Variant.Filled" Color="Color.Success" Size="Size.Small" StartIcon="@Icons.Material.Filled.Edit"
OnClick="() => BtnEdit()" Disabled>
@LanguageLocale.general_btnEdit
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Default" Size="Size.Small"
StartIcon="@Icons.Material.Filled.ArrowBack" OnClick="() => BtnReturn()">
@LanguageLocale.general_btnReturn
</MudButton>
</MudStack>
@if (stayModel is not null)
{
<StayEntryForm Model="stayModel" ReadOnly="true" />
}
@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);
}
}
@@ -43,7 +43,7 @@
<PropertyColumn Property="x => x.UpdateTime" Title="@LanguageLocale.dto_GuestDocument_UpdateTime" />
@* <TemplateColumn CellClass="d-flex justify-between" Title="Options" StickyRight Hideable="false"
<TemplateColumn CellClass="d-flex justify-between" Title="Options" StickyRight Hideable="false"
ShowColumnOptions="false">
<CellTemplate>
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Outlined.Info"
@@ -51,13 +51,15 @@
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Outlined.Delete"
OnClick="() => BtnDeleteItem(context.Item)" />
</CellTemplate>
</TemplateColumn> *@
</TemplateColumn>
</Columns>
<PagerContent>
<MudDataGridPager T="StayEntryDTO" />
</PagerContent>
</MudDataGrid>
<ConfirmDeleteMsgBox @ref="_confirmDeleteMsgBox" />
@code {
private bool _dataLoading = true;
@@ -67,6 +69,7 @@
private IEnumerable<StayEntryDTO> stayEntryList = [];
private MudDataGrid<StayEntryDTO> _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
}
+2
View File
@@ -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";
@@ -12,7 +12,7 @@ public class StayEntryDTO
public string? PaymentMethod { get; set; }
public List<GuestDocumentDTO> GuestDocuments { get; set; } = [];
public List<GuestDocumentDTO>? GuestDocuments { get; set; } = null;
public DateTime? CreateTime { get; set; }
+2 -2
View File
@@ -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,
};
}
@@ -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<StayEntry> AddQueryConfig(this IQueryable<StayEntry> 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;
}
}
+12 -1
View File
@@ -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<GuestLogContext>(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();
@@ -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<bool> DeleteEntryAsync(StayEntryDTO dto);
public Task<bool> DeleteEntryAsync(long id);
public Task<StayEntryDTO?> GetDtoByIdAsync(long id);
public Task<IEnumerable<StayEntryDTO>> GetDtoListAsync();
public Task<StayEntryDTO?> GetDtoByIdAsync(long id, StayEntryQueryConfig? queryConfig = null);
public Task<IEnumerable<StayEntryDTO>> GetDtoListAsync(StayEntryQueryConfig? queryConfig = null);
public Task<bool> IsGuestInStayInPeriod(string docType, string docNum, DateTime startTime, DateTime endTime);
public Task<IEnumerable<StayEntryDTO>> GetStaysWithGuest(string docType, string docNum);
public Task<IEnumerable<StayEntryDTO>> GetStaysWithGuest(string docType, string docNum, StayEntryQueryConfig? queryConfig = null);
}
@@ -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<StayEntryDTO?> GetDtoByIdAsync(long id)
public async Task<StayEntryDTO?> 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<IEnumerable<StayEntryDTO>> GetDtoListAsync()
public async Task<IEnumerable<StayEntryDTO>> 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<IEnumerable<StayEntryDTO>> GetStaysWithGuest(string docType, string docNum)
public async Task<IEnumerable<StayEntryDTO>> 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
@@ -24,6 +24,7 @@
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
</ItemGroup>
@@ -26,6 +26,7 @@ public class StayEntryDTOValidator : ValueValidator<StayEntryDTO>
.Must(val => val != null && dataListService.IsValueInList(GuestConsts.DataList_PaymentMethods, val));
RuleFor(e => e.GuestDocuments)
.NotNull()
.NotEmpty();
}
}