Compare commits

...
3 Commits
Author SHA256 Message Date
Daniel-Garmig 0de4e1ba10 GuestLog - General Improvements.
- GuestDocument details - Added navigation to details for stays.
- StayEntry details - Added dialog to see details of GuestDocuments.
- StayEntry List - Added filter for result table.
- StayEntry New - Show suggested guests on GuestSearchDialog so no searchs are required in some cases.
- NavMenu - Removed default navigation links.
2026-08-18 22:17:16 +02:00
Daniel-Garmig 340bc583b7 GuestLog - Nominatim AddressQueryService fix.
Small fix in Nominatim AddressQueryService to map "town" results.
2026-08-18 20:16:38 +02:00
Daniel-Garmig 67845e250f 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.
2026-08-17 21:28:26 +02:00
22 changed files with 456 additions and 81 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!;
}
@@ -34,6 +34,14 @@
@if (!isLoading)
{
@* Only show on first load *@
@if (loadCount == 1)
{
<MudText Align="Align.Center" GutterBottom>
@LanguageLocale.control_GuestDocumentSearchDialog_txtSuggestedGuests
</MudText>
}
<MudList T="GuestDocumentDTO" @bind-SelectedValue="SelectedResult"
SelectionMode="SelectionMode.SingleSelection" Color="Color.Info">
@foreach (GuestDocumentDTO result in results)
@@ -74,10 +82,39 @@
private GuestDocumentDTO? SelectedResult { get; set; }
private bool isLoading = false;
private int loadCount = 0;
protected override async Task OnInitializedAsync()
{
//Load last 5 guests.
await LoadGuests(limit: 5);
}
private void Cancel() => MudDialog.Cancel();
private async Task LoadGuests(int limit = 25)
{
try
{
isLoading = true;
results = await guestDocumentService.GetDtoListByFilterAsync(filter);
isLoading = false;
loadCount++;
}
catch (Exception ex)
{
logger.LogError(ex, "GuestDocument Search returned an error!");
Snackbar.Add($"Error searching: {ex.Message}", Severity.Error);
}
}
#region Event Handlers
private async Task BtnSearchGuestDocuments()
{
if (filter.IsEmpty)
@@ -86,17 +123,8 @@
return;
}
try
{
isLoading = true;
results = await guestDocumentService.GetDtoListByFilterAsync(filter);
isLoading = false;
}
catch (Exception ex)
{
logger.LogError(ex, "GuestDocument Search returned an error!");
Snackbar.Add($"Error searching: {ex.Message}", Severity.Error);
}
//Load unlimited guests using current filter.
await LoadGuests();
}
private async Task btnSelectGuest()
@@ -114,4 +142,7 @@
MudDialog.Close(dialogResult);
return;
}
#endregion
}
@@ -0,0 +1,44 @@
@using System.ComponentModel
@using Somnia.GuestLog.Models.DTOs
@using Somnia.GuestLog.Services
@inject IDataListService dataListService;
<MudForm Model="@Filter" IsTouchedChanged="@(() => FilterChanged.InvokeAsync(Filter))">
<MudStack Row Breakpoint="Breakpoint.LgAndDown" StretchItems="StretchItems.All">
<MudStack Row style="margin-block: 0.2em;">
<MudDatePicker @bind-Date="Filter.DateInStart" For="@(() => Filter.DateInStart)"
Label="@LanguageLocale.general_dateStart" OpenTo="OpenTo.Date" Editable Required="@DateRequired" />
<MudDatePicker @bind-Date="Filter.DateInEnd" For="@(() => Filter.DateInEnd)"
Label="@LanguageLocale.general_dateEnd" OpenTo="OpenTo.Date" Editable Required="@DateRequired" />
</MudStack>
<MudStack Row style="margin-block: 0.2em;">
<MudTextField @bind-Value="Filter.RoomNumber" For="@(() => Filter.RoomNumber)"
Label="@LanguageLocale.dto_StayEntry_RoomNumber" FullWidth Clearable />
<MudSelect @bind-Value="Filter.PaymentMethod" For="@(() => Filter.PaymentMethod)"
Label="@LanguageLocale.dto_StayEntry_PaymentMethod" FullWidth Clearable>
@foreach (string paymentType in
dataListService.GetDataListByName(GuestConsts.DataList_PaymentMethods))
{
<MudSelectItem Value="paymentType">
@dataListService.GetLocalizedValue(GuestConsts.DataList_PaymentMethods, paymentType)
</MudSelectItem>
}
</MudSelect>
</MudStack>
</MudStack>
</MudForm>
@code {
[Parameter, Bindable(true)]
public StayEntryFilterDTO Filter { get; set; } = new();
[Parameter]
public EventCallback<StayEntryFilterDTO> FilterChanged { get; set; }
[Parameter]
public bool DateRequired { get; set; } = true;
}
@@ -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;
@@ -69,10 +69,7 @@
<MudTh>@LanguageLocale.dto_GuestDocument_Name</MudTh>
<MudTh>@LanguageLocale.dto_GuestDocument_Surname</MudTh>
<MudTh>@LanguageLocale.dto_GuestDocument_Surname2</MudTh>
@if (!ReadOnly)
{
<MudTh>@LanguageLocale.general_actions</MudTh>
}
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="@LanguageLocale.dto_GuestDocument_DocumentType">
@@ -97,6 +94,13 @@
OnClick="@(() => BtnDeleteGuest(context.DocumentType!, context.DocumentNum!))" />
</MudTd>
}
@if (ReadOnly)
{
<MudTd DataLabel="@LanguageLocale.general_actions">
<MudIconButton Icon="@Icons.Material.Filled.Info"
OnClick="@(() => BtnViewGuestDetails(context))" />
</MudTd>
}
</RowTemplate>
</MudTable>
@@ -143,7 +147,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;
@@ -181,7 +185,15 @@
private async Task BtnAddGuest()
{
var dialog = await dialogService.ShowAsync<GuestDocumentSearchDialog>();
DialogOptions dialogOptions = new()
{
MaxWidth = MaxWidth.Medium,
FullWidth = true,
BackdropClick = false,
CloseOnEscapeKey = true,
};
var dialog = await dialogService.ShowAsync<GuestDocumentSearchDialog>(title: "", options: dialogOptions);
DialogResult? result = await dialog.Result;
if (result is null || result.Canceled)
@@ -198,6 +210,8 @@
}
//Check if this guest is already added.
if (Model.GuestDocuments is not null && Model.GuestDocuments.Count > 0)
{
bool alreadyAdded = Model.GuestDocuments.Any(e => e.DocumentType == resultData.DocumentType
&& e.DocumentNum == resultData.DocumentNum);
if (alreadyAdded)
@@ -205,14 +219,40 @@
snackbar.Add("GuestDocument already added!", MudBlazor.Severity.Warning);
return;
}
}
//Init List.
if (Model.GuestDocuments is null)
{
Model.GuestDocuments = [];
}
Model.GuestDocuments.Add(resultData);
}
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();
}
private async Task BtnViewGuestDetails(GuestDocumentDTO item)
{
var parameters =
new DialogParameters<GuestDocDetailsDialog> { { x => x.DocumentDTO, item } };
var options = new DialogOptions()
{
FullScreen = true,
CloseButton = true,
CloseOnEscapeKey = true,
};
await dialogService.ShowAsync<GuestDocDetailsDialog>("GuestDoc Details", parameters, options);
}
#endregion
@@ -1,12 +1,16 @@
<MudNavMenu>
<MudNavMenu>
<MudNavLink Href="" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Home">Home</MudNavLink>
@*
<MudNavLink Href="counter" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Add">Counter</MudNavLink>
<MudNavLink Href="weather" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.List">Weather</MudNavLink>
*@
<MudNavLink Href="@GuestConsts.Page_GuestDocs_List" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.DocumentScanner">Guests</MudNavLink>
<MudNavLink Href="@GuestConsts.Page_StayEntry_List" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Book">Stays</MudNavLink>
<MudNavLink Href="@GuestConsts.Page_GuestDocs_List" Match="NavLinkMatch.Prefix"
Icon="@Icons.Material.Filled.DocumentScanner">
@LanguageLocale.navMenu_GuestDocumentList
</MudNavLink>
<MudNavLink Href="@GuestConsts.Page_StayEntry_List" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Book">
@LanguageLocale.navMenu_StayEntryList
</MudNavLink>
</MudNavMenu>
@@ -2,6 +2,7 @@
@using Somnia.GuestLog.Models.DTOs;
@using Somnia.GuestLog.Services;
@using Microsoft.AspNetCore.WebUtilities;
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@@ -42,6 +43,7 @@
<MudTh>@LanguageLocale.dto_StayEntry_DateOut</MudTh>
<MudTh>@LanguageLocale.dto_StayEntry_RoomCount</MudTh>
<MudTh>@LanguageLocale.dto_StayEntry_RoomNumber</MudTh>
<MudTh>@LanguageLocale.general_actions</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="@LanguageLocale.dto_StayEntry_DateIn">
@@ -56,6 +58,9 @@
<MudTd DataLabel="@LanguageLocale.dto_StayEntry_RoomNumber">
@context.RoomNumber
</MudTd>
<MudTd DataLabel="@LanguageLocale.general_actions">
<MudIconButton Icon="@Icons.Material.Filled.Info" OnClick="@(() => BtnViewStayDetails(context))" />
</MudTd>
</RowTemplate>
<NoRecordsContent>
<MudText>@LanguageLocale.general_noResults</MudText>
@@ -110,16 +115,6 @@
}
}
private void BtnEdit()
{
Snackbar.Add("Not implemented yet!", Severity.Info);
}
private void BtnReturn()
{
Navigation.NavigateTo(GuestConsts.Page_GuestDocs_List);
}
private async Task LoadTabGuestStays()
{
//Only load when there is no data.
@@ -137,4 +132,33 @@
guestStayList = await stayService.GetStaysWithGuest(documentModel.DocumentType, documentModel.DocumentNum);
guestStaysLoading = false;
}
#region Event Handlers
private void BtnEdit()
{
Snackbar.Add("Not implemented yet!", Severity.Info);
}
private void BtnReturn()
{
Navigation.NavigateTo(GuestConsts.Page_GuestDocs_List);
}
private void BtnViewStayDetails(StayEntryDTO item)
{
string itemId = $"{item.Id ?? -1}";
string uri = GuestConsts.Page_StayEntry_Detail.Replace(GuestConsts.Page_StayEntry_Detail_Param_Id, itemId);
string relativeUri = Navigation.ToBaseRelativePath(Navigation.Uri);
uri = QueryHelpers.AddQueryString(uri, [
new KeyValuePair<string, string?>(GuestConsts.QueryParam_ReturnUrl, relativeUri)
]);
Navigation.NavigateTo(uri);
}
#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,74 @@
@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; }
[SupplyParameterFromQuery(Name = GuestConsts.QueryParam_ReturnUrl)]
public string? ReturnUrl { 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()
{
string uri = ReturnUrl ?? GuestConsts.Page_StayEntry_List;
Navigation.NavigateTo(uri);
}
}
@@ -24,6 +24,10 @@
</MudButton>
</MudStack>
<MudPaper Class="pa-4 my-4" Elevation="2" Height="100%">
<StayEntryFilterForm @bind-Filter="stayFilter" />
</MudPaper>
<MudDataGrid @ref="_dataGrid" T="StayEntryDTO" Items="@stayEntryList" Loading="@_dataLoading" ReadOnly="true"
Elevation="2">
<ToolBarContent>
@@ -43,7 +47,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,22 +55,31 @@
<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;
private bool _showAllData = false;
private const int resultLimit = 200;
private StayEntryFilterDTO stayFilter = new StayEntryFilterDTO()
{
DateInStart = DateTime.Today.AddDays(-14),
DateInEnd = DateTime.Today,
};
private IEnumerable<StayEntryDTO> stayEntryList = [];
private MudDataGrid<StayEntryDTO> _dataGrid = null!;
private ConfirmDeleteMsgBox _confirmDeleteMsgBox = null!;
protected override async Task OnInitializedAsync()
@@ -79,8 +92,7 @@
_dataLoading = true;
int dataLimit = _showAllData ? 0 : resultLimit;
stayEntryList = await StayEntryService.GetDtoListAsync();
//stayEntryList = await StayEntryService.GetDtoListAsync(filter: filter, limit: dataLimit);
stayEntryList = await StayEntryService.GetDtoListByFilterAsync(filter: stayFilter, limit: dataLimit);
if (showMsg)
{
@@ -102,6 +114,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
}
+5
View File
@@ -10,6 +10,11 @@ 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 QueryParam_ReturnUrl = "returnUrl";
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; }
@@ -0,0 +1,10 @@
namespace Somnia.GuestLog.Models.DTOs;
public class StayEntryFilterDTO
{
public DateTime? DateInStart { get; set; }
public DateTime? DateInEnd { get; set; }
public string? RoomNumber { get; set; }
public string? PaymentMethod { 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();
@@ -166,6 +166,8 @@
<data name="general.noResults" xml:space="preserve"><value>No Results!</value></data>
<data name="general.showAllElements" xml:space="preserve"><value>Show all elements</value></data>
<data name="general.wildcardHelperText" xml:space="preserve"><value>Use '%' as wildcard</value></data>
<data name="general.dateStart" xml:space="preserve"><value>Date Start</value></data>
<data name="general.dateEnd" xml:space="preserve"><value>Date End</value></data>
<data name="general.address.address" xml:space="preserve"><value>Address</value></data>
<data name="general.address.city" xml:space="preserve"><value>City</value></data>
@@ -174,6 +176,10 @@
<data name="general.address.country" xml:space="preserve"><value>Country</value></data>
<data name="general.address.zipCode" xml:space="preserve"><value>Zip Code</value></data>
<data name="navMenu.GuestDocumentList" xml:space="preserve"><value>Guests</value></data>
<data name="navMenu.StayEntryList" xml:space="preserve"><value>Stays</value></data>
<data name="page.GuestDocList.pageTitle" xml:space="preserve"><value>Guest Documents</value></data>
<data name="page.GuestDocList.documentList" xml:space="preserve"><value>Document List</value></data>
<data name="page.GuestDocList.btnNewGuest" xml:space="preserve"><value>New Guest</value></data>
@@ -204,5 +210,6 @@
<data name="control.GuestDocumentSearchDialog.dialogTitle" xml:space="preserve"><value>Search Guest Document</value></data>
<data name="control.GuestDocumentSearchDialog.btnSelectDocument" xml:space="preserve"><value>Use Guest</value></data>
<data name="control.GuestDocumentSearchDialog.txtSuggestedGuests" xml:space="preserve"><value>Suggested Guests</value></data>
</root>
@@ -43,7 +43,7 @@ public class NominatimAddressQueryService(
{
HouseNumber = result.Address.House_number,
AddressRoad = result.Address.Road ?? result.Address.Square,
AddressCity = result.Address.City ?? result.Address.Municipality ?? result.Address.Village ?? result.Address.Village,
AddressCity = result.Address.City ?? result.Address.Municipality ?? result.Address.Town ?? result.Address.Village,
AddressProvince = result.Address.Province,
AddressState = result.Address.State,
AddressCountry = result.Address.Country,
@@ -1,4 +1,5 @@
using Somnia.GuestLog.Models.DTOs;
using Somnia.GuestLog.Models.QueryConfigs;
namespace Somnia.GuestLog.Services;
@@ -9,11 +10,12 @@ 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<IEnumerable<StayEntryDTO>> GetDtoListByFilterAsync(StayEntryFilterDTO filter, int limit = 0, 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,8 @@ using Somnia.GuestLog.Infrastructure;
using Somnia.GuestLog.Models;
using Somnia.GuestLog.Models.DTOs;
using Somnia.GuestLog.Models.Entities;
using Somnia.GuestLog.Models.QueryConfigs;
using Somnia.GuestLog.Utils;
namespace Somnia.GuestLog.Services;
@@ -41,7 +43,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 +113,53 @@ 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));
}
public async Task<IEnumerable<StayEntryDTO>> GetDtoListByFilterAsync(StayEntryFilterDTO filter, int limit = 0, StayEntryQueryConfig? queryConfig = null)
{
using var dbContext = contextFactory.CreateDbContext();
var query = dbContext.StayEntries
.AddQueryConfig(queryConfig)
.AsQueryable();
query = query.WhereIf(filter.DateInStart is not null && filter.DateInEnd is not null,
e => e.DateIn >= filter.DateInStart!.Value.ToUniversalTime()
&& e.DateIn < filter.DateInEnd!.Value.ToUniversalTime());
query = query.WhereIf(!string.IsNullOrEmpty(filter.RoomNumber),
e => EF.Functions.Like(e.RoomNumber, $"%{filter.RoomNumber}%"));
query = query.WhereIf(!string.IsNullOrEmpty(filter.PaymentMethod),
e => e.PaymentMethod == filter.PaymentMethod);
if (limit > 0)
{
query = query.Take(limit);
}
var dbList = await query.ToListAsync();
return dbList.Select(x => GetDtoFromDb(x, queryConfig));
}
@@ -133,29 +167,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();
}
}