From 6dc2d2426bbc85b5ff70e784bfbcf2e6306b20313c16767fdeade3cdc05834e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= <93217193+Daniel-Garmig@users.noreply.github.com> Date: Sat, 15 Aug 2026 19:33:32 +0200 Subject: [PATCH] GuestLog - StayEntry - Initial Implementation - Added new entity "StayEntry" to log stays of guests. - Added pages for StayEntry management (List and creation). - New DataList for PaymentMethods. - Other general changes and improvements. --- .../Controls/GuestDocumentSearchDialog.razor | 117 ++++++++++ .../Forms/GuestDocumentFilterForm.razor | 41 ++++ .../Components/Forms/StayEntryForm.razor | 220 ++++++++++++++++++ .../Components/Layout/NavMenu.razor | 1 + .../Pages/GuestDocuments/GuestDocList.razor | 2 +- .../Pages/StayEntries/StayEntryList.razor | 107 +++++++++ .../Pages/StayEntries/StayEntryNew.razor | 67 ++++++ src/Somnia.GuestLog/Components/_Imports.razor | 3 + src/Somnia.GuestLog/GuestConsts.cs | 4 + .../Infrastructure/DbConsts.cs | 8 + .../EntityConfigs/StayEntryEntityConfig.cs | 45 ++++ .../Infrastructure/GuestLogContext.cs | 2 + ...60813085339_AddTable_StayEntry.Designer.cs | 191 +++++++++++++++ .../20260813085339_AddTable_StayEntry.cs | 75 ++++++ .../GuestLogContextModelSnapshot.cs | 70 ++++++ .../Models/DTOs/GuestDocFilterDTO.cs | 9 +- .../Models/DTOs/GuestDocumentDTO.cs | 3 + .../Models/DTOs/StayEntryDTO.cs | 20 ++ .../Models/Entities/StayEntry.cs | 24 ++ src/Somnia.GuestLog/Models/EntityMapping.cs | 52 +++++ src/Somnia.GuestLog/Program.cs | 2 + .../Resources/DataListLocale.resx | 6 + .../Resources/LanguageLocale.resx | 28 ++- .../Services/GuestDocumentService.cs | 39 +--- .../Services/IStayEntryService.cs | 19 ++ .../Services/StaticDataListService.cs | 9 + .../Services/StayEntryService.cs | 162 +++++++++++++ .../Validators/StayEntryDTOValidator.cs | 31 +++ .../Validators/ValueValidator.cs | 21 ++ 29 files changed, 1348 insertions(+), 30 deletions(-) create mode 100644 src/Somnia.GuestLog/Components/Controls/GuestDocumentSearchDialog.razor create mode 100644 src/Somnia.GuestLog/Components/Forms/GuestDocumentFilterForm.razor create mode 100644 src/Somnia.GuestLog/Components/Forms/StayEntryForm.razor create mode 100644 src/Somnia.GuestLog/Components/Pages/StayEntries/StayEntryList.razor create mode 100644 src/Somnia.GuestLog/Components/Pages/StayEntries/StayEntryNew.razor create mode 100644 src/Somnia.GuestLog/Infrastructure/DbConsts.cs create mode 100644 src/Somnia.GuestLog/Infrastructure/EntityConfigs/StayEntryEntityConfig.cs create mode 100644 src/Somnia.GuestLog/Infrastructure/Migrations/20260813085339_AddTable_StayEntry.Designer.cs create mode 100644 src/Somnia.GuestLog/Infrastructure/Migrations/20260813085339_AddTable_StayEntry.cs create mode 100644 src/Somnia.GuestLog/Models/DTOs/StayEntryDTO.cs create mode 100644 src/Somnia.GuestLog/Models/Entities/StayEntry.cs create mode 100644 src/Somnia.GuestLog/Models/EntityMapping.cs create mode 100644 src/Somnia.GuestLog/Services/IStayEntryService.cs create mode 100644 src/Somnia.GuestLog/Services/StayEntryService.cs create mode 100644 src/Somnia.GuestLog/Validators/StayEntryDTOValidator.cs create mode 100644 src/Somnia.GuestLog/Validators/ValueValidator.cs diff --git a/src/Somnia.GuestLog/Components/Controls/GuestDocumentSearchDialog.razor b/src/Somnia.GuestLog/Components/Controls/GuestDocumentSearchDialog.razor new file mode 100644 index 0000000..cd0f3b1 --- /dev/null +++ b/src/Somnia.GuestLog/Components/Controls/GuestDocumentSearchDialog.razor @@ -0,0 +1,117 @@ +@using Somnia.GuestLog.Models.DTOs; +@using Somnia.GuestLog.Services + +@inject ILogger logger; +@inject ISnackbar Snackbar +@inject IGuestDocumentService guestDocumentService; + + + + + + + @LanguageLocale.control_GuestDocumentSearchDialog_dialogTitle + + + + + + @LanguageLocale.general_btnSearch + + + + + @if (!isLoading && results.Count() == 0) + { + @LanguageLocale.general_noResults + } + + @if (isLoading) + { + + } + + @if (!isLoading) + { + + @foreach (GuestDocumentDTO result in results) + { + + + + + + + } + + } + + + + + + @LanguageLocale.general_btnCancel + + + @LanguageLocale.control_GuestDocumentSearchDialog_btnSelectDocument + + + + +@code { + // Dialog Instance injected when open by the DialogService. + // This field is null when dialog is not open by the DialogService. + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = null!; + + + private GuestDocFilterDTO filter { get; set; } = new(); + private IEnumerable results = []; + private GuestDocumentDTO? SelectedResult { get; set; } + + private bool isLoading = false; + + + private void Cancel() => MudDialog.Cancel(); + + private async Task BtnSearchGuestDocuments() + { + if (filter.IsEmpty) + { + Snackbar.Add("Add some search!", Severity.Warning); + 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); + } + } + + private async Task btnSelectGuest() + { + if (SelectedResult is null) + { + Snackbar.Add("Select a guest document", Severity.Warning); + return; + } + + DialogResult dialogResult = DialogResult.Ok(SelectedResult); + + //Close the dialog when opened by DialogService. + logger.LogDebug("GuestDocumentSearchDialog - Closed MudDialogInstance!"); + MudDialog.Close(dialogResult); + return; + } +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/Components/Forms/GuestDocumentFilterForm.razor b/src/Somnia.GuestLog/Components/Forms/GuestDocumentFilterForm.razor new file mode 100644 index 0000000..525cfac --- /dev/null +++ b/src/Somnia.GuestLog/Components/Forms/GuestDocumentFilterForm.razor @@ -0,0 +1,41 @@ +@using Somnia.GuestLog.Models.DTOs +@using Somnia.GuestLog.Services + +@inject IDataListService dataListService; + + + + + + @foreach (string docType in dataListService.GetDataListByName(GuestConsts.DataList_DocumentTypes)) + { + + @dataListService.GetLocalizedValue(GuestConsts.DataList_DocumentTypes, docType) + + } + + + + + + + + + + +@code { + + [Parameter] + public GuestDocFilterDTO Filter { get; set; } = new(); + + [Parameter] + public EventCallback FilterChanged { get; set; } + +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/Components/Forms/StayEntryForm.razor b/src/Somnia.GuestLog/Components/Forms/StayEntryForm.razor new file mode 100644 index 0000000..6fa0c10 --- /dev/null +++ b/src/Somnia.GuestLog/Components/Forms/StayEntryForm.razor @@ -0,0 +1,220 @@ +@using FluentValidation; +@using Microsoft.AspNetCore.SignalR +@using Somnia.GuestLog.Models.DTOs; +@using Somnia.GuestLog.Services; +@using Somnia.GuestLog.Validators; + +@inject IDataListService dataListService; +@inject IValidator stayValidator; +@inject IDialogService dialogService; +@inject ISnackbar snackbar + + + + + + + + @LanguageLocale.control_StayEntryForm_sectionDetails + + + + + + + + + + + + + + + @foreach (string payment in PaymentMethodSet) + { + + @dataListService.GetLocalizedValue(GuestConsts.DataList_PaymentMethods, payment) + + } + + + + + + + + @LanguageLocale.control_StayEntryForm_sectionGuests + + + + @if (!ReadOnly) + { + + + @LanguageLocale.control_StayEntryForm_btnAddGuest + + + } + + + + @LanguageLocale.dto_GuestDocument_DocumentType + @LanguageLocale.dto_GuestDocument_DocumentNum + @LanguageLocale.dto_GuestDocument_Name + @LanguageLocale.dto_GuestDocument_Surname + @LanguageLocale.dto_GuestDocument_Surname2 + @if (!ReadOnly) + { + @LanguageLocale.general_actions + } + + + + @context.DocumentType + + + @context.DocumentNum + + + @context.Name + + + @context.Surname + + + @context.Surname2 + + @if (!ReadOnly) + { + + + + } + + + + + + + + +@code { + [Parameter] + public StayEntryDTO Model { private get; set; } = new(); + + [Parameter] + public bool ReadOnly { get; set; } = false; + + private TimeSpan? TimeIn = DateTime.Now.TimeOfDay; + private TimeSpan? TimeOut = new(hours: 12, minutes: 0, seconds: 0); + + + private MudForm _form = null!; + + private const string FieldMarginY = "margin-block: 0.5em"; + private IEnumerable PaymentMethodSet + { + get + { + if (field is null) + { + field = dataListService.GetDataListByName(GuestConsts.DataList_PaymentMethods); + } + return field; + } + } + + + protected override void OnInitialized() + { + Model.DateIn = DateTime.Today; + } + + #region Public Methods + + public async Task ValidateForm() + { + await _form.ValidateAsync(); + + if (Model.GuestDocuments.Count == 0) + { + snackbar.Add("At least 1 guest is required!", MudBlazor.Severity.Warning); + return false; + } + + return _form.IsValid; + } + + public StayEntryDTO GetModel() + { + //Update model times. + Model.DateIn = GetDateTimeFromDateAndTime(Model.DateIn, TimeIn); + Model.DateOut = GetDateTimeFromDateAndTime(Model.DateOut, TimeOut); + + return Model; + } + + #endregion + + + private static DateTime? GetDateTimeFromDateAndTime(DateTime? date, TimeSpan? time) + { + DateOnly? dateOnly = date is not null ? DateOnly.FromDateTime(date.Value) : null; + TimeOnly? timeOnly = time is not null ? TimeOnly.FromTimeSpan(time.Value) : null; + + if (dateOnly is not null && timeOnly is not null) + { + return new DateTime(dateOnly.Value, timeOnly.Value); + } + + return null; + } + + #region Event Handlers + + private async Task BtnAddGuest() + { + var dialog = await dialogService.ShowAsync(); + DialogResult? result = await dialog.Result; + + if (result is null || result.Canceled) + { + return; + } + + GuestDocumentDTO? resultData = await dialog.GetReturnValueAsync(); + + if (resultData is null) + { + snackbar.Add("Error! No Guest Document selected!", MudBlazor.Severity.Error); + return; + } + + //Check if this guest is already added. + 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; + } + + Model.GuestDocuments.Add(resultData); + } + + private void BtnDeleteGuest(string docType, string docNum) + { + //Filter out given GuestDocument from model. + Model.GuestDocuments = Model.GuestDocuments.Where(e => !(e.DocumentType == docType && e.DocumentNum == docNum)).ToList(); + } + + #endregion + +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/Components/Layout/NavMenu.razor b/src/Somnia.GuestLog/Components/Layout/NavMenu.razor index 981e14f..048b526 100644 --- a/src/Somnia.GuestLog/Components/Layout/NavMenu.razor +++ b/src/Somnia.GuestLog/Components/Layout/NavMenu.razor @@ -6,6 +6,7 @@ Weather Guests + Stays diff --git a/src/Somnia.GuestLog/Components/Pages/GuestDocuments/GuestDocList.razor b/src/Somnia.GuestLog/Components/Pages/GuestDocuments/GuestDocList.razor index 7f973fc..9d6782f 100644 --- a/src/Somnia.GuestLog/Components/Pages/GuestDocuments/GuestDocList.razor +++ b/src/Somnia.GuestLog/Components/Pages/GuestDocuments/GuestDocList.razor @@ -58,7 +58,7 @@ @LanguageLocale.page_GuestDocList_documentList - diff --git a/src/Somnia.GuestLog/Components/Pages/StayEntries/StayEntryList.razor b/src/Somnia.GuestLog/Components/Pages/StayEntries/StayEntryList.razor new file mode 100644 index 0000000..ea39c8c --- /dev/null +++ b/src/Somnia.GuestLog/Components/Pages/StayEntries/StayEntryList.razor @@ -0,0 +1,107 @@ +@attribute [Route($"{GuestConsts.Page_StayEntry_List}")] + +@using Somnia.GuestLog.Models.DTOs; +@using Somnia.GuestLog.Services; + +@inject IStayEntryService StayEntryService +@inject NavigationManager Navigation +@inject ISnackbar Snackbar +@inject IDialogService DialogService +@inject IDataListService dataListService; + +Somnia - Stay Entries + + + @LanguageLocale.page_StayEntryList_pageTitle + + + @LanguageLocale.general_btnSearch + + + @LanguageLocale.page_StayEntryList_btnNewStay + + + + + + @LanguageLocale.page_StayEntryList_entryList + + + + + + + + + + + + + + @* + + + + + *@ + + + + + + +@code { + + private bool _dataLoading = true; + private bool _showAllData = false; + private const int resultLimit = 200; + + private IEnumerable stayEntryList = []; + + private MudDataGrid _dataGrid = null!; + + + protected override async Task OnInitializedAsync() + { + await LoadDataAsync(showMsg: false); + } + + private async Task LoadDataAsync(bool showMsg = true) + { + _dataLoading = true; + + int dataLimit = _showAllData ? 0 : resultLimit; + stayEntryList = await StayEntryService.GetDtoListAsync(); + //stayEntryList = await StayEntryService.GetDtoListAsync(filter: filter, limit: dataLimit); + + if (showMsg) + { + Snackbar.Add("Data loaded from DB", Severity.Info, o => o.VisibleStateDuration = 1000); + } + _dataLoading = false; + } + + + #region Event Handlers + + private async Task BtnSearchDataAsync() + { + await LoadDataAsync(); + } + + private void BtnNewStay() + { + Navigation.NavigateTo(GuestConsts.Page_StayEntry_New); + } + + #endregion + +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/Components/Pages/StayEntries/StayEntryNew.razor b/src/Somnia.GuestLog/Components/Pages/StayEntries/StayEntryNew.razor new file mode 100644 index 0000000..959b2d9 --- /dev/null +++ b/src/Somnia.GuestLog/Components/Pages/StayEntries/StayEntryNew.razor @@ -0,0 +1,67 @@ +@attribute [Route($"{GuestConsts.Page_StayEntry_New}")] + +@using Somnia.GuestLog.Models.DTOs; +@using Somnia.GuestLog.Services; + + +@inject ISnackbar Snackbar +@inject IDialogService DialogService +@inject NavigationManager Navigation +@inject IStayEntryService stayService +@inject IDataListService dataListService; + +Somnia - Stay Entries - New + + + + @LanguageLocale.page_StayEntryNew_pageTitle + + + + @LanguageLocale.general_btnCreate + + + @LanguageLocale.general_btnReturn + + + + + +@code { + StayEntryForm _form = null!; + + StayEntryDTO newStayModel = new(); + + + private void BtnReturn() + { + Navigation.NavigateTo(GuestConsts.Page_StayEntry_List); + } + + private async Task BtnSaveStay() + { + bool isFormValid = await _form.ValidateForm(); + if (!isFormValid) + { + Snackbar.Add("Invalid Form!", Severity.Warning); + return; + } + + var loadingDialog = await DialogService.ShowAsync(); + + StayEntryDTO model = _form.GetModel(); + bool isCreated = await stayService.CreateEntryAsync(model); + if (!isCreated) + { + Snackbar.Add($"Error creating StayEntry", Severity.Error); + loadingDialog.Close(); + return; + } + + Snackbar.Add($"StayEntry Created!", Severity.Success); + Navigation.NavigateTo(GuestConsts.Page_StayEntry_List); + } + +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/Components/_Imports.razor b/src/Somnia.GuestLog/Components/_Imports.razor index 6c6a064..829502f 100644 --- a/src/Somnia.GuestLog/Components/_Imports.razor +++ b/src/Somnia.GuestLog/Components/_Imports.razor @@ -12,4 +12,7 @@ @using Somnia.GuestLog @using Somnia.GuestLog.Components @using Somnia.GuestLog.Components.Layout +@using Somnia.GuestLog.Components.Forms +@using Somnia.GuestLog.Components.Controls @using Somnia.GuestLog.Resources +@using System.Linq; \ No newline at end of file diff --git a/src/Somnia.GuestLog/GuestConsts.cs b/src/Somnia.GuestLog/GuestConsts.cs index cb88347..870331f 100644 --- a/src/Somnia.GuestLog/GuestConsts.cs +++ b/src/Somnia.GuestLog/GuestConsts.cs @@ -7,10 +7,14 @@ public static class GuestConsts public const string Page_GuestDocs_New = "/guest-documents/new"; public const string Page_GuestDocs_Detail = "/guest-documents/d/{docId}"; + public const string Page_StayEntry_List = "/stayEntries"; + public const string Page_StayEntry_New = "/stayEntries/new"; + public const string DataList_DocumentTypes = "DocumentType"; public const string DataList_GenderOptions = "GenderOption"; public const string DataList_CountryCodes = "CountryCode"; + public const string DataList_PaymentMethods = "PaymentMethod"; public const string CountryCode_ESP = "ESP"; diff --git a/src/Somnia.GuestLog/Infrastructure/DbConsts.cs b/src/Somnia.GuestLog/Infrastructure/DbConsts.cs new file mode 100644 index 0000000..700580a --- /dev/null +++ b/src/Somnia.GuestLog/Infrastructure/DbConsts.cs @@ -0,0 +1,8 @@ +namespace Somnia.GuestLog.Infrastructure; + +public static class DbConsts +{ + public const string DbType_Timestamptz = "timestamp with time zone"; + public const string DbType_Timestamptz_0 = "timestamp(0) with time zone"; + public const string DbType_Timestamptz_3 = "timestamp(3) with time zone"; +} diff --git a/src/Somnia.GuestLog/Infrastructure/EntityConfigs/StayEntryEntityConfig.cs b/src/Somnia.GuestLog/Infrastructure/EntityConfigs/StayEntryEntityConfig.cs new file mode 100644 index 0000000..8aed14b --- /dev/null +++ b/src/Somnia.GuestLog/Infrastructure/EntityConfigs/StayEntryEntityConfig.cs @@ -0,0 +1,45 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Somnia.GuestLog.Models.Entities; + +namespace Somnia.GuestLog.Infrastructure.EntityConfigs; + +public class StayEntryEntityConfig : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("StayEntry"); + + //PK + builder.HasKey(e => e.Id); + + builder.Property(e => e.Id) + .IsRequired() + .UseIdentityByDefaultColumn(); + + builder.Property(e => e.DateIn) + .IsRequired() + .HasColumnType(DbConsts.DbType_Timestamptz_0); + builder.Property(e => e.DateOut) + .IsRequired() + .HasColumnType(DbConsts.DbType_Timestamptz_0); + + builder.Property(e => e.RoomNumber) + .HasMaxLength(32); + builder.Property(e => e.PaymentMethod) + .IsRequired() + .HasMaxLength(5); + + + builder.Property(e => e.CreateTime) + .IsRequired() + .HasColumnType(DbConsts.DbType_Timestamptz); + builder.Property(e => e.UpdateTime) + .IsRequired() + .HasColumnType(DbConsts.DbType_Timestamptz); + + + builder.HasMany(e => e.GuestDocuments) + .WithMany(); + } +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/Infrastructure/GuestLogContext.cs b/src/Somnia.GuestLog/Infrastructure/GuestLogContext.cs index 296cd7b..eb1eeff 100644 --- a/src/Somnia.GuestLog/Infrastructure/GuestLogContext.cs +++ b/src/Somnia.GuestLog/Infrastructure/GuestLogContext.cs @@ -11,11 +11,13 @@ public partial class GuestLogContext : DbContext { } public DbSet GuestDocuments { get; set; } + public DbSet StayEntries { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfiguration(new GuestDocumentEntityConfig()); + modelBuilder.ApplyConfiguration(new StayEntryEntityConfig()); OnModelCreatingPartial(modelBuilder); base.OnModelCreating(modelBuilder); diff --git a/src/Somnia.GuestLog/Infrastructure/Migrations/20260813085339_AddTable_StayEntry.Designer.cs b/src/Somnia.GuestLog/Infrastructure/Migrations/20260813085339_AddTable_StayEntry.Designer.cs new file mode 100644 index 0000000..cf25107 --- /dev/null +++ b/src/Somnia.GuestLog/Infrastructure/Migrations/20260813085339_AddTable_StayEntry.Designer.cs @@ -0,0 +1,191 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Somnia.GuestLog.Infrastructure; + +#nullable disable + +namespace Somnia.GuestLog.Infrastructure.Migrations +{ + [DbContext(typeof(GuestLogContext))] + [Migration("20260813085339_AddTable_StayEntry")] + partial class AddTable_StayEntry + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("GuestDocumentStayEntry", b => + { + b.Property("StayEntryId") + .HasColumnType("bigint"); + + b.Property("GuestDocumentsDocumentType") + .HasColumnType("character varying(8)"); + + b.Property("GuestDocumentsDocumentNum") + .HasColumnType("character varying(64)"); + + b.HasKey("StayEntryId", "GuestDocumentsDocumentType", "GuestDocumentsDocumentNum"); + + b.HasIndex("GuestDocumentsDocumentType", "GuestDocumentsDocumentNum"); + + b.ToTable("GuestDocumentStayEntry"); + }); + + modelBuilder.Entity("Somnia.GuestLog.Models.Entities.GuestDocument", b => + { + b.Property("DocumentType") + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("DocumentNum") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("BirthDate") + .HasColumnType("date"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EmailAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Gender") + .HasMaxLength(1) + .HasColumnType("character varying(1)"); + + b.Property("IssueDate") + .HasColumnType("date"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Nationality") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.Property("PhoneNumber") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("SupportNum") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Surname") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Surname2") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.ComplexProperty(typeof(Dictionary), "AddressDetails", "Somnia.GuestLog.Models.Entities.GuestDocument.AddressDetails#GuestAddress", b1 => + { + b1.Property("Address") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b1.Property("AddressCity") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b1.Property("AddressCountry") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b1.Property("AddressProvince") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b1.Property("AddressState") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b1.Property("ZipCode") + .HasMaxLength(12) + .HasColumnType("character varying(12)"); + }); + + b.HasKey("DocumentType", "DocumentNum"); + + b.ToTable("GuestDocument", (string)null); + }); + + modelBuilder.Entity("Somnia.GuestLog.Models.Entities.StayEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DateIn") + .HasColumnType("timestamp(0) with time zone"); + + b.Property("DateOut") + .HasColumnType("timestamp(0) with time zone"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("character varying(5)"); + + b.Property("RoomCount") + .HasColumnType("integer"); + + b.Property("RoomNumber") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("StayEntry", (string)null); + }); + + modelBuilder.Entity("GuestDocumentStayEntry", b => + { + b.HasOne("Somnia.GuestLog.Models.Entities.StayEntry", null) + .WithMany() + .HasForeignKey("StayEntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Somnia.GuestLog.Models.Entities.GuestDocument", null) + .WithMany() + .HasForeignKey("GuestDocumentsDocumentType", "GuestDocumentsDocumentNum") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Somnia.GuestLog/Infrastructure/Migrations/20260813085339_AddTable_StayEntry.cs b/src/Somnia.GuestLog/Infrastructure/Migrations/20260813085339_AddTable_StayEntry.cs new file mode 100644 index 0000000..0de6464 --- /dev/null +++ b/src/Somnia.GuestLog/Infrastructure/Migrations/20260813085339_AddTable_StayEntry.cs @@ -0,0 +1,75 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Somnia.GuestLog.Infrastructure.Migrations +{ + /// + public partial class AddTable_StayEntry : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "StayEntry", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + DateIn = table.Column(type: "timestamp(0) with time zone", nullable: false), + DateOut = table.Column(type: "timestamp(0) with time zone", nullable: false), + RoomCount = table.Column(type: "integer", nullable: false), + RoomNumber = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + PaymentMethod = table.Column(type: "character varying(5)", maxLength: 5, nullable: false), + CreateTime = table.Column(type: "timestamp with time zone", nullable: false), + UpdateTime = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_StayEntry", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "GuestDocumentStayEntry", + columns: table => new + { + StayEntryId = table.Column(type: "bigint", nullable: false), + GuestDocumentsDocumentType = table.Column(type: "character varying(8)", nullable: false), + GuestDocumentsDocumentNum = table.Column(type: "character varying(64)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GuestDocumentStayEntry", x => new { x.StayEntryId, x.GuestDocumentsDocumentType, x.GuestDocumentsDocumentNum }); + table.ForeignKey( + name: "FK_GuestDocumentStayEntry_GuestDocument_GuestDocumentsDocument~", + columns: x => new { x.GuestDocumentsDocumentType, x.GuestDocumentsDocumentNum }, + principalTable: "GuestDocument", + principalColumns: new[] { "DocumentType", "DocumentNum" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_GuestDocumentStayEntry_StayEntry_StayEntryId", + column: x => x.StayEntryId, + principalTable: "StayEntry", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_GuestDocumentStayEntry_GuestDocumentsDocumentType_GuestDocu~", + table: "GuestDocumentStayEntry", + columns: new[] { "GuestDocumentsDocumentType", "GuestDocumentsDocumentNum" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "GuestDocumentStayEntry"); + + migrationBuilder.DropTable( + name: "StayEntry"); + } + } +} diff --git a/src/Somnia.GuestLog/Infrastructure/Migrations/GuestLogContextModelSnapshot.cs b/src/Somnia.GuestLog/Infrastructure/Migrations/GuestLogContextModelSnapshot.cs index 92d0a1c..b99767e 100644 --- a/src/Somnia.GuestLog/Infrastructure/Migrations/GuestLogContextModelSnapshot.cs +++ b/src/Somnia.GuestLog/Infrastructure/Migrations/GuestLogContextModelSnapshot.cs @@ -23,6 +23,24 @@ namespace Somnia.GuestLog.Infrastructure.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("GuestDocumentStayEntry", b => + { + b.Property("StayEntryId") + .HasColumnType("bigint"); + + b.Property("GuestDocumentsDocumentType") + .HasColumnType("character varying(8)"); + + b.Property("GuestDocumentsDocumentNum") + .HasColumnType("character varying(64)"); + + b.HasKey("StayEntryId", "GuestDocumentsDocumentType", "GuestDocumentsDocumentNum"); + + b.HasIndex("GuestDocumentsDocumentType", "GuestDocumentsDocumentNum"); + + b.ToTable("GuestDocumentStayEntry"); + }); + modelBuilder.Entity("Somnia.GuestLog.Models.Entities.GuestDocument", b => { b.Property("DocumentType") @@ -112,6 +130,58 @@ namespace Somnia.GuestLog.Infrastructure.Migrations b.ToTable("GuestDocument", (string)null); }); + + modelBuilder.Entity("Somnia.GuestLog.Models.Entities.StayEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DateIn") + .HasColumnType("timestamp(0) with time zone"); + + b.Property("DateOut") + .HasColumnType("timestamp(0) with time zone"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("character varying(5)"); + + b.Property("RoomCount") + .HasColumnType("integer"); + + b.Property("RoomNumber") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("StayEntry", (string)null); + }); + + modelBuilder.Entity("GuestDocumentStayEntry", b => + { + b.HasOne("Somnia.GuestLog.Models.Entities.StayEntry", null) + .WithMany() + .HasForeignKey("StayEntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Somnia.GuestLog.Models.Entities.GuestDocument", null) + .WithMany() + .HasForeignKey("GuestDocumentsDocumentType", "GuestDocumentsDocumentNum") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); #pragma warning restore 612, 618 } } diff --git a/src/Somnia.GuestLog/Models/DTOs/GuestDocFilterDTO.cs b/src/Somnia.GuestLog/Models/DTOs/GuestDocFilterDTO.cs index b7a7275..ff5a8ee 100644 --- a/src/Somnia.GuestLog/Models/DTOs/GuestDocFilterDTO.cs +++ b/src/Somnia.GuestLog/Models/DTOs/GuestDocFilterDTO.cs @@ -4,10 +4,17 @@ public class GuestDocFilterDTO { public string? DocumentType { get; set; } public string? DocumentNumber { get; set; } - + public string? Name { get; set; } public string? Surname { get; set; } public string? PhoneNumber { get; set; } + + public bool IsEmpty => string.IsNullOrEmpty(DocumentType) + && string.IsNullOrEmpty(DocumentNumber) + && string.IsNullOrEmpty(Name) + && string.IsNullOrEmpty(Surname) + && string.IsNullOrEmpty(PhoneNumber); + } diff --git a/src/Somnia.GuestLog/Models/DTOs/GuestDocumentDTO.cs b/src/Somnia.GuestLog/Models/DTOs/GuestDocumentDTO.cs index 206c8b8..bfee270 100644 --- a/src/Somnia.GuestLog/Models/DTOs/GuestDocumentDTO.cs +++ b/src/Somnia.GuestLog/Models/DTOs/GuestDocumentDTO.cs @@ -31,4 +31,7 @@ public class GuestDocumentDTO public DateTime? CreateTime { get; set; } public DateTime? UpdateTime { get; set; } + + + public string FullKey => $"{DocumentType}-{DocumentNum}"; } \ No newline at end of file diff --git a/src/Somnia.GuestLog/Models/DTOs/StayEntryDTO.cs b/src/Somnia.GuestLog/Models/DTOs/StayEntryDTO.cs new file mode 100644 index 0000000..94fa01e --- /dev/null +++ b/src/Somnia.GuestLog/Models/DTOs/StayEntryDTO.cs @@ -0,0 +1,20 @@ +namespace Somnia.GuestLog.Models.DTOs; + +public class StayEntryDTO +{ + public long? Id { get; set; } + + public DateTime? DateIn { get; set; } + public DateTime? DateOut { get; set; } + + public int? RoomCount { get; set; } + public string? RoomNumber { get; set; } + public string? PaymentMethod { get; set; } + + + public List GuestDocuments { get; set; } = []; + + + public DateTime? CreateTime { get; set; } + public DateTime? UpdateTime { get; set; } +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/Models/Entities/StayEntry.cs b/src/Somnia.GuestLog/Models/Entities/StayEntry.cs new file mode 100644 index 0000000..8668068 --- /dev/null +++ b/src/Somnia.GuestLog/Models/Entities/StayEntry.cs @@ -0,0 +1,24 @@ +namespace Somnia.GuestLog.Models.Entities; + +public class StayEntry : BaseEntity +{ + public long Id { get; set; } + + public required DateTime DateIn { get; set; } + public required DateTime DateOut { get; set; } + + public required int RoomCount { get; set; } + public string? RoomNumber { get; set; } + + public required string PaymentMethod { get; set; } + + public virtual ICollection GuestDocuments { get; } = []; + + + //Generated Properties. + public required DateTime CreateTime { get; set; } + public required DateTime UpdateTime { get; set; } + + + public override string GetFullKey() => $"{Id}"; +} diff --git a/src/Somnia.GuestLog/Models/EntityMapping.cs b/src/Somnia.GuestLog/Models/EntityMapping.cs new file mode 100644 index 0000000..332d749 --- /dev/null +++ b/src/Somnia.GuestLog/Models/EntityMapping.cs @@ -0,0 +1,52 @@ +using Somnia.GuestLog.Models.DTOs; +using Somnia.GuestLog.Models.Entities; + +namespace Somnia.GuestLog.Models; + +internal static class EntityMapping +{ + + public static GuestDocumentDTO MapGuestDocumentEntityToDTO(GuestDocument entity) + { + return new() + { + DocumentType = entity.DocumentType, + DocumentNum = entity.DocumentNum, + IssueDate = entity.IssueDate.ToDateTime(TimeOnly.MinValue), + SupportNum = entity.SupportNum, + Nationality = entity.Nationality, + Name = entity.Name, + Surname = entity.Surname, + Surname2 = entity.Surname2, + BirthDate = entity.BirthDate.ToDateTime(TimeOnly.MinValue), + Gender = entity.Gender, + PhoneNumber = entity.PhoneNumber, + EmailAddress = entity.EmailAddress, + Address = entity.AddressDetails?.Address ?? "", + AddressCity = entity.AddressDetails?.AddressCity ?? "", + AddressProvince = entity.AddressDetails?.AddressProvince ?? "", + AddressState = entity.AddressDetails?.AddressState ?? "", + AddressCountry = entity.AddressDetails?.AddressCountry! ?? "", + ZipCode = entity.AddressDetails?.ZipCode ?? "", + CreateTime = entity.CreateTime, + UpdateTime = entity.UpdateTime, + }; + } + + public static StayEntryDTO MapStayEntryEntityToDTO(StayEntry entity) + { + return new() + { + Id = entity.Id, + DateIn = entity.DateIn, + DateOut = entity.DateOut, + RoomCount = entity.RoomCount, + RoomNumber = entity.RoomNumber, + PaymentMethod = entity.PaymentMethod, + CreateTime = entity.CreateTime, + UpdateTime = entity.UpdateTime, + GuestDocuments = [.. entity.GuestDocuments.Select(e => MapGuestDocumentEntityToDTO(e))], + }; + } + +} diff --git a/src/Somnia.GuestLog/Program.cs b/src/Somnia.GuestLog/Program.cs index 427e27e..695e599 100644 --- a/src/Somnia.GuestLog/Program.cs +++ b/src/Somnia.GuestLog/Program.cs @@ -35,9 +35,11 @@ builder.Services.AddHttpClient(); // Add Validation Services builder.Services.AddSingleton, GuestDocumentDTOValidator>(); builder.Services.AddSingleton, GuestDocIdDTOValidator>(); +builder.Services.AddSingleton, StayEntryDTOValidator>(); // Add Services builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); // Add Factories (and it's services) diff --git a/src/Somnia.GuestLog/Resources/DataListLocale.resx b/src/Somnia.GuestLog/Resources/DataListLocale.resx index ab7f576..de9e14c 100644 --- a/src/Somnia.GuestLog/Resources/DataListLocale.resx +++ b/src/Somnia.GuestLog/Resources/DataListLocale.resx @@ -388,4 +388,10 @@ Female Other + Cash + Card + Wire Transfer + Phone + Other + \ No newline at end of file diff --git a/src/Somnia.GuestLog/Resources/LanguageLocale.resx b/src/Somnia.GuestLog/Resources/LanguageLocale.resx index 737f323..e781f48 100644 --- a/src/Somnia.GuestLog/Resources/LanguageLocale.resx +++ b/src/Somnia.GuestLog/Resources/LanguageLocale.resx @@ -137,6 +137,19 @@ Creation Time Update Time + Id + Date In + Date Out + Room Count + Room Number + Payment Method + Guests + Creation Time + Update Time + + TMP + + Actions Reload Data In Progress... Configure Columns @@ -151,6 +164,7 @@ Save Cancel No Results! + Show all elements Use '%' as wildcard Address @@ -163,20 +177,32 @@ Guest Documents Document List New Guest - Show all elements This Guest Document Already exists!! Go to existing document + Stay Entries + Entry List + New Stay + + New Stay + Document Details Guest Details Contact Details Guest Address Search Address + Stay Details + Guest List + Add Guest + Search Address Search Address Search Address Use Address + Search Guest Document + Use Guest + \ No newline at end of file diff --git a/src/Somnia.GuestLog/Services/GuestDocumentService.cs b/src/Somnia.GuestLog/Services/GuestDocumentService.cs index 78e8eb7..978b832 100644 --- a/src/Somnia.GuestLog/Services/GuestDocumentService.cs +++ b/src/Somnia.GuestLog/Services/GuestDocumentService.cs @@ -1,6 +1,7 @@ using FluentValidation; using Microsoft.EntityFrameworkCore; using Somnia.GuestLog.Infrastructure; +using Somnia.GuestLog.Models; using Somnia.GuestLog.Models.DTOs; using Somnia.GuestLog.Models.Entities; using Somnia.GuestLog.Utils; @@ -35,6 +36,7 @@ public class GuestDocumentService( ZipCode = dto.ZipCode, }; + DateTime createTime = DateTime.UtcNow; GuestDocument dbItem = new() { DocumentType = dto.DocumentType!, @@ -50,8 +52,8 @@ public class GuestDocumentService( PhoneNumber = dto.PhoneNumber, EmailAddress = dto.EmailAddress, AddressDetails = addressItem, - CreateTime = DateTime.UtcNow, - UpdateTime = DateTime.UtcNow, + CreateTime = createTime, + UpdateTime = createTime, }; try @@ -125,7 +127,7 @@ public class GuestDocumentService( .OrderByDescending(e => e.UpdateTime) .ToListAsync(); - return dbList.Select(x => GetDtoFromDb(x)); + return dbList.Select(GetDtoFromDb); } /// @@ -163,30 +165,13 @@ public class GuestDocumentService( return dbList.Select(x => GetDtoFromDb(x)); } - public GuestDocumentDTO GetDtoFromDb(GuestDocument dbItem) + + #region Internal Methods + + private static GuestDocumentDTO GetDtoFromDb(GuestDocument dbItem) { - return new() - { - DocumentType = dbItem.DocumentType, - DocumentNum = dbItem.DocumentNum, - IssueDate = dbItem.IssueDate.ToDateTime(TimeOnly.MinValue), - SupportNum = dbItem.SupportNum, - Nationality = dbItem.Nationality, - Name = dbItem.Name, - Surname = dbItem.Surname, - Surname2 = dbItem.Surname2, - BirthDate = dbItem.BirthDate.ToDateTime(TimeOnly.MinValue), - Gender = dbItem.Gender, - PhoneNumber = dbItem.PhoneNumber, - EmailAddress = dbItem.EmailAddress, - Address = dbItem.AddressDetails?.Address ?? "", - AddressCity = dbItem.AddressDetails?.AddressCity ?? "", - AddressProvince = dbItem.AddressDetails?.AddressProvince ?? "", - AddressState = dbItem.AddressDetails?.AddressState ?? "", - AddressCountry = dbItem.AddressDetails?.AddressCountry! ?? "", - ZipCode = dbItem.AddressDetails?.ZipCode ?? "", - CreateTime = dbItem.CreateTime, - UpdateTime = dbItem.UpdateTime, - }; + return EntityMapping.MapGuestDocumentEntityToDTO(dbItem); } + + #endregion } \ No newline at end of file diff --git a/src/Somnia.GuestLog/Services/IStayEntryService.cs b/src/Somnia.GuestLog/Services/IStayEntryService.cs new file mode 100644 index 0000000..2cc113f --- /dev/null +++ b/src/Somnia.GuestLog/Services/IStayEntryService.cs @@ -0,0 +1,19 @@ +using Somnia.GuestLog.Models.DTOs; + +namespace Somnia.GuestLog.Services; + +public interface IStayEntryService +{ + public Task CreateEntryAsync(StayEntryDTO dto); + public Task UpdateEntryAsync(StayEntryDTO updatedDto); + public Task DeleteEntryAsync(StayEntryDTO dto); + public Task DeleteEntryAsync(long id); + + public Task GetDtoByIdAsync(long id); + public Task> GetDtoListAsync(); + + + public Task IsGuestInStayInPeriod(string docType, string docNum, DateTime startTime, DateTime endTime); + public Task> GetStaysWithGuest(string docType, string docNum); + +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/Services/StaticDataListService.cs b/src/Somnia.GuestLog/Services/StaticDataListService.cs index 9b8f4e3..d1ac1bf 100644 --- a/src/Somnia.GuestLog/Services/StaticDataListService.cs +++ b/src/Somnia.GuestLog/Services/StaticDataListService.cs @@ -20,6 +20,7 @@ public class StaticDataListService( new KeyValuePair>(GuestConsts.DataList_DocumentTypes, _documentTypeSet), new KeyValuePair>(GuestConsts.DataList_GenderOptions, _genderOptionSet), new KeyValuePair>(GuestConsts.DataList_CountryCodes, _countryCodeSet), + new KeyValuePair>(GuestConsts.DataList_PaymentMethods, _paymentMethodSet), ]); public List GetAvailableLists() @@ -92,6 +93,14 @@ public class StaticDataListService( "VIR","VNM","VUT","WLF","WSM","YEM","ZAF","ZMB","ZWE", ]; + private static readonly HashSet _paymentMethodSet = [ + "EFECT", + "TARJT", + "TRANS", + "MOVIL", + "OTRO", + ]; + #endregion } \ No newline at end of file diff --git a/src/Somnia.GuestLog/Services/StayEntryService.cs b/src/Somnia.GuestLog/Services/StayEntryService.cs new file mode 100644 index 0000000..fcd46da --- /dev/null +++ b/src/Somnia.GuestLog/Services/StayEntryService.cs @@ -0,0 +1,162 @@ +using FluentValidation; +using Microsoft.EntityFrameworkCore; +using Somnia.GuestLog.Infrastructure; +using Somnia.GuestLog.Models; +using Somnia.GuestLog.Models.DTOs; +using Somnia.GuestLog.Models.Entities; + +namespace Somnia.GuestLog.Services; + +public class StayEntryService( + ILogger logger, + IDbContextFactory contextFactory, + IValidator dtoValidator +) : IStayEntryService +{ + public async Task CreateEntryAsync(StayEntryDTO dto) + { + ArgumentNullException.ThrowIfNull(dto); + + var valResult = await dtoValidator.ValidateAsync(dto); + if (!valResult.IsValid) + { + logger.LogWarning("StayEntryService - CreateEntryAsync: Dto validation failed. Fix the errors: {errorList}", valResult.Errors); + return false; + } + + DateTime createTime = DateTime.UtcNow; + StayEntry dbStay = new() + { + DateIn = dto.DateIn!.Value.ToUniversalTime(), + DateOut = dto.DateOut!.Value.ToUniversalTime(), + RoomCount = dto.RoomCount!.Value, + RoomNumber = dto.RoomNumber, + PaymentMethod = dto.PaymentMethod!, + CreateTime = createTime, + UpdateTime = createTime, + }; + + try + { + using var dbContext = contextFactory.CreateDbContext(); + + //Check and add GuestDocuments + foreach (GuestDocumentDTO guestDto in dto.GuestDocuments) + { + //Get it from db. + GuestDocument? dbGuest = await dbContext.GuestDocuments.FindAsync(guestDto.DocumentType, guestDto.DocumentNum) + ?? throw new InvalidOperationException($"GuestDocument not found! Id: '{guestDto.FullKey}'"); + + //Check if Guest is already in other stay during the same period. + bool isInOtherStay = await this.IsGuestInStayInPeriod(dbGuest.DocumentType, dbGuest.DocumentNum, dbStay.DateIn, dbStay.DateOut); + if (isInOtherStay) + { + throw new InvalidOperationException($"Guest ('{dbGuest.GetFullKey()}') is already in a stay during the period: {dbStay.DateIn}-{dbStay.DateOut}"); + } + + //Add dbGuest to dbStay. + dbStay.GuestDocuments.Add(dbGuest); + } + + await dbContext.AddAsync(dbStay); + await dbContext.SaveChangesAsync(); + + logger.LogInformation("StayEntryService - CreateEntryAsync: StayEntry Created: '{id}'", dbStay.GetFullKey()); + return true; + } + catch (Exception ex) + { + logger.LogError(ex, "StayEntryService - CreateEntryAsync: Error creating StayEntry"); + return false; + } + } + + public Task UpdateEntryAsync(StayEntryDTO updatedDto) + { + throw new NotImplementedException(); + } + + public Task DeleteEntryAsync(StayEntryDTO dto) + { + if (dto.Id is null) + { + throw new InvalidOperationException("StayEntryService - DeleteEntry: A DTO without key can't be deleted!"); + } + + return DeleteEntryAsync(dto.Id.Value); + } + + public async Task DeleteEntryAsync(long id) + { + using var dbContext = contextFactory.CreateDbContext(); + StayEntry? dbItem = await dbContext.StayEntries.FindAsync(id); + if (dbItem is null) + { + logger.LogWarning("StayEntryService - DeleteEntry: Error deleting StayEntry. Entity not found with key: '{key}'", id); + return false; + } + + try + { + dbContext.StayEntries.Remove(dbItem); + await dbContext.SaveChangesAsync(); + + logger.LogInformation("StayEntryService - DeleteEntry: StayEntry deleted: '{id}'", dbItem.GetFullKey()); + return true; + } + catch (Exception ex) + { + logger.LogError(ex, "StayEntryService - DeleteEntry: Error deleting StayEntry: '{id}'", dbItem.GetFullKey()); + return false; + } + } + + public async Task GetDtoByIdAsync(long id) + { + using var dbContext = contextFactory.CreateDbContext(); + StayEntry? dbItem = await dbContext.StayEntries.FindAsync(id); + return dbItem is not null ? GetDtoFromDb(dbItem) : null; + } + + public async Task> GetDtoListAsync() + { + using var dbContext = contextFactory.CreateDbContext(); + var dbList = await dbContext.StayEntries + .OrderByDescending(e => e.UpdateTime) + .ToListAsync(); + + return dbList.Select(GetDtoFromDb); + } + + + public async Task IsGuestInStayInPeriod(string docType, string docNum, DateTime startTime, DateTime endTime) + { + 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)); + + return rs; + } + + public async Task> GetStaysWithGuest(string docType, string docNum) + { + using var dbContext = contextFactory.CreateDbContext(); + + var rs = await dbContext.StayEntries.Where( + s => s.GuestDocuments.Any(g => g.DocumentType == docType && g.DocumentNum == docNum)) + .ToListAsync(); + + return rs.Select(GetDtoFromDb); + } + + #region Internal Methods + + private static StayEntryDTO GetDtoFromDb(StayEntry dbItem) + { + return EntityMapping.MapStayEntryEntityToDTO(dbItem); + } + + #endregion +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/Validators/StayEntryDTOValidator.cs b/src/Somnia.GuestLog/Validators/StayEntryDTOValidator.cs new file mode 100644 index 0000000..678bcf3 --- /dev/null +++ b/src/Somnia.GuestLog/Validators/StayEntryDTOValidator.cs @@ -0,0 +1,31 @@ +using FluentValidation; +using Somnia.GuestLog.Models.DTOs; +using Somnia.GuestLog.Services; + +namespace Somnia.GuestLog.Validators; + +public class StayEntryDTOValidator : ValueValidator +{ + public StayEntryDTOValidator(IDataListService dataListService) + { + RuleFor(e => e.DateIn) + .NotEmpty() + .LessThanOrEqualTo(DateTime.Today.AddDays(1).AddSeconds(-1)); + + RuleFor(e => e.DateOut) + .NotEmpty() + .GreaterThan(DateTime.Today); + + RuleFor(e => e.RoomCount) + .NotEmpty() + .GreaterThan(0); + + RuleFor(e => e.PaymentMethod) + .Cascade(CascadeMode.Stop) + .NotEmpty() + .Must(val => val != null && dataListService.IsValueInList(GuestConsts.DataList_PaymentMethods, val)); + + RuleFor(e => e.GuestDocuments) + .NotEmpty(); + } +} diff --git a/src/Somnia.GuestLog/Validators/ValueValidator.cs b/src/Somnia.GuestLog/Validators/ValueValidator.cs new file mode 100644 index 0000000..e16886b --- /dev/null +++ b/src/Somnia.GuestLog/Validators/ValueValidator.cs @@ -0,0 +1,21 @@ +using FluentValidation; + +namespace Somnia.GuestLog.Validators; + +/// +/// Base class for validators that will be used by MudForms. +/// +/// Type that is validated +public abstract class ValueValidator : AbstractValidator +{ + /// + /// Method used by MudForm to validate properties. + /// + public Func>> ValidateValue => async (model, propertyName) => + { + var result = await ValidateAsync(ValidationContext.CreateWithOptions((T)model, x => + x.IncludeProperties(propertyName))); + return result.IsValid ? [] : result.Errors.Select(e => e.ErrorMessage); + }; + +}