Compare commits

..
31 Commits
Author SHA256 Message Date
Daniel-Garmig 0ab3957efa Localization - Added Spanish
- Added language selector.
- Translated strings to Spanish.
- Added Package MudBlazor.Translations for translations.
2026-09-18 12:42:04 +02:00
Daniel-Garmig fbe1750510 GuestDocument - Page create - Bugfix
Fixed a bug that caused exceptions when "Create New" button was clicked before searching if GuestDocument already exists.
2026-09-17 17:24:58 +02:00
Daniel-Garmig 23c8d490d8 Updated packages
Updated:
- FluentStorage
- EntityFrameworkCore
- MudBlazor
2026-09-17 17:19:58 +02:00
Daniel-Garmig c5d8487046 Small fixes and improvements:
- GuestDocument - At least one contact field is required.
- GuestDocument - List Page - Hide not useful columns: "version"
- StayEntry - Show stayEntry ID in UI msg after creation.
- StayEntry - Create Page - Time fields don't reset anymore when dates change.
- Show local times in UI instead of UTC times.
- Validators - Use expresions for dates so they are recalculated each time.
2026-09-12 15:28:25 +02:00
Daniel-Garmig 491ecc4cd1 GuestDocument - Add edit mode.
Added Edit mode for GuestDocument in GuestDocument details page.
2026-09-10 19:34:46 +02:00
Daniel-Garmig a1a55aa396 Small fixes and improvements:
- StayExport - SpainParteViajerosProvider - Fixed validation for codigoPostal/codMunicipio.
- AddressQueryDialog - Add loading indication and "debounce" input to reduce API queries.
2026-09-10 18:27:56 +02:00
Daniel-Garmig dc85319da5 StayExport - SpainParteViajerosProvider - Fixes in export format
- Added required XML Namespace (ns2)
- Reorder some elements.
- Fix collection serializations.
- Fix gender code mapping.
- Improve some error messages.
- CodigoMunicipioCatastro - Add padding to codes to follow required format.
2026-09-10 15:05:05 +02:00
Daniel-Garmig 0abdfcd683 GuestDocument - Improvements to GuestDocument creation:
- IssueDate is now Nullable. Is not required for DNI and NIE document types.
- Improvements in SupportNumber for NIE documents.
- Address search can now be skipped.
2026-09-10 11:53:41 +02:00
Daniel-Garmig ae2ea51060 Small fixes and improvements:
- Show app version on Sidebar.
- StayEntryForm - Remove unnecesary refs to components.
- Fixed some result and log messages in entity services.
2026-09-10 11:26:21 +02:00
Daniel-Garmig dcd5bb2aad Deploy configuration
- Added tasks for publishing as container image.
- Improvements for deployment: Db migrations on startup, reverse-proxy forward headers, and DataProtect management.
- Added deploy example using docker-compose.
2026-09-09 17:50:27 +02:00
Daniel-Garmig fb2e00f06c Added Serilog for improved logs
- Logs will be shown by console and stored in files.
2026-09-08 21:13:35 +02:00
Daniel-Garmig 9ec87d56da feat StayExport - Added ExportDetails for StayExports
Store a new field ('ExportDetails') with export details. This is a JSON field in DB that can store information about the export depending on the format provider used.

Changes:
- Updated models and created required migrations.
- Show field in StayExport list.
2026-09-08 19:58:47 +02:00
Daniel-Garmig 7b8020879b feat StayExport - StayExportProvider SpainParteViejeros - Initial implementation
- Added initial implementation to export stays in "SpainParteViajeros" format.
- Added required configurations.
2026-09-08 14:31:00 +02:00
Daniel-Garmig dc5293bdf7 feat StayExport - Added service SpainCodigoMunicipio
- This service gets the "municipio" code of INE using Catastro API services.
2026-09-08 13:27:17 +02:00
Daniel-Garmig 4b72e3b3d3 feat StayExport - Added delete for StayExports. 2026-09-06 22:24:25 +02:00
Daniel-Garmig 6fe5a5e5a3 feat StayExport - Refactor Export Services.
- Created new service: IStayExportService" for common export operations.
- Updated older services to be "IStayExportFormatProvider". This services will only implement the export format.
- Renamed "IStayExportServiceFactory" to "IStayExportFormatProviderFactory" as this factory now is used to get the format providers for the given format.
2026-09-04 22:33:01 +02:00
Daniel-Garmig 058352b7f3 feat StayExport - Added common Interface "IResult" for Result so both (generic and non-generic one) can be used easier. 2026-09-04 20:50:38 +02:00
Daniel-Garmig 313a714a99 feat StayExport - Stay Export Pages - Initial implementation 2026-09-04 20:38:00 +02:00
Daniel-Garmig cf31cbc414 feat StayExport - Added LocalizerService
- Added new "LocalizerService" to get localization for enum values (and other dynamic/db values in the future).
2026-09-02 13:15:10 +02:00
Daniel-Garmig b318f2415c feat StayExport - Moved Entity Services. 2026-09-01 18:14:27 +02:00
Daniel-Garmig c24faedee5 feat StayExport - Initial Implementation
- Created models for DB and DTOs: StayExportFile
- Added required db migrations.
- Added StayExportService and StayExportServiceFactory for stay exporting services.
- Added configuration options required for StayExport.
2026-09-01 18:14:22 +02:00
Daniel-Garmig 7052ec734c feat StayExport - Added lib FluentStorage 2026-09-01 15:23:23 +02:00
Daniel-Garmig 887c3d787a GuestDocument Versions - Part 1:
- Changed GuestDocument PK from `DocumentType`+`DocumentNumber` to to `ID`+`Version` for future versioning.
- Added DB field "IsLastVersion" to simplify queries where only last/current version is allowed.
- Updated GuestDocument DB fields type for `CreateTime` & `UpdateTime` to add precision.
- Updated usages of GuestDocument PK to new PK.
- Limit delete of a GuestDocument when it's currently used in a StayEntry.

Note: DB changes require recreating db tables, so all data is lost and migration "rollback" is not allowed.
2026-09-01 15:04:38 +02:00
Daniel-Garmig a01eed82e1 GuestLog - Nominatim AddressQueryService fix for Provinces.
- Small fix in Nominatim AddressQueryService to map provinces using fields "state_district" and "county" in results.
2026-09-01 11:01:05 +02:00
Daniel-Garmig b08aaf2d7b Moved and renamed Db/Entity services for better organization. 2026-08-30 20:20:03 +02:00
Daniel-Garmig 4f22cd4541 GuestLog - Moved Enums
- Enums moved to Enum folder (by type).
- ResultCode class changed to enum to ensure type safety in results.
2026-08-25 15:18:02 +02:00
Daniel-Garmig b18bd7caa6 GuestLog - StayEntry pages fixes
- Details/New - Fixed time field binding. Now it binds directly to the Model instead of using temp TimeSpans.
- New - Fixed StayEntry validation to allow creating Stays with a DateOut in the past.
- List - Added ordering to db results so limit behaves in a deterministic way. Fixed default search so stays created today are shown.
2026-08-24 20:50:38 +02:00
Daniel-Garmig 511bd023f2 GuestLog - User message improvements
- Added UserMessageService to have common methods for showing messages and errors to the user.
- Added Result record so services can return status on operation error (so it can be shown in the UI messages).
- Updated services to use the new Result record.
- Updated pages to use the new IUserMessageService.
2026-08-23 21:57:26 +02:00
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
103 changed files with 5455 additions and 667 deletions
+9
View File
@@ -0,0 +1,9 @@
{
"files.associations": {
"appsettings*.json": "jsonc",
"*_config.json": "jsonc",
"*_config.json.sample": "jsonc"
},
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "modificationsIfAvailable",
}
+36
View File
@@ -0,0 +1,36 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"type": "dotnet",
"task": "build",
"group": "build",
"problemMatcher": [],
"label": "Build Somnia.GuestLog"
},
{
"label": "Publish Container Image",
"type": "shell",
"command": "dotnet",
"args": [
"publish",
"-c",
"Release",
"--os",
"linux",
"--arch",
"x64",
"-p:PublishProfile=DefaultContainer"
],
"options": {
"cwd": "${workspaceFolder}/src/Somnia.GuestLog"
},
"group": "build",
"presentation": {
"reveal": "always"
}
},
]
}
+23
View File
@@ -0,0 +1,23 @@
# Somnia.GuestLog Deployment
## Deploy using docker-compose
Use [docker-compose file](docker-compose.yaml) as example. Configure required enviroment variables.
Notes:
- By default, HTTP-only is deploy. Uncomment required sections to enable HTTPS (and mount required certs).
- Reverse-proxies should work?
[To-Be-Improved]
## Manually update registry
Container images are built by CI/CD pipelines and uploaded to the registry (**not yet!**).
If required, a image can be manually built and uploaded to the registry (for testing or development purposes):
```sh
podman push [IMAGE_ID] [REGISTRY]/somnia/somnia-guestlog:[TAG]
# Example:
podman push localhost/somnia-guestlog:latest gitea.uberelectronnetwork.cc/somnia/somnia-guestlog:devbuild
```
+66
View File
@@ -0,0 +1,66 @@
services:
database:
environment:
POSTGRES_DB: ${POSTGRES_DB:-SomniaGuestLog}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-NotAStrongPassword}
POSTGRES_USER: ${POSTGRES_USER:-somnia}
healthcheck:
retries: 5
start_period: 60s
test:
- CMD
- pg_isready
timeout: 5s
image: postgres:18-alpine
volumes:
- somniaguestlog_database_data:/var/lib/postgresql:rw
somnia-guestlog:
depends_on:
- database
environment:
SOMNIA_ConnectionStrings__DbSomniaGuestLog: Host=database;Port=5432;Username=${POSTGRES_USER:-somnia};Password=${POSTGRES_PASSWORD:-NotAStrongPassword};Database=${POSTGRES_DB:-SomniaGuestLog}
# Configure path for data protection keys. This path must persists between container restarts (use a mount or a volume).
SOMNIA_DataProtectionDir: /SomniaGuestLog/dataprotection
ASPNETCORE_URLS: "http://+:8080"
# Somnia AddressQuery Service - START
SOMNIA_AddressQuery__Provider: "Nominatim"
# Override if required.
# SOMNIA_AddressQuery__ServiceURL: ""
# SOMNIA_AddressQuery__ApiKey: ""
SOMNIA_AddressQuery__UserAgent: Somnia-GuestLog-Dev-Container
SOMNIA_AddressQuery__ResultLang: es
# Somnia AddressQuery Service - END
SOMNIA_StayExport__ExportStorageConnectionString: disk://path=/SomniaGuestLog/StayExportFiles
# Somnia StayExport - SpainParteViajeros Provider - START
# Override vars if required
# SOMNIA_StayExportSpainParteViajeros__InternetAvailable: true
# SOMNIA_StayExportSpainParteViajeros__CodigoEstablecimiento: 123123123
# Somnia StayExport - SpainParteViajeros Provider - END
# Enable for HTTPS - START
# ASPNETCORE_URLS: "https://+:8090;http://+:8080"
# ASPNETCORE_HTTPS_PORTS: 8090
# ASPNETCORE_Kestrel__Certificates__Default__Password: ${CERTIFICATE_CREDENTIAL:-}
# ASPNETCORE_Kestrel__Certificates__Default__Path: /https/aspnetapp.pfx
# Enable for HTTPS - END
image: localhost/somnia-guestlog:latest
# image: gitea.uberelectronnetwork.cc/somnia/somnia-guestlog:devbuild
ports:
- protocol: tcp
published: 8080
target: 8080
# Enable for HTTPS
# - protocol: tcp
# published: 8090
# target: 8090
restart: unless-stopped
volumes:
- somniaguestlog_app_data:/SomniaGuestLog
# Enable for HTTPS and configure path to your certificate.
# - ${HOME}/.aspnet/https:/https/
volumes:
somniaguestlog_database_data: Null
somniaguestlog_app_data: Null
+18 -7
View File
@@ -6,23 +6,34 @@
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Extensions.MudBlazor.StaticInput" Version="4.1.0" />
<PackageVersion Include="FluentStorage" Version="8.0.22" />
<PackageVersion Include="FluentStorage.AWS" Version="8.0.21" />
<PackageVersion Include="FluentValidation" Version="12.1.1" />
<PackageVersion Include="Grpc.AspNetCore" Version="2.64.0" />
<PackageVersion Include="Microsoft.Extensions.Localization" Version="10.0.9" />
<PackageVersion Include="MudBlazor" Version="9.4.0" />
<PackageVersion Include="MudBlazor" Version="9.10.0" />
<PackageVersion Include="MudBlazor.Translations" Version="3.6.0" />
<PackageVersion Include="Serilog" Version="4.4.0" />
<PackageVersion Include="Serilog.Enrichers.Process" Version="3.0.0" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageVersion Include="Serilog.Extensions.Hosting" Version="10.0.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="10.0.1" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="10.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="10.0.12" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.7" />
</ItemGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7">
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.12" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.12">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageVersion>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.7" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.12" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.12" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Proxies" Version="10.0.12" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
</ItemGroup>
</Project>
+8
View File
@@ -0,0 +1,8 @@
namespace Somnia.GuestLog;
public static class AppSettings
{
public const string AppName = "Somnia-GuestLog-dev";
public const string AppVersion = "v0.0.0-dev"; //2026-09-08
}
+1
View File
@@ -23,6 +23,7 @@
<ReconnectModal />
<script src="@Assets["_framework/blazor.web.js"]"></script>
<script src="@Assets["_content/MudBlazor/MudBlazor.min.js"]"></script>
<script src="js/App.js"></script>
</body>
</html>
@@ -1,9 +1,10 @@
@using Somnia.GuestLog.Models.DTOs;
@using Somnia.GuestLog.Services
@using Somnia.GuestLog.Services.AddressQuery;
@inject ILogger<AddressQueryDialog> logger;
@inject IAddressQueryFactory addressQueryFactory;
@inject ISnackbar Snackbar
@inject IUserMessageService userMessage
<MudDialog @ref="@DialogRef">
<TitleContent>
@@ -16,13 +17,22 @@
<MudStack row>
<MudTextField @bind-Value="SearchText" Label="@LanguageLocale.control_AddressQueryDialog_searchText"
Required />
<MudIconButton Icon="@Icons.Material.Filled.Search" OnClick="btnSearchAddress" />
<MudIconButton Icon="@Icons.Material.Filled.Search" OnClick="btnSearchAddress" Disabled="@_isDataLoading" />
</MudStack>
<MudPaper Class="mt-4 pa-2">
@if (addressResults.Count == 0)
@if (addressResults.Count == 0 && !_isDataLoading)
{
<MudText Typo="Typo.body1">@LanguageLocale.general_noResults</MudText>
<MudElement Style="width: 100%; height: 100px;" HtmlTag="div">
<MudText Typo="Typo.body1" Align="Align.Center">
@LanguageLocale.general_noResults
</MudText>
</MudElement>
}
@if (_isDataLoading)
{
<MudSkeleton Width="100%" Height="100px" />
}
<MudList T="AddressResultDTO" @bind-SelectedValue="SelectedAddressResult"
@@ -80,6 +90,9 @@
private List<AddressResultDTO> addressResults = [];
private AddressResultDTO? SelectedAddressResult { get; set; }
private bool _isDataLoading = false;
private string _lastSearchText = "";
private void Cancel()
{
@@ -98,21 +111,25 @@
private async Task btnSearchAddress()
{
if (SearchText.Length == 0)
if (SearchText.Length == 0 || SearchText == _lastSearchText)
{
return;
}
try
{
var queryService = addressQueryFactory.GetService();
_isDataLoading = true;
var queryService = addressQueryFactory.GetService();
addressResults = await queryService.SearchAddressAsync(SearchText);
_lastSearchText = SearchText;
_isDataLoading = false;
}
catch (Exception ex)
{
logger.LogError(ex, "SearchAddress returned an error!");
Snackbar.Add($"Error searching address: {ex.Message}", Severity.Error);
userMessage.ShowMessage($"Error searching address: {ex.Message}", MessageKind.Error);
}
}
@@ -120,7 +137,7 @@
{
if (SelectedAddressResult is null)
{
Snackbar.Add("Select an address", Severity.Warning);
userMessage.ShowMessage("Select an address", MessageKind.Warning);
return;
}
@@ -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,9 +1,10 @@
@using Somnia.GuestLog.Models.DTOs;
@using Somnia.GuestLog.Services
@using Somnia.GuestLog.Services.Entity
@inject ILogger<AddressQueryDialog> logger;
@inject ISnackbar Snackbar
@inject IGuestDocumentService guestDocumentService;
@inject IUserMessageService userMessage
@inject IGuestDocumentEntityService guestDocumentService;
<MudDialog Style="max-height: 90%;">
@@ -34,6 +35,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)
@@ -69,41 +78,64 @@
private IMudDialogInstance MudDialog { get; set; } = null!;
private GuestDocFilterDTO filter { get; set; } = new();
private GuestDocFilterDTO filter { get; set; } = new()
{
OnlyLastVersion = true
};
private IEnumerable<GuestDocumentDTO> results = [];
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 BtnSearchGuestDocuments()
private async Task LoadGuests(int limit = 25)
{
if (filter.IsEmpty)
{
Snackbar.Add("Add some search!", Severity.Warning);
return;
}
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);
userMessage.ShowMessage($"Error searching: {ex.Message}", MessageKind.Error);
}
}
#region Event Handlers
private async Task BtnSearchGuestDocuments()
{
if (filter.IsEmpty)
{
userMessage.ShowMessage("Search can't be empty!", MessageKind.Warning);
return;
}
//Load unlimited guests using current filter.
await LoadGuests();
}
private async Task btnSelectGuest()
{
if (SelectedResult is null)
{
Snackbar.Add("Select a guest document", Severity.Warning);
userMessage.ShowMessage("A guest document must be selected", MessageKind.Warning);
return;
}
@@ -114,4 +146,7 @@
MudDialog.Close(dialogResult);
return;
}
#endregion
}
@@ -0,0 +1,60 @@
@using System.Globalization
@using Microsoft.AspNetCore.Localization
@inject IJSRuntime JS
@inject NavigationManager Navigation
<MudItem>
<MudIconButton Icon="@Icons.Material.Outlined.Translate" Color="Color.Primary" DropShadow="false"
OnClick="@BtnToggleOpen" />
<MudPopover Open="_open" AnchorOrigin="Origin.BottomCenter" TransformOrigin="Origin.TopLeft" Paper="false">
<MudPaper Outlined="true" Class="px-4 py-8">
<MudSelect Value="currentCulture" T="CultureInfo" Label="@LanguageLocale.general_languageSelector" FullWidth="true"
ValueChanged="(culture) => CmbSelectedCultureChanged(culture)">
@foreach (CultureInfo culture in supportedCultures)
{
<MudSelectItem Value="culture">
@culture.DisplayName
</MudSelectItem>
}
</MudSelect>
</MudPaper>
</MudPopover>
</MudItem>
@code {
private bool _open;
private CultureInfo? currentCulture;
private List<CultureInfo> supportedCultures = [
new CultureInfo("en-US"),
new CultureInfo("es-ES"),
];
protected override void OnInitialized()
{
currentCulture = CultureInfo.CurrentCulture;
}
private void BtnToggleOpen() => _open = !_open;
private async Task CmbSelectedCultureChanged(CultureInfo? newCulture)
{
if (newCulture is null)
{
return;
}
string cookieValue = CookieRequestCultureProvider.MakeCookieValue(
new RequestCulture(newCulture, newCulture));
await JS.InvokeVoidAsync("setBlazorCultureCookie", cookieValue);
Navigation.NavigateTo(Navigation.Uri, forceLoad: true);
//Close Popover.
_open = false;
}
}
@@ -6,53 +6,61 @@
@inject IDataListService dataListService;
@inject IValidator<GuestDocumentDTO> guestDocValidator;
@inject IUserMessageService userMessage;
<MudForm @ref="@_form" Model="@Model" Validation="@((guestDocValidator as GuestDocumentDTOValidator)!.ValidateValue)" ValidationDelay="0"
ReadOnly="@ReadOnly">
<MudForm @ref="@_form" Model="@Model" Validation="@((guestDocValidator as GuestDocumentDTOValidator)!.ValidateValue)"
ValidationDelay="0" ReadOnly="@ReadOnly">
<MudGrid>
<MudItem xs="12" md="6">
<MudPaper Class="pa-4" Elevation="3" Height="100%">
<MudText Typo="Typo.subtitle1" Align="Align.Center">@LanguageLocale.control_GuestDocForm_sectionDocument</MudText>
<MudText Typo="Typo.subtitle1" Align="Align.Center">
@LanguageLocale.control_GuestDocForm_sectionDocument
</MudText>
<MudDivider DividerType="DividerType.Middle" Class="mb-4" />
<MudStack Row style="@FieldMarginY">
<MudSelect @bind-Value="Model.DocumentType" For="@(() => Model.DocumentType)" Label="@LanguageLocale.dto_GuestDocument_DocumentType"
Required ReadOnly="@ReadOnlyDocId">
@foreach (string docType in dataListService.GetDataListByName(GuestConsts.DataList_DocumentTypes))
<MudSelect @bind-Value="Model.DocumentType" For="@(() => Model.DocumentType)"
Label="@LanguageLocale.dto_GuestDocument_DocumentType" Required ReadOnly="@ReadOnlyDocId">
@foreach (string docType in
dataListService.GetDataListByName(GuestConsts.DataList_DocumentTypes))
{
<MudSelectItem Value="docType">
@dataListService.GetLocalizedValue(GuestConsts.DataList_DocumentTypes, docType)
</MudSelectItem>
}
</MudSelect>
<MudTextField @bind-Value="Model.DocumentNum" For="@(() => Model.DocumentNum)" Label="@LanguageLocale.dto_GuestDocument_DocumentNum"
Required ReadOnly="@ReadOnlyDocId" />
<MudTextField @bind-Value="Model.DocumentNum" For="@(() => Model.DocumentNum)"
Label="@LanguageLocale.dto_GuestDocument_DocumentNum" Required ReadOnly="@ReadOnlyDocId" />
</MudStack>
<MudStack Row style="@FieldMarginY">
<MudDatePicker @bind-Date="Model.IssueDate" For="@(() => Model.IssueDate)" Label="@LanguageLocale.dto_GuestDocument_IssueDate"
Clearable OpenTo="OpenTo.Year" Editable />
<MudDatePicker @bind-Date="Model.IssueDate" For="@(() => Model.IssueDate)"
Label="@LanguageLocale.dto_GuestDocument_IssueDate" Clearable OpenTo="OpenTo.Year" Editable />
<MudTextField @bind-Value="Model.SupportNum" For="@(() => Model.SupportNum)" Label="@LanguageLocale.dto_GuestDocument_SupportNum"
Required="@isSuportNumberRequired" Mask="@(new PatternMask("aaa000000"))"
<MudTextField @bind-Value="Model.SupportNum" For="@(() => Model.SupportNum)"
Label="@LanguageLocale.dto_GuestDocument_SupportNum" Required="@isSuportNumberRequired"
Mask="SuportNumberPatternMask"
OnBlur="@(() => Model.SupportNum = Model.SupportNum?.ToUpper())" />
</MudStack>
<MudAutocomplete T="string" @bind-Value="Model.Nationality" For="@(() => Model.Nationality)" Label="@LanguageLocale.dto_GuestDocument_Nationality"
SearchFunc="@SearchCountryCodes" ToStringFunc="@(e=> string.IsNullOrEmpty(e) ? "" : @dataListService.GetLocalizedValue(GuestConsts.DataList_CountryCodes, e))"
SelectValueOnTab="true" AdornmentIcon="@Icons.Material.Filled.Search"
style="@FieldMarginY" />
<MudAutocomplete T="string" @bind-Value="Model.Nationality" For="@(() => Model.Nationality)"
Label="@LanguageLocale.dto_GuestDocument_Nationality" SearchFunc="@SearchCountryCodes"
ToStringFunc="@(e=> string.IsNullOrEmpty(e) ? "" : @dataListService.GetLocalizedValue(GuestConsts.DataList_CountryCodes, e))"
SelectValueOnTab="true" AdornmentIcon="@Icons.Material.Filled.Search" style="@FieldMarginY"
Required />
</MudPaper>
</MudItem>
<MudItem xs="12" md="6">
<MudPaper Class="pa-4" Elevation="3" Height="100%">
<MudText Typo="Typo.subtitle1" Align="Align.Center">@LanguageLocale.control_GuestDocForm_sectionGuest</MudText>
<MudText Typo="Typo.subtitle1" Align="Align.Center">
@LanguageLocale.control_GuestDocForm_sectionGuest
</MudText>
<MudDivider DividerType="DividerType.Middle" Class="mb-4" />
<MudTextField @bind-Value="Model.Name" For="@(() => Model.Name)"
<MudTextField @bind-Value="Model.Name" For="@(() => Model.Name)" Required
Label="@LanguageLocale.dto_GuestDocument_Name"
OnBlur="@(() => Model.Name = Model.Name?.ToUpper())" />
<MudTextField @bind-Value="Model.Surname" For="@(() => Model.Surname)"
<MudTextField @bind-Value="Model.Surname" For="@(() => Model.Surname)" Required
Label="@LanguageLocale.dto_GuestDocument_Surname"
OnBlur="@(() => Model.Surname = Model.Surname?.ToUpper())" />
<MudTextField @bind-Value="Model.Surname2" For="@(() => Model.Surname2)"
@@ -60,11 +68,14 @@
OnBlur="@(() => Model.Surname2 = Model.Surname2?.ToUpper())" />
<MudStack Row style="@FieldMarginY">
<MudDatePicker @bind-Date="Model.BirthDate" For="@(() => Model.BirthDate)" Label="@LanguageLocale.dto_GuestDocument_BirthDate"
Clearable OpenTo="OpenTo.Year" Editable />
<MudDatePicker @bind-Date="Model.BirthDate" For="@(() => Model.BirthDate)"
Label="@LanguageLocale.dto_GuestDocument_BirthDate" OpenTo="OpenTo.Year" Clearable Required
Editable />
<MudSelect @bind-Value="Model.Gender" For="@(() => Model.Gender)" Label="@LanguageLocale.dto_GuestDocument_Gender">
@foreach (string gender in dataListService.GetDataListByName(GuestConsts.DataList_GenderOptions))
<MudSelect @bind-Value="Model.Gender" For="@(() => Model.Gender)"
Label="@LanguageLocale.dto_GuestDocument_Gender">
@foreach (string gender in
dataListService.GetDataListByName(GuestConsts.DataList_GenderOptions))
{
<MudSelectItem Value="gender">
@dataListService.GetLocalizedValue(GuestConsts.DataList_GenderOptions, gender)
@@ -77,50 +88,58 @@
<MudItem xs="12" md="6">
<MudPaper Class="pa-4" Elevation="3" Height="100%">
<MudText Typo="Typo.subtitle1" Align="Align.Center">@LanguageLocale.control_GuestDocForm_sectionContact</MudText>
<MudText Typo="Typo.subtitle1" Align="Align.Center">
@LanguageLocale.control_GuestDocForm_sectionContact
</MudText>
<MudDivider DividerType="DividerType.Middle" Class="mb-4" />
<MudStack Row>
<MudTextField @bind-Value="Model.PhoneNumber" For="@(() => Model.PhoneNumber)"
Label="@LanguageLocale.dto_GuestDocument_PhoneNumber"
InputType="InputType.Telephone" />
Label="@LanguageLocale.dto_GuestDocument_PhoneNumber" InputType="InputType.Telephone" />
<MudTextField @bind-Value="Model.EmailAddress" For="@(() => Model.EmailAddress)"
Label="@LanguageLocale.dto_GuestDocument_EmailAddress"
InputType="InputType.Email" />
Label="@LanguageLocale.dto_GuestDocument_EmailAddress" InputType="InputType.Email" />
</MudStack>
</MudPaper>
</MudItem>
<MudItem xs="12" md="6">
<MudPaper Class="pa-4" Elevation="3" Height="100%">
<MudText Typo="Typo.subtitle1" Align="Align.Center">@LanguageLocale.control_GuestDocForm_sectionAddress</MudText>
<MudText Typo="Typo.subtitle1" Align="Align.Center">
@LanguageLocale.control_GuestDocForm_sectionAddress
</MudText>
<MudDivider DividerType="DividerType.Middle" Class="mb-4" />
@if (!ReadOnly)
{
<MudButton OnClick="BtnOpenAddressSearchDialog" Variant="Variant.Outlined" FullWidth="true" Class="mb-2">
@LanguageLocale.control_GuestDocForm_btnSearchAddress
</MudButton>
<MudStack Row>
<MudButton OnClick="BtnOpenAddressSearchDialog" Variant="Variant.Outlined" FullWidth="true"
Class="mb-2">
@LanguageLocale.control_GuestDocForm_btnSearchAddress
</MudButton>
<MudIconButton OnClick="BtnSkipAddressSearch" Variant="Variant.Outlined" Disabled="!ReadOnlyAddress"
Icon="@Icons.Material.Filled.SkipNext" Class="mb-2" />
</MudStack>
}
<MudTextField @bind-Value="Model.Address" For="@(() => Model.Address)"
Label="@LanguageLocale.general_address_address" ReadOnly="@ReadOnlyAddress" />
Label="@LanguageLocale.general_address_address" ReadOnly="@ReadOnlyAddress" />
<MudStack Row style="@FieldMarginY">
<MudTextField @bind-Value="Model.AddressCity" For="@(() => Model.AddressCity)"
Label="@LanguageLocale.general_address_city" ReadOnly="@ReadOnlyAddress" />
Label="@LanguageLocale.general_address_city" ReadOnly="@ReadOnlyAddress" />
<MudTextField @bind-Value="Model.AddressProvince" For="@(() => Model.AddressProvince)"
Label="@LanguageLocale.general_address_province" ReadOnly="@ReadOnlyAddress" />
Label="@LanguageLocale.general_address_province" ReadOnly="@ReadOnlyAddress" />
<MudTextField @bind-Value="Model.AddressState" For="@(() => Model.AddressState)"
Label="@LanguageLocale.general_address_state" ReadOnly="@ReadOnlyAddress" />
Label="@LanguageLocale.general_address_state" ReadOnly="@ReadOnlyAddress" />
</MudStack>
<MudStack Row style="@FieldMarginY">
<MudAutocomplete T="string" @bind-Value="Model.AddressCountry" For="@(() => Model.AddressCountry)"
Label="@LanguageLocale.general_address_country" ReadOnly="@ReadOnlyAddress"
SearchFunc="@SearchCountryCodes" ToStringFunc="@(e=> string.IsNullOrEmpty(e) ? "" : @dataListService.GetLocalizedValue(GuestConsts.DataList_CountryCodes, e))"
SelectValueOnTab="true" AdornmentIcon="@Icons.Material.Filled.Search" />
Label="@LanguageLocale.general_address_country" ReadOnly="@ReadOnlyAddress"
SearchFunc="@SearchCountryCodes"
ToStringFunc="@(e=> string.IsNullOrEmpty(e) ? "" : @dataListService.GetLocalizedValue(GuestConsts.DataList_CountryCodes, e))"
SelectValueOnTab="true" AdornmentIcon="@Icons.Material.Filled.Search" />
<MudTextField @bind-Value="Model.ZipCode" For="@(() => Model.ZipCode)"
Label="@LanguageLocale.general_address_zipCode" ReadOnly="@ReadOnlyAddress" />
Label="@LanguageLocale.general_address_zipCode" ReadOnly="@ReadOnlyAddress" />
</MudStack>
</MudPaper>
</MudItem>
@@ -155,7 +174,20 @@
private Dictionary<string, string>? countryDict = null;
private bool isSuportNumberRequired { get => Model?.Nationality == GuestConsts.CountryCode_ESP; }
private bool isSuportNumberRequired
{
get => Model?.DocumentType == GuestConsts.DocumentType_DNI
|| Model?.DocumentType == GuestConsts.DocumentType_NIE;
}
private PatternMask SuportNumberPatternMask
{
get => Model?.DocumentType switch
{
GuestConsts.DocumentType_DNI => new PatternMask("aaa000000"),
GuestConsts.DocumentType_NIE => new PatternMask("a00000000"),
_ => new PatternMask("")
};
}
private string CreateTimeString
{
get
@@ -193,7 +225,7 @@
countryDict = new(countryCodeSet.Count);
//Init dict.
foreach(string code in dataListService.GetDataListByName(GuestConsts.DataList_CountryCodes))
foreach (string code in dataListService.GetDataListByName(GuestConsts.DataList_CountryCodes))
{
countryDict.Add(code, dataListService.GetLocalizedValue(GuestConsts.DataList_CountryCodes, code));
}
@@ -237,6 +269,14 @@
ReadOnlyAddress = ReadOnly;
}
private async Task BtnSkipAddressSearch()
{
userMessage.ShowMessage("Address search skipped! Address is editable now", MessageKind.Info);
//Make Address editable (if the form is editable).
ReadOnlyAddress = ReadOnly;
}
#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;
@@ -7,7 +7,7 @@
@inject IDataListService dataListService;
@inject IValidator<StayEntryDTO> stayValidator;
@inject IDialogService dialogService;
@inject ISnackbar snackbar
@inject IUserMessageService userMessage
<MudForm @ref="@_form" Model="@Model" Validation="@((stayValidator as StayEntryDTOValidator)!.ValidateValue)"
@@ -21,12 +21,18 @@
<MudDivider DividerType="DividerType.Middle" Class="mb-4" />
<MudStack Row style="@FieldMarginY">
<MudDatePicker @bind-Date="Model.DateIn" For="@(() => Model.DateIn)"
Label="@LanguageLocale.dto_StayEntry_DateIn" Clearable OpenTo="OpenTo.Date" Editable />
<MudTimePicker @bind-Time="TimeIn" Label=" " Editable />
<MudDatePicker @bind-Date="Model.DateOut" For="@(() => Model.DateOut)"
Label="@LanguageLocale.dto_StayEntry_DateOut" Clearable OpenTo="OpenTo.Date" Editable />
<MudTimePicker @bind-Time="TimeOut" Label=" " Editable />
<MudDatePicker Date="Model.DateIn" For="@(() => Model.DateIn)"
DateChanged="@((newVal) => Model.DateIn = GetDateTimeFromDateAndTime(newVal, Model.DateIn?.TimeOfDay))"
Label="@LanguageLocale.dto_StayEntry_DateIn" Clearable OpenTo="OpenTo.Date" Editable
AutoClose="true" />
<MudTimePicker Time="Model.DateIn?.TimeOfDay" Label=" " Editable
TimeChanged="@((newVal) => Model.DateIn = GetDateTimeFromDateAndTime(Model.DateIn, newVal))" />
<MudDatePicker Date="Model.DateOut" For="@(() => Model.DateOut)"
DateChanged="@((newVal) => Model.DateOut = GetDateTimeFromDateAndTime(newVal, Model.DateOut?.TimeOfDay))"
Label="@LanguageLocale.dto_StayEntry_DateOut" Clearable OpenTo="OpenTo.Date" Editable
AutoClose="true" />
<MudTimePicker Time="Model.DateOut?.TimeOfDay" Label=" " Editable
TimeChanged="@((newVal) => Model.DateOut = GetDateTimeFromDateAndTime(Model.DateOut, newVal))" />
</MudStack>
<MudStack Row style="@FieldMarginY">
<MudTextField @bind-Value="Model.RoomCount" For="@(() => Model.RoomCount)"
@@ -69,10 +75,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>
}
<MudTh>@LanguageLocale.general_actions</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="@LanguageLocale.dto_GuestDocument_DocumentType">
@@ -94,7 +97,14 @@
{
<MudTd DataLabel="@LanguageLocale.general_actions">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
OnClick="@(() => BtnDeleteGuest(context.DocumentType!, context.DocumentNum!))" />
OnClick="@(() => BtnDeleteGuest(context.Id!.Value, context.Version!.Value))" />
</MudTd>
}
@if (ReadOnly)
{
<MudTd DataLabel="@LanguageLocale.general_actions">
<MudIconButton Icon="@Icons.Material.Filled.Info"
OnClick="@(() => BtnViewGuestDetails(context))" />
</MudTd>
}
</RowTemplate>
@@ -109,11 +119,11 @@
[Parameter]
public StayEntryDTO Model { private get; set; } = new();
[Parameter]
public bool ReadOnly { get; set; } = false;
[Parameter, EditorRequired]
public FormMode FormMode { get; set; }
private TimeSpan? TimeIn = DateTime.Now.TimeOfDay;
private TimeSpan? TimeOut = new(hours: 12, minutes: 0, seconds: 0);
private bool ReadOnly => FormMode == FormMode.ReadOnly;
private MudForm _form = null!;
@@ -134,7 +144,20 @@
protected override void OnInitialized()
{
Model.DateIn = DateTime.Today;
//Initialize values when not ReadOnly.
if (FormMode == FormMode.Create)
{
Model.DateIn = DateTime.Now;
DateTime dateOut = DateTime.Today.AddDays(1);
Model.DateOut = new(
year: dateOut.Year, month: dateOut.Month, day: dateOut.Day,
hour: 12, minute: 0, second: 0
);
//Init Guest List
Model.GuestDocuments = [];
}
}
#region Public Methods
@@ -143,9 +166,9 @@
{
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);
userMessage.ShowMessage("At least 1 guest is required!", MessageKind.Warning);
return false;
}
@@ -154,10 +177,6 @@
public StayEntryDTO GetModel()
{
//Update model times.
Model.DateIn = GetDateTimeFromDateAndTime(Model.DateIn, TimeIn);
Model.DateOut = GetDateTimeFromDateAndTime(Model.DateOut, TimeOut);
return Model;
}
@@ -181,7 +200,17 @@
private async Task BtnAddGuest()
{
var dialog = await dialogService.ShowAsync<GuestDocumentSearchDialog>();
Debug.Assert(Model.GuestDocuments != null);
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)
@@ -193,26 +222,47 @@
if (resultData is null)
{
snackbar.Add("Error! No Guest Document selected!", MudBlazor.Severity.Error);
userMessage.ShowMessage("Error! No Guest Document selected!", MessageKind.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)
if (Model.GuestDocuments.Count > 0)
{
snackbar.Add("GuestDocument already added!", MudBlazor.Severity.Warning);
return;
bool alreadyAdded = Model.GuestDocuments.Any(e => e.Id == resultData.Id);
if (alreadyAdded)
{
userMessage.ShowMessage("GuestDocument already added!", MessageKind.Warning);
return;
}
}
Model.GuestDocuments.Add(resultData);
}
private void BtnDeleteGuest(string docType, string docNum)
private void BtnDeleteGuest(long guestDocId, int version)
{
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.Id == guestDocId && e.Version == version))
.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
@@ -19,9 +19,14 @@
<MudToggleIconButton Class="ml-auto" Icon="@Icons.Material.Outlined.LightMode"
ToggledIcon="@Icons.Material.Outlined.DarkMode" ToggledColor="Color.Primary" Color="Color.Primary"
Toggled="@_isDarkMode" ToggledChanged="@DarkModeToggle" />
<LanguageSelector />
</MudAppBar>
<MudDrawer id="nav-drawer" @bind-Open="_drawerOpen" ClipMode="DrawerClipMode.Docked" Elevation="2">
<NavMenu />
<MudSpacer />
<MudText Typo="Typo.caption" Class="mx-2" GutterBottom>
@AppSettings.AppName - @AppSettings.AppVersion
</MudText>
</MudDrawer>
<MudMainContent Class="pt-16 pa-4">
@Body
@@ -1,12 +1,19 @@
<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>
<MudNavLink Href="@GuestConsts.Page_StayExport_List" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.ImportExport">
@LanguageLocale.navMenu_StayExport
</MudNavLink>
</MudNavMenu>
@@ -1,11 +1,12 @@
@page "/debug/addrSearch"
@using Somnia.GuestLog.Models.DTOs;
@using Somnia.GuestLog.Services;
@using Somnia.GuestLog.Services.AddressQuery;
@inject ILogger<AddressQuery> logger;
@inject IAddressQueryFactory addressQueryFactory;
@inject ISnackbar Snackbar
@inject IUserMessageService userMessage
@inject IDialogService DialogService
<PageTitle>DEBUG - Address Query</PageTitle>
@@ -89,7 +90,7 @@
catch (Exception ex)
{
logger.LogError(ex, "SearchAddress returned an error!");
Snackbar.Add($"Error searching address: {ex.Message}", Severity.Error);
userMessage.ShowMessage($"Error searching address: {ex.Message}", MessageKind.Error);
}
}
@@ -1,8 +1,10 @@
@page "/debug/empty"
@using Somnia.GuestLog.Services
@inject ILogger<Empty> logger;
@inject ISnackbar Snackbar
@inject IUserMessageService userMessage
@inject IDialogService DialogService
<PageTitle>DEBUG - Empty</PageTitle>
@@ -2,27 +2,43 @@
@using Somnia.GuestLog.Models.DTOs;
@using Somnia.GuestLog.Services;
@using Microsoft.AspNetCore.WebUtilities;
@inject ISnackbar Snackbar
@inject IUserMessageService userMessage
@inject IDialogService DialogService
@inject NavigationManager Navigation
@inject IGuestDocumentService guestDocService
@inject IStayEntryService stayService
@inject IGuestDocumentEntityService guestDocService
@inject IStayEntryEntityService stayService
@inject IDialogService DialogService
<PageTitle>Somnia - Guest Docs - Details</PageTitle>
<MudStack Row="true" Class="my-4">
<MudText Typo="Typo.h4" GutterBottom="true">Guest Document - "@docId"</MudText>
<MudText Typo="Typo.h4" GutterBottom="true">Guest Document: @docId - @documentModel?.FullDocument</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>
@if (_isEditMode)
{
<MudButton Variant="Variant.Filled" Color="Color.Error" Size="Size.Small" StartIcon="@Icons.Material.Filled.Cancel"
OnClick="() => BtnCancelEdit()" Disabled="!_isEditMode">
@LanguageLocale.general_btnCancel
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Success" Size="Size.Small" StartIcon="@Icons.Material.Filled.Save"
OnClick="() => BtnSaveEdit()" Disabled="!_isEditMode">
@LanguageLocale.general_btnSave
</MudButton>
}
@if (!_isEditMode)
{
<MudButton Variant="Variant.Filled" Color="Color.Success" Size="Size.Small" StartIcon="@Icons.Material.Filled.Edit"
OnClick="() => BtnEnableEdit()" Disabled="_isEditMode">
@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 (documentModel is not null)
@@ -31,10 +47,11 @@
Centered>
<MudTabPanel Text="Details" Icon="@Icons.Material.Filled.Info">
<GuestDocForm @ref="_form" Model="documentModel" ReadOnly="true" />
<GuestDocForm @ref="_form" Model="documentModel" ReadOnly="!_isEditMode" />
</MudTabPanel>
<MudTabPanel Text="Stays" ToolTip="Guest stays" OnClick="LoadTabGuestStays" Icon="@Icons.Material.Filled.Bed">
<MudTabPanel Text="Stays" ToolTip="Guest stays" Icon="@Icons.Material.Filled.Bed" OnClick="LoadTabGuestStays"
Disabled="_isEditMode">
<MudText>Total Stays: @(guestStaysLoading ? "Loading..." : guestStayList.Count())</MudText>
<MudTable Items="@guestStayList" Hover="true" Loading="@guestStaysLoading" Class="mt-2" Elevation="2">
<HeaderContent>
@@ -42,6 +59,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 +74,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>
@@ -72,7 +93,7 @@
@code {
[Parameter]
public string? docId { get; set; }
public long docId { get; set; }
GuestDocForm _form = null!;
@@ -82,44 +103,27 @@
IEnumerable<StayEntryDTO> guestStayList = [];
bool guestStaysLoading = true;
private bool _isEditMode = false;
protected override async Task OnInitializedAsync()
{
if (docId is null)
{
Snackbar.Add("No GuestDocument provided!!", Severity.Error);
Navigation.NotFound();
return;
}
GuestDocIdDTO? docIdModel = GuestDocIdDTO.DecodeFromString(docId);
if (docIdModel is null)
{
Snackbar.Add($"Provided GuestDocId can't be decoded: '{docId}'", Severity.Error);
Navigation.NotFound();
return;
}
//Get model from db.
documentModel = await guestDocService.GetDtoByIdAsync(docIdModel.DocumentType, docIdModel.DocumentNum);
await LoadGuestDocumentAsync();
}
private async Task LoadGuestDocumentAsync()
{
//Get model from db.
documentModel = await guestDocService.GetLastVersionDtoByIdAsync(docId);
if (documentModel is null)
{
Snackbar.Add($"GuestDocument not found with ID: '{docId}'", Severity.Error);
userMessage.ShowMessage($"GuestDocument not found with ID: '{docId}'", MessageKind.Error);
Navigation.NotFound();
return;
}
}
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.
@@ -128,13 +132,101 @@
return;
}
if (documentModel is null || documentModel.DocumentType is null || documentModel.DocumentNum is null)
if (documentModel is null || documentModel.Id is null || documentModel.Version is null)
{
throw new InvalidOperationException();
}
guestStaysLoading = true;
guestStayList = await stayService.GetStaysWithGuest(documentModel.DocumentType, documentModel.DocumentNum);
guestStayList = await stayService.GetStaysWithGuest(documentModel.Id.Value);
guestStaysLoading = false;
}
#region Event Handlers
private void BtnEnableEdit()
{
Debug.Assert(_isEditMode == false);
userMessage.ShowMessage(LanguageLocale.page_GuestDocDetail_msgEditEnabled, MessageKind.Normal);
_isEditMode = true;
}
private async Task BtnCancelEdit()
{
Debug.Assert(_isEditMode == true);
var result = await DialogService.ShowMessageBoxAsync(
LanguageLocale.general_confirmTitle, LanguageLocale.page_GuestDocDetail_msgConfirmCancelEdit,
yesText: "OK", cancelText: LanguageLocale.general_btnCancel);
if (result == null || result.Value == false)
{
// Cancel cancel -> Continue with edit.
return;
}
//Cancel edit -> Reload data from DB.
await LoadGuestDocumentAsync();
userMessage.ShowMessage(LanguageLocale.general_msgEditCanceled, MessageKind.Normal);
_isEditMode = false;
}
private async Task BtnSaveEdit()
{
Debug.Assert(_isEditMode == true && documentModel is not null);
var confirmResult = await DialogService.ShowMessageBoxAsync(
LanguageLocale.general_confirmTitle, LanguageLocale.page_GuestDocDetail_msgConfirmSaveEdit,
yesText: "OK", cancelText: LanguageLocale.general_btnCancel);
if (confirmResult == null || confirmResult.Value == false)
{
// Cancel save -> Continue with edit.
return;
}
//Show Loading dialog...
var loadingDialog = await DialogService.ShowAsync<LoadingDialog>();
//Save edit.
Result updateResult = await guestDocService.UpdateDocumentAsync(documentModel);
if (!updateResult.IsSuccess)
{
string errorMsg = string.Format(LanguageLocale.page_GuestDocDetail_msgEditError, documentModel.FullDocument);
userMessage.ShowResultWithMessage(updateResult, errorMsg);
loadingDialog.Close();
return;
}
//Reload data from DB.
await LoadGuestDocumentAsync();
loadingDialog.Close();
userMessage.ShowMessage(LanguageLocale.general_msgEditSaved, MessageKind.Success);
_isEditMode = false;
}
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
}
@@ -3,9 +3,9 @@
@using Somnia.GuestLog.Models.DTOs;
@using Somnia.GuestLog.Services;
@inject IGuestDocumentService GuestDocService
@inject IGuestDocumentEntityService GuestDocService
@inject NavigationManager Navigation
@inject ISnackbar Snackbar
@inject IUserMessageService userMessage
@inject IDialogService DialogService
@inject IDataListService dataListService;
@@ -43,6 +43,9 @@
</MudStack>
</ToolBarContent>
<Columns>
<PropertyColumn Property="x => x.Id" Title="@LanguageLocale.dto_GuestDocument_Id" Hideable="false" />
<PropertyColumn Property="x => x.Version" Title="@LanguageLocale.dto_GuestDocument_Version" Hidden
Hideable="true" />
<PropertyColumn Title="@LanguageLocale.dto_GuestDocument_DocumentType"
Property="x => dataListService.GetLocalizedValue(GuestConsts.DataList_DocumentTypes, x.DocumentType)"
Hideable="false" />
@@ -50,6 +53,7 @@
Title="@LanguageLocale.dto_GuestDocument_DocumentNum" />
<PropertyColumn Property="x => x.Name" Title="@LanguageLocale.dto_GuestDocument_Name" />
<PropertyColumn Property="x => x.Surname" Title="@LanguageLocale.dto_GuestDocument_Surname" />
<PropertyColumn Property="x => x.Surname2" Title="@LanguageLocale.dto_GuestDocument_Surname2" />
<PropertyColumn Title="@LanguageLocale.dto_GuestDocument_Nationality"
Property="x => dataListService.GetLocalizedValue(GuestConsts.DataList_CountryCodes, x.Nationality)" />
<PropertyColumn Property="x => x.UpdateTime" Title="@LanguageLocale.dto_GuestDocument_UpdateTime" />
@@ -69,17 +73,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 +86,7 @@
private GuestDocFilterDTO docFilter = new GuestDocFilterDTO();
private MudDataGrid<GuestDocumentDTO> _dataGrid = null!;
private MudMessageBox _msgBoxConfirmDelete = null!;
private ConfirmDeleteMsgBox _msgBoxConfirmDelete = null!;
protected override async Task OnInitializedAsync()
@@ -111,7 +105,7 @@
if (showMsg)
{
Snackbar.Add("Data loaded from DB", Severity.Info, o => o.VisibleStateDuration = 1000);
userMessage.ShowMessage("Data loaded from DB", MessageKind.Info);
}
_dataLoading = false;
}
@@ -136,31 +130,31 @@
private async Task BtnViewDetails(GuestDocumentDTO item)
{
GuestDocIdDTO idDto = GuestDocIdDTO.BuildFromDto(item) ?? throw new InvalidOperationException();
string strId = item.Id?.ToString() ?? throw new InvalidOperationException();
string itemId = GuestDocIdDTO.EncodeAsString(idDto);
string uri = GuestConsts.Page_GuestDocs_Detail.Replace(GuestConsts.Page_GuestDocs_Detail_Param_DocId, itemId);
string uri = GuestConsts.Page_GuestDocs_Detail.Replace(GuestConsts.Page_GuestDocs_Detail_Param_DocId, strId);
Navigation.NavigateTo(uri);
}
private async Task BtnDeleteItem(GuestDocumentDTO item)
{
bool? confirmDelete = await _msgBoxConfirmDelete.ShowAsync();
bool? confirmDelete = await _msgBoxConfirmDelete.MsgBox.ShowAsync();
if (confirmDelete is null || confirmDelete.Value == false)
{
return;
}
bool deleted = await GuestDocService.DeleteDocumentAsync(item);
if (!deleted)
Result deleteResult = await GuestDocService.DeleteDocumentAsync(item);
if (!deleteResult.IsSuccess)
{
Snackbar.Add($"Error deleting document {item.DocumentType}-{item.DocumentNum}", Severity.Error);
string errorMsg = $"Error deleting document {item.DocumentType}-{item.DocumentNum}";
userMessage.ShowResultWithMessage(deleteResult, errorMsg);
return;
}
Snackbar.Add($"Document {item.DocumentType}-{item.DocumentNum} deleted!", Severity.Success);
await LoadDataAsync();
userMessage.ShowMessage($"Document {item.DocumentType}-{item.DocumentNum} deleted!", MessageKind.Success);
await LoadDataAsync(showMsg: false);
}
private void BtnNewGuest()
@@ -4,10 +4,10 @@
@using Somnia.GuestLog.Services;
@using Somnia.GuestLog.Validators;
@inject ISnackbar Snackbar
@inject IUserMessageService userMessage
@inject IDialogService DialogService
@inject NavigationManager Navigation
@inject IGuestDocumentService guestDocService
@inject IGuestDocumentEntityService guestDocService
@inject IDataListService dataListService;
@inject FluentValidation.IValidator<GuestDocIdDTO> docIdValidator;
@@ -18,10 +18,13 @@
<MudStack Row="true" Class="my-4">
<MudText Typo="Typo.h4" GutterBottom="true">New Guest Document</MudText>
<MudSpacer />
<MudButton Variant="Variant.Filled" Color="Color.Success" Size="Size.Small" StartIcon="@Icons.Material.Filled.Add"
OnClick="() => BtnSaveNewGuest()">
@LanguageLocale.general_btnCreate
</MudButton>
@if (currentPageMode == ePageMode.createNew)
{
<MudButton Variant="Variant.Filled" Color="Color.Success" Size="Size.Small" StartIcon="@Icons.Material.Filled.Add"
OnClick="() => BtnSaveNewGuest()">
@LanguageLocale.general_btnCreate
</MudButton>
}
<MudButton Variant="Variant.Filled" Color="Color.Default" Size="Size.Small"
StartIcon="@Icons.Material.Filled.ArrowBack" OnClick="() => BtnReturn()">
@LanguageLocale.general_btnReturn
@@ -82,6 +85,7 @@
ePageMode currentPageMode = ePageMode.selectDocId;
GuestDocIdDTO currentDocId = new() { DocumentNum = "", DocumentType = "" };
long? alreadyExistDocId = null;
GuestDocumentDTO newDocumentModel = new();
bool loadingDocId = false;
@@ -100,7 +104,8 @@
}
loadingDocId = true;
GuestDocumentDTO? dto = await guestDocService.GetDtoByIdAsync(currentDocId.DocumentType, currentDocId.DocumentNum);
GuestDocumentDTO? dto =
await guestDocService.GetLastVersionDtoByDocumentAsync(currentDocId.DocumentType, currentDocId.DocumentNum);
loadingDocId = false;
if (dto is null)
@@ -117,6 +122,7 @@
//GuestDoc already exists!
currentPageMode = ePageMode.alreadyExistDoc;
alreadyExistDocId = dto.Id;
}
private void BtnReturn()
@@ -126,42 +132,44 @@
private async Task BtnSaveNewGuest()
{
Debug.Assert(currentPageMode == ePageMode.createNew && _form != null);
bool isFormValid = await _form.ValidateForm();
if (!isFormValid)
{
Snackbar.Add("Invalid Form!", Severity.Error);
userMessage.ShowMessage("Invalid Form!", MessageKind.Warning);
return;
}
GuestDocumentDTO model = _form.Model;
Snackbar.Add($"Document '{model.DocumentNum}' creating...", Severity.Info);
//Show Loading dialog...
var loadingDialog = await DialogService.ShowAsync<LoadingDialog>();
bool isCreated = await guestDocService.CreateDocumentAsync(model);
if (!isCreated)
Result createResult = await guestDocService.CreateDocumentAsync(model);
if (!createResult.IsSuccess)
{
Snackbar.Add($"Error creating Document '{model.DocumentNum}'", Severity.Error);
string errorMsg = $"Error creating Document '{model.FullDocument}'";
userMessage.ShowResultWithMessage(createResult, errorMsg);
loadingDialog.Close();
return;
}
Snackbar.Add($"Document '{model.DocumentNum}' Created!", Severity.Success);
userMessage.ShowMessage($"Document '{model.FullDocument}' Created!", MessageKind.Success);
Navigation.NavigateTo(GuestConsts.Page_GuestDocs_List);
}
private void BtnGoToDocument()
{
if (currentPageMode != ePageMode.alreadyExistDoc)
if (currentPageMode != ePageMode.alreadyExistDoc || alreadyExistDocId is null)
{
Snackbar.Add("Invalid operation!", Severity.Error);
userMessage.ShowMessage("Invalid operation!", MessageKind.Error);
return;
}
string docId = GuestDocIdDTO.EncodeAsString(currentDocId);
string uri = GuestConsts.Page_GuestDocs_Detail.Replace("{docId}", docId);
string strId = alreadyExistDocId.Value.ToString();
string uri = GuestConsts.Page_GuestDocs_Detail.Replace(GuestConsts.Page_GuestDocs_Detail_Param_DocId, strId);
Navigation.NavigateTo(uri);
}
@@ -0,0 +1,91 @@
@attribute [Route($"{GuestConsts.Page_StayEntry_Detail}")]
@using Somnia.GuestLog.Models.DTOs;
@using Somnia.GuestLog.Services;
@inject IUserMessageService userMessage
@inject IDialogService DialogService
@inject NavigationManager Navigation
@inject IGuestDocumentEntityService guestDocService
@inject IStayEntryEntityService 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" FormMode="FormMode.ReadOnly" />
<MudText Class="mt-4">@CreateTimeString</MudText>
}
@code {
[Parameter]
public long StayId { get; set; }
[SupplyParameterFromQuery(Name = GuestConsts.QueryParam_ReturnUrl)]
public string? ReturnUrl { get; set; }
StayEntryDTO? stayModel = null;
private string? CreateTimeString
{
get
{
if (stayModel is null)
{
return null;
}
List<string> textList = [];
textList.Add($"Created at: {stayModel.CreateTime?.ToString() ?? "???"}");
textList.Add($"Last Update Time: {stayModel.UpdateTime?.ToString() ?? "???"}");
return string.Join("; ", textList);
}
}
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)
{
userMessage.ShowMessage($"Stay not found with ID: '{StayId}'", MessageKind.Error);
Navigation.NotFound();
return;
}
}
private void BtnEdit()
{
userMessage.ShowMessage("Not implemented yet!", MessageKind.Info);
}
private void BtnReturn()
{
string uri = ReturnUrl ?? GuestConsts.Page_StayEntry_List;
Navigation.NavigateTo(uri);
}
}
@@ -3,9 +3,9 @@
@using Somnia.GuestLog.Models.DTOs;
@using Somnia.GuestLog.Services;
@inject IStayEntryService StayEntryService
@inject IStayEntryEntityService StayEntryService
@inject NavigationManager Navigation
@inject ISnackbar Snackbar
@inject IUserMessageService userMessage
@inject IDialogService DialogService
@inject IDataListService dataListService;
@@ -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.AddDays(1),
};
private IEnumerable<StayEntryDTO> stayEntryList = [];
private MudDataGrid<StayEntryDTO> _dataGrid = null!;
private ConfirmDeleteMsgBox _confirmDeleteMsgBox = null!;
protected override async Task OnInitializedAsync()
@@ -79,12 +92,11 @@
_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)
{
Snackbar.Add("Data loaded from DB", Severity.Info, o => o.VisibleStateDuration = 1000);
userMessage.ShowMessage("Data loaded from DB", MessageKind.Info);
}
_dataLoading = false;
}
@@ -102,6 +114,35 @@
Navigation.NavigateTo(GuestConsts.Page_StayEntry_New);
}
private async Task BtnViewDetails(StayEntryDTO item)
{
string itemId = item.Id?.ToString() ?? throw new InvalidOperationException();
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;
}
Result deleteResult = await StayEntryService.DeleteEntryAsync(item);
if (!deleteResult.IsSuccess)
{
string errorMsg = $"Error deleting Stay {item.Id}";
userMessage.ShowResultWithMessage(deleteResult, errorMsg);
return;
}
userMessage.ShowMessage($"Stay {item.Id} deleted!", MessageKind.Success);
await LoadDataAsync();
}
#endregion
}
@@ -4,10 +4,10 @@
@using Somnia.GuestLog.Services;
@inject ISnackbar Snackbar
@inject IUserMessageService userMessage
@inject IDialogService DialogService
@inject NavigationManager Navigation
@inject IStayEntryService stayService
@inject IStayEntryEntityService stayService
@inject IDataListService dataListService;
<PageTitle>Somnia - Stay Entries - New</PageTitle>
@@ -27,7 +27,7 @@
</MudButton>
</MudStack>
<StayEntryForm @ref="_form" Model="@newStayModel" />
<StayEntryForm @ref="_form" Model="@newStayModel" FormMode="FormMode.Create" />
@code {
StayEntryForm _form = null!;
@@ -45,22 +45,22 @@
bool isFormValid = await _form.ValidateForm();
if (!isFormValid)
{
Snackbar.Add("Invalid Form!", Severity.Warning);
userMessage.ShowMessage("Invalid Form!", MessageKind.Warning);
return;
}
var loadingDialog = await DialogService.ShowAsync<LoadingDialog>();
StayEntryDTO model = _form.GetModel();
bool isCreated = await stayService.CreateEntryAsync(model);
if (!isCreated)
Result createResult = await stayService.CreateEntryAsync(model);
if (!createResult.IsSuccess)
{
Snackbar.Add($"Error creating StayEntry", Severity.Error);
userMessage.ShowResultWithMessage(createResult, $"Error creating StayEntry");
loadingDialog.Close();
return;
}
Snackbar.Add($"StayEntry Created!", Severity.Success);
userMessage.ShowMessage("StayEntry Created: (Id: {0})", MessageKind.Success, createResult.Context ?? ["???"]);
Navigation.NavigateTo(GuestConsts.Page_StayEntry_List);
}
@@ -0,0 +1,197 @@
@attribute [Route($"{GuestConsts.Page_StayExport_List}")]
@using System.Text.Json;
@using Somnia.GuestLog.Models.DTOs;
@using Somnia.GuestLog.Services;
@using Somnia.GuestLog.Services.StayExport;
@inject IStayExportFileEntityService stayExportEntityService
@inject NavigationManager Navigation
@inject IUserMessageService userMessage
@inject IDialogService DialogService
@inject IDataListService dataListService;
@inject ILocalizerService Localizer;
@inject IStayExportService ExportService;
@inject IJSRuntime JS
<PageTitle>Somnia - Stay Export Files</PageTitle>
<MudStack Row="true" Class="my-4">
<MudText Typo="Typo.h4" GutterBottom="true">@LanguageLocale.page_StayExportList_pageTitle</MudText>
<MudSpacer />
<MudButton Variant="Variant.Filled" Color="Color.Primary" Size="Size.Small"
StartIcon="@Icons.Material.Filled.Search" OnClick="() => BtnSearchDataAsync()">
@LanguageLocale.general_btnSearch
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Size="Size.Small" StartIcon="@Icons.Material.Filled.Add"
OnClick="() => BtnNewExport()">
@LanguageLocale.page_StayExportList_btnNewExport
</MudButton>
</MudStack>
@* <MudPaper Class="pa-4 my-4" Elevation="2" Height="100%">
FILTER
</MudPaper> *@
<MudDataGrid @ref="_dataGrid" T="StayExportFileDTO" Items="@stayExportList" Loading="@_dataLoading" ReadOnly="true"
Elevation="2">
<ToolBarContent>
<MudText Typo="Typo.subtitle1">@LanguageLocale.page_StayExportList_exportList</MudText>
</ToolBarContent>
<Columns>
<PropertyColumn Property="x => x.Id" Hideable="false" Title="@LanguageLocale.dto_StayExportFile_Id" />
<PropertyColumn Property="x => Localizer.GetEnumValue(x.ExportFormat)"
Title="@LanguageLocale.dto_StayExportFile_ExportFormat" />
<TemplateColumn Title="@LanguageLocale.dto_StayExportFile_ExportParams">
<CellTemplate>
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Outlined.Info"
OnClick="() => ShowExportParams(context.Item)" />
</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => Localizer.GetEnumValue(x.ExportResult)"
Title="@LanguageLocale.dto_StayExportFile_ExportResult" />
<PropertyColumn Property="x => x.ExportTime" Title="@LanguageLocale.dto_StayExportFile_ExportTime" />
<TemplateColumn Title="@LanguageLocale.dto_StayExportFile_ExportDetails">
<CellTemplate>
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Outlined.Info"
OnClick="() => ShowExportDetails(context.Item)" />
</CellTemplate>
</TemplateColumn>
<TemplateColumn CellClass="d-flex justify-between" Title="Options" StickyRight Hideable="false"
ShowColumnOptions="false">
<CellTemplate>
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Outlined.Download"
OnClick="() => BtnDownloadFile(context.Item)" />
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Outlined.Delete"
OnClick="() => BtnDeleteItem(context.Item)" />
</CellTemplate>
</TemplateColumn>
</Columns>
<PagerContent>
<MudDataGridPager T="StayExportFileDTO" />
</PagerContent>
</MudDataGrid>
<ConfirmDeleteMsgBox @ref="_confirmDeleteMsgBox" />
@code {
private bool _dataLoading = true;
private const int resultLimit = 200;
private IEnumerable<StayExportFileDTO> stayExportList = [];
private MudDataGrid<StayExportFileDTO> _dataGrid = null!;
private ConfirmDeleteMsgBox _confirmDeleteMsgBox = null!;
protected override async Task OnInitializedAsync()
{
await LoadDataAsync(showMsg: false);
}
private async Task LoadDataAsync(bool showMsg = true)
{
_dataLoading = true;
stayExportList = await stayExportEntityService.GetDtoListAsync(limit: resultLimit);
if (showMsg)
{
userMessage.ShowMessage("Data loaded from DB", MessageKind.Info);
}
_dataLoading = false;
}
#region Event Handlers
private async Task BtnSearchDataAsync()
{
await LoadDataAsync();
}
private void BtnNewExport()
{
Navigation.NavigateTo(GuestConsts.Page_StayExport_New);
}
private async Task ShowExportParams(StayExportFileDTO dto)
{
string jsonParams = JsonSerializer.Serialize(dto.ExportParams, options: new() { WriteIndented = true });
await DialogService.ShowMessageBoxAsync("Details", (MarkupString)$"<pre>{jsonParams}</pre>");
}
private async Task ShowExportDetails(StayExportFileDTO dto)
{
if (string.IsNullOrEmpty(dto.ExportDetails))
{
userMessage.ShowMessage("This ExportFile doesn't have any export details", MessageKind.Info);
return;
}
using var jDoc = JsonDocument.Parse(dto.ExportDetails);
string jsonDetails = JsonSerializer.Serialize(jDoc, new JsonSerializerOptions { WriteIndented = true });
await DialogService.ShowMessageBoxAsync("Details", (MarkupString)$"<pre>{jsonDetails}</pre>");
}
private async Task BtnDownloadFile(StayExportFileDTO item)
{
if (item.ExportResult == Enums.Models.StayExportFileResult.NoData)
{
userMessage.ShowMessage("A Export without data can't be downloaded!", MessageKind.Warning);
return;
}
if (string.IsNullOrEmpty(item.ExportedPath))
{
userMessage.ShowMessage("File can't be downloaded: no path found!", MessageKind.Warning);
return;
}
using Stream objectStream = new MemoryStream();
Result loadResult = await ExportService.GetExportedFileAsync(item, objectStream);
if (!loadResult.IsSuccess)
{
userMessage.ShowResultWithMessage(loadResult, "Exported File can't be downloaded");
return;
}
objectStream.Seek(0, SeekOrigin.Begin);
using var streamRef = new DotNetStreamReference(stream: objectStream);
var fileName = Path.GetFileName(item.ExportedPath);
await JS.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef);
userMessage.ShowMessage("Exported File downloaded!", MessageKind.Success);
}
private async Task BtnDeleteItem(StayExportFileDTO item)
{
bool? confirmDelete = await _confirmDeleteMsgBox.MsgBox.ShowAsync();
if (confirmDelete is null || confirmDelete.Value == false)
{
return;
}
Result deleteResult = await ExportService.DeleteExportedFileAsync(item);
if (!deleteResult.IsSuccess)
{
string errorMsg = $"Error deleting Export file {item.Id}";
userMessage.ShowResultWithMessage(deleteResult, errorMsg);
return;
}
userMessage.ShowMessage($"Export file {item.Id} deleted!", MessageKind.Success);
await LoadDataAsync();
}
#endregion
}
@@ -0,0 +1,162 @@
@attribute [Route($"{GuestConsts.Page_StayExport_New}")]
@using FluentValidation;
@using Somnia.GuestLog.Models.DTOs;
@using Somnia.GuestLog.Enums.Models;
@using Somnia.GuestLog.Services;
@using Somnia.GuestLog.Validators;
@using Somnia.GuestLog.Services.StayExport;
@inject IUserMessageService userMessage
@inject IDialogService DialogService
@inject NavigationManager Navigation
@inject IValidator<StayExportParamsDTO> ExportParamsValidator;
@inject ILocalizerService Localizer;
@inject IStayExportService ExportService;
@inject IJSRuntime JS
<PageTitle>Somnia - Stay Export - New</PageTitle>
<MudStack Row="true" Class="my-4">
<MudText Typo="Typo.h4" GutterBottom="true">
@LanguageLocale.page_StayExportNew_pageTitle
</MudText>
<MudSpacer />
<MudButton Variant="Variant.Filled" Color="Color.Success" Size="Size.Small" StartIcon="@Icons.Material.Filled.Add"
OnClick="() => BtnCreateExport()" Disabled="@_isExported">
@LanguageLocale.general_btnCreate
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Default" Size="Size.Small"
StartIcon="@Icons.Material.Filled.ArrowBack" OnClick="() => BtnReturn()">
@LanguageLocale.general_btnReturn
</MudButton>
</MudStack>
<MudForm @ref="@_form" Model="@exportParams"
Validation="@((ExportParamsValidator as StayExportParamsDTOValidator)!.ValidateValue)" ReadOnly="@_isExported">
<MudPaper Class="pa-4 my-4" Elevation="3" Height="100%">
<MudStack Row style="margin-block: 0.2em;">
<MudSelect @bind-Value="exportFormat" Label="@LanguageLocale.dto_StayExportFile_ExportFormat" FullWidth
Clearable>
@foreach (StayExportFormat option in Enum.GetValues<StayExportFormat>())
{
<MudSelectItem Value="new Nullable<StayExportFormat>(option)">
@Localizer.GetEnumValue(option)
</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="exportParams.ExportType" For="@(() => exportParams.ExportType)"
Label="@LanguageLocale.dto_StayExportParams_ExportType" FullWidth Clearable>
@foreach (StayExportParamType option in Enum.GetValues<StayExportParamType>())
{
<MudSelectItem Value="new Nullable<StayExportParamType>(option)">
@Localizer.GetEnumValue(option)
</MudSelectItem>
}
</MudSelect>
</MudStack>
</MudPaper>
@if (_showPeriodFields)
{
<MudPaper Class="pa-4" Elevation="3" Height="100%">
<MudStack Row>
<MudDatePicker @bind-Date="exportParams.ExportPeriodStart" For="@(() => exportParams.ExportPeriodStart)"
Label="@LanguageLocale.dto_StayExportParams_ExportPeriodStart" Clearable OpenTo="OpenTo.Date" Editable
AutoClose="true" />
<MudDatePicker @bind-Date="exportParams.ExportPeriodEnd" For="@(() => exportParams.ExportPeriodEnd)"
Label="@LanguageLocale.dto_StayExportParams_ExportPeriodEnd" Clearable OpenTo="OpenTo.Date" Editable
AutoClose="true" />
</MudStack>
</MudPaper>
}
</MudForm>
@if (_isExported && exportedFile != null)
{
<MudPaper Class="pa-4 my-6" Elevation="3" Height="100%">
<MudStack>
<MudText Typo="Typo.h4">File Exported!</MudText>
<MudTextField Value="@Localizer.GetEnumValue(exportedFile.ExportResult)"
Label="@LanguageLocale.dto_StayExportFile_ExportResult" FullWidth ReadOnly="true" />
<MudTextField Value="@Localizer.GetEnumValue(exportedFile.ExportFormat)"
Label="@LanguageLocale.dto_StayExportFile_ExportFormat" FullWidth ReadOnly="true" />
<MudButton OnClick="BtnDownloadExportedFile">Download Exported File</MudButton>
</MudStack>
</MudPaper>
}
@code {
MudForm _form = null!;
StayExportFormat? exportFormat = null;
StayExportParamsDTO exportParams = new();
StayExportFileDTO? exportedFile = null;
bool _showPeriodFields => exportParams.ExportType == StayExportParamType.PeriodDateIn;
bool _isExported => exportedFile != null;
private void BtnReturn()
{
Navigation.NavigateTo(GuestConsts.Page_StayExport_List);
}
private async Task BtnCreateExport()
{
await _form.ValidateAsync();
if (!_form.IsValid || exportFormat == null)
{
userMessage.ShowMessage("Invalid Form!", MessageKind.Warning);
return;
}
var loadingDialog = await DialogService.ShowAsync<LoadingDialog>();
var exportResult = await ExportService.ExportStaysByParamsAsync(exportFormat.Value, exportParams);
exportedFile = exportResult.Value;
loadingDialog.Close();
if (!exportResult.IsSuccess)
{
userMessage.ShowResultWithMessage(exportResult, $"Error exporting stays");
return;
}
userMessage.ShowMessage(
$"Stay exported: {exportResult.ResultCode} with export result: {exportedFile?.ExportResult}",
MessageKind.Success);
}
private async Task BtnDownloadExportedFile()
{
Debug.Assert(exportedFile != null);
if (string.IsNullOrEmpty(exportedFile.ExportedPath))
{
userMessage.ShowMessage("Exported File can't be downloaded as no file was saved", MessageKind.Warning);
return;
}
using Stream dataStream = new MemoryStream();
Result loadResult = await ExportService.GetExportedFileAsync(exportedFile, dataStream);
if (!loadResult.IsSuccess)
{
userMessage.ShowResultWithMessage(loadResult, "Exported File can't be downloaded");
return;
}
dataStream.Seek(0, SeekOrigin.Begin);
using var streamRef = new DotNetStreamReference(stream: dataStream);
var fileName = Path.GetFileName(exportedFile.ExportedPath);
await JS.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef);
userMessage.ShowMessage("Exported File downloaded!", MessageKind.Success);
}
}
@@ -1 +1,3 @@
@using Somnia.GuestLog.Components.Controls
@using Somnia.GuestLog.Services.Entity;
@using System.Diagnostics;
@@ -1,5 +1,6 @@
@using System.Net.Http
@using System.Net.Http.Json
@using System.Linq;
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@@ -15,4 +16,4 @@
@using Somnia.GuestLog.Components.Forms
@using Somnia.GuestLog.Components.Controls
@using Somnia.GuestLog.Resources
@using System.Linq;
@using Somnia.GuestLog.Enums.UI
@@ -0,0 +1,50 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Serilog": {
"Using": [
"Serilog.Sinks.Console",
"Serilog.Sinks.File"
],
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
}
},
"Enrich": [
"FromLogContext",
"WithThreadId",
"WithProcessId"
],
"Properties": {
"ApplicationName": "Somnia-GuestLog"
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"restrictedToMinimumLevel": "Information"
}
},
{
"Name": "File",
"Args": {
// Logs data path
"path": "/SomniaGuestLog/logs/log_.log",
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{ProcessId}-{ThreadId}] [{Level}] {JobKey}{Message}{NewLine}{Exception}",
"rollingInterval": "Day",
"rollOnFileSizeLimit": true,
"fileSizeLimitBytes": 1073741824,
"retainedFileCountLimit": 30,
"restrictedToMinimumLevel": "Information"
}
}
]
}
}
@@ -0,0 +1,9 @@
namespace Somnia.GuestLog.Enums.Models;
public enum StayExportFileResult
{
OK = 0,
NoData = 1,
ExportError = 2,
UploadError = 3,
}
@@ -0,0 +1,7 @@
namespace Somnia.GuestLog.Enums.Models;
public enum StayExportFormat
{
GuestLogJson = 0,
SpainParteViajeros = 1,
}
@@ -0,0 +1,6 @@
namespace Somnia.GuestLog.Enums.Models;
public enum StayExportParamType
{
PeriodDateIn = 0,
}
@@ -0,0 +1,7 @@
namespace Somnia.GuestLog.Enums.Services;
public enum AddressQueryProvider
{
Nominatim,
GoogleApiV4
}
@@ -0,0 +1,15 @@
namespace Somnia.GuestLog.Enums.Services;
public enum ResultCode
{
//Common Codes
Ok,
UnknownError,
FailedValidation,
NotFound,
FileStorageError,
//Specific Codes
GuestInStay,
GuestCurrentlyInStay,
}
+6
View File
@@ -0,0 +1,6 @@
namespace Somnia.GuestLog.Enums.UI;
public enum FormMode
{
Create, ReadOnly, Edit
}
@@ -0,0 +1,6 @@
namespace Somnia.GuestLog.Enums.UI;
public enum MessageKind
{
Info, Warning, Error, Success, Normal
}
+18 -1
View File
@@ -6,10 +6,18 @@ public static class GuestConsts
public const string Page_GuestDocs_List = "/guest-documents";
public const string Page_GuestDocs_New = "/guest-documents/new";
public const string Page_GuestDocs_Detail = $"/guest-documents/d/{Page_GuestDocs_Detail_Param_DocId}";
public const string Page_GuestDocs_Detail_Param_DocId = "{docId}";
public const string Page_GuestDocs_Detail_Param_DocId = "{docId:long}";
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 Page_StayExport_List = "/stayExport";
public const string Page_StayExport_New = "/stayExport/new";
public const string QueryParam_ReturnUrl = "returnUrl";
public const string DataList_DocumentTypes = "DocumentType";
@@ -23,4 +31,13 @@ public static class GuestConsts
public const string DocumentType_DNI = "DNI";
public const string DocumentType_NIE = "NIE";
public const string DocumentType_PASSPORT = "PASSPORT";
public const string GenderOptions_MALE = "M";
public const string GenderOptions_FEMALE = "F";
public const string GenderOptions_OTHER = "O";
public const int GuestDocument_FirstVersion = 1;
public const string EmptyField = "-";
}
@@ -5,4 +5,8 @@ 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";
public const string DbType_jsonb = "jsonb";
public const string PK_NotApply = "N/A";
}
@@ -11,7 +11,13 @@ public class GuestDocumentEntityConfig : IEntityTypeConfiguration<GuestDocument>
builder.ToTable("GuestDocument");
//PK
builder.HasKey(e => new { e.DocumentType, e.DocumentNum });
builder.HasKey(e => new { e.Id, e.Version });
builder.Property(e => e.Id)
.UseIdentityByDefaultColumn();
builder.Property(e => e.Version)
.IsRequired();
builder.Property(e => e.DocumentType)
.IsRequired()
@@ -48,6 +54,15 @@ public class GuestDocumentEntityConfig : IEntityTypeConfiguration<GuestDocument>
builder.Property(e => e.EmailAddress)
.HasMaxLength(256);
builder.Property(e => e.CreateTime)
.IsRequired()
.HasColumnType(DbConsts.DbType_Timestamptz_3);
builder.Property(e => e.UpdateTime)
.IsRequired()
.HasColumnType(DbConsts.DbType_Timestamptz_3);
builder.Property(e => e.IsLastVersion)
.IsRequired();
var addressBuilder = builder.ComplexProperty(e => e.AddressDetails);
@@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Somnia.GuestLog.Models.Entities;
namespace Somnia.GuestLog.Infrastructure.EntityConfigs;
public class StayExportFileEntityConfig : IEntityTypeConfiguration<StayExportFile>
{
public void Configure(EntityTypeBuilder<StayExportFile> builder)
{
builder.ToTable("StayExportFile");
//PK
builder.HasKey(e => e.Id);
builder.Property(e => e.Id)
.IsRequired()
.UseIdentityByDefaultColumn();
builder.ComplexProperty(e => e.ExportParams, e =>
{
e.ToJson();
});
builder.Property(e => e.ExportedPath)
.HasMaxLength(256);
builder.Property(e => e.ExportDetails)
.HasColumnType(DbConsts.DbType_jsonb);
builder.Property(e => e.ExportTime)
.IsRequired()
.HasColumnType(DbConsts.DbType_Timestamptz);
}
}
@@ -13,11 +13,14 @@ public partial class GuestLogContext : DbContext
public DbSet<GuestDocument> GuestDocuments { get; set; }
public DbSet<StayEntry> StayEntries { get; set; }
public DbSet<StayExportFile> StayExportFiles { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration<GuestDocument>(new GuestDocumentEntityConfig());
modelBuilder.ApplyConfiguration<StayEntry>(new StayEntryEntityConfig());
modelBuilder.ApplyConfiguration<StayExportFile>(new StayExportFileEntityConfig());
OnModelCreatingPartial(modelBuilder);
base.OnModelCreating(modelBuilder);
@@ -0,0 +1,205 @@
// <auto-generated />
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("20260830201743_GuestDocument_ChangePKToIdAndVersion")]
partial class GuestDocument_ChangePKToIdAndVersion
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.7")
.HasAnnotation("Proxies:ChangeTracking", false)
.HasAnnotation("Proxies:CheckEquality", false)
.HasAnnotation("Proxies:LazyLoading", true)
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("GuestDocumentStayEntry", b =>
{
b.Property<long>("StayEntryId")
.HasColumnType("bigint");
b.Property<long>("GuestDocumentsId")
.HasColumnType("bigint");
b.Property<int>("GuestDocumentsVersion")
.HasColumnType("integer");
b.HasKey("StayEntryId", "GuestDocumentsId", "GuestDocumentsVersion");
b.HasIndex("GuestDocumentsId", "GuestDocumentsVersion");
b.ToTable("GuestDocumentStayEntry");
});
modelBuilder.Entity("Somnia.GuestLog.Models.Entities.GuestDocument", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<int>("Version")
.HasColumnType("integer");
b.Property<DateOnly>("BirthDate")
.HasColumnType("date");
b.Property<DateTime>("CreateTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("DocumentNum")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("DocumentType")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("EmailAddress")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Gender")
.HasMaxLength(1)
.HasColumnType("character varying(1)");
b.Property<DateOnly>("IssueDate")
.HasColumnType("date");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Nationality")
.IsRequired()
.HasMaxLength(3)
.HasColumnType("character varying(3)");
b.Property<string>("PhoneNumber")
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("SupportNum")
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<string>("Surname")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Surname2")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<DateTime>("UpdateTime")
.HasColumnType("timestamp with time zone");
b.ComplexProperty(typeof(Dictionary<string, object>), "AddressDetails", "Somnia.GuestLog.Models.Entities.GuestDocument.AddressDetails#GuestAddress", b1 =>
{
b1.Property<string>("Address")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b1.Property<string>("AddressCity")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b1.Property<string>("AddressCountry")
.IsRequired()
.HasMaxLength(3)
.HasColumnType("character varying(3)");
b1.Property<string>("AddressProvince")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b1.Property<string>("AddressState")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b1.Property<string>("ZipCode")
.HasMaxLength(12)
.HasColumnType("character varying(12)");
});
b.HasKey("Id", "Version");
b.ToTable("GuestDocument", (string)null);
});
modelBuilder.Entity("Somnia.GuestLog.Models.Entities.StayEntry", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreateTime")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("DateIn")
.HasColumnType("timestamp(0) with time zone");
b.Property<DateTime>("DateOut")
.HasColumnType("timestamp(0) with time zone");
b.Property<string>("PaymentMethod")
.IsRequired()
.HasMaxLength(5)
.HasColumnType("character varying(5)");
b.Property<int>("RoomCount")
.HasColumnType("integer");
b.Property<string>("RoomNumber")
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<DateTime>("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("GuestDocumentsId", "GuestDocumentsVersion")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,101 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Somnia.GuestLog.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class GuestDocument_ChangePKToIdAndVersion : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
//Data don't really matter right now. Just get it working good.
//This migration will destry all data, but it's fine.
migrationBuilder.DropTable(
name: "GuestDocumentStayEntry"
);
migrationBuilder.Sql("""DELETE FROM "StayEntry";""");
migrationBuilder.DropTable(
name: "GuestDocument"
);
migrationBuilder.CreateTable(
name: "GuestDocument",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Version = table.Column<int>(type: "integer", nullable: false),
DocumentType = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
DocumentNum = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
IssueDate = table.Column<DateOnly>(type: "date", nullable: false),
SupportNum = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: true),
Nationality = table.Column<string>(type: "character varying(3)", maxLength: 3, nullable: false),
Name = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
Surname = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
Surname2 = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
BirthDate = table.Column<DateOnly>(type: "date", nullable: false),
Gender = table.Column<string>(type: "character varying(1)", maxLength: 1, nullable: true),
PhoneNumber = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
EmailAddress = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
CreateTime = table.Column<DateTime>(type: "timestamp(3) with time zone", nullable: false),
UpdateTime = table.Column<DateTime>(type: "timestamp(3) with time zone", nullable: false),
IsLastVersion = table.Column<bool>(type: "boolean", nullable: false),
AddressDetails_Address = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
AddressDetails_AddressCity = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
AddressDetails_AddressCountry = table.Column<string>(type: "character varying(3)", maxLength: 3, nullable: true),
AddressDetails_AddressProvince = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
AddressDetails_AddressState = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
AddressDetails_ZipCode = table.Column<string>(type: "character varying(12)", maxLength: 12, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_GuestDocument", x => new { x.Id, x.Version });
}
);
//Existing index for "DocType" and "DocNum" can be useful, so let's keep it.
migrationBuilder.CreateTable(
name: "GuestDocumentStayEntry",
columns: table => new
{
StayEntryId = table.Column<long>(type: "bigint", nullable: false),
GuestDocumentsId = table.Column<long>(type: "bigint", nullable: false),
GuestDocumentsVersion = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_GuestDocumentStayEntry", x => new { x.StayEntryId, x.GuestDocumentsId, x.GuestDocumentsVersion });
table.ForeignKey(
name: "FK_GuestDocumentStayEntry_GuestDocument_GuestDocumentsId_Guest~",
columns: x => new { x.GuestDocumentsId, x.GuestDocumentsVersion },
principalTable: "GuestDocument",
principalColumns: new[] { "Id", "Version" },
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_GuestDocumentsId_GuestDocumentsVersi~",
table: "GuestDocumentStayEntry",
columns: new[] { "GuestDocumentsId", "GuestDocumentsVersion" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
throw new NotSupportedException("Rollback is not supported for this migration!");
}
}
}
@@ -0,0 +1,249 @@
// <auto-generated />
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("20260901161146_AddTable_StayExportFile")]
partial class AddTable_StayExportFile
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.7")
.HasAnnotation("Proxies:ChangeTracking", false)
.HasAnnotation("Proxies:CheckEquality", false)
.HasAnnotation("Proxies:LazyLoading", true)
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("GuestDocumentStayEntry", b =>
{
b.Property<long>("StayEntryId")
.HasColumnType("bigint");
b.Property<long>("GuestDocumentsId")
.HasColumnType("bigint");
b.Property<int>("GuestDocumentsVersion")
.HasColumnType("integer");
b.HasKey("StayEntryId", "GuestDocumentsId", "GuestDocumentsVersion");
b.HasIndex("GuestDocumentsId", "GuestDocumentsVersion");
b.ToTable("GuestDocumentStayEntry");
});
modelBuilder.Entity("Somnia.GuestLog.Models.Entities.GuestDocument", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<int>("Version")
.HasColumnType("integer");
b.Property<DateOnly>("BirthDate")
.HasColumnType("date");
b.Property<DateTime>("CreateTime")
.HasColumnType("timestamp(3) with time zone");
b.Property<string>("DocumentNum")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("DocumentType")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("EmailAddress")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Gender")
.HasMaxLength(1)
.HasColumnType("character varying(1)");
b.Property<bool>("IsLastVersion")
.HasColumnType("boolean");
b.Property<DateOnly>("IssueDate")
.HasColumnType("date");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Nationality")
.IsRequired()
.HasMaxLength(3)
.HasColumnType("character varying(3)");
b.Property<string>("PhoneNumber")
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("SupportNum")
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<string>("Surname")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Surname2")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<DateTime>("UpdateTime")
.HasColumnType("timestamp(3) with time zone");
b.ComplexProperty(typeof(Dictionary<string, object>), "AddressDetails", "Somnia.GuestLog.Models.Entities.GuestDocument.AddressDetails#GuestAddress", b1 =>
{
b1.Property<string>("Address")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b1.Property<string>("AddressCity")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b1.Property<string>("AddressCountry")
.IsRequired()
.HasMaxLength(3)
.HasColumnType("character varying(3)");
b1.Property<string>("AddressProvince")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b1.Property<string>("AddressState")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b1.Property<string>("ZipCode")
.HasMaxLength(12)
.HasColumnType("character varying(12)");
});
b.HasKey("Id", "Version");
b.ToTable("GuestDocument", (string)null);
});
modelBuilder.Entity("Somnia.GuestLog.Models.Entities.StayEntry", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreateTime")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("DateIn")
.HasColumnType("timestamp(0) with time zone");
b.Property<DateTime>("DateOut")
.HasColumnType("timestamp(0) with time zone");
b.Property<string>("PaymentMethod")
.IsRequired()
.HasMaxLength(5)
.HasColumnType("character varying(5)");
b.Property<int>("RoomCount")
.HasColumnType("integer");
b.Property<string>("RoomNumber")
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<DateTime>("UpdateTime")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("StayEntry", (string)null);
});
modelBuilder.Entity("Somnia.GuestLog.Models.Entities.StayExportFile", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<int>("ExportFormat")
.HasColumnType("integer");
b.Property<int>("ExportResult")
.HasColumnType("integer");
b.Property<DateTime>("ExportTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("ExportedPath")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.ComplexProperty(typeof(Dictionary<string, object>), "ExportParams", "Somnia.GuestLog.Models.Entities.StayExportFile.ExportParams#StayExportParams", b1 =>
{
b1.IsRequired();
b1.Property<DateTime?>("ExportPeriodEnd");
b1.Property<DateTime?>("ExportPeriodStart");
b1.Property<int>("ExportType");
b1
.ToJson("ExportParams")
.HasColumnType("jsonb");
});
b.HasKey("Id");
b.ToTable("StayExportFile", (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("GuestDocumentsId", "GuestDocumentsVersion")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,40 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Somnia.GuestLog.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddTable_StayExportFile : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "StayExportFile",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ExportFormat = table.Column<int>(type: "integer", nullable: false),
ExportResult = table.Column<int>(type: "integer", nullable: false),
ExportedPath = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
ExportTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
ExportParams = table.Column<string>(type: "jsonb", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_StayExportFile", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "StayExportFile");
}
}
}
@@ -0,0 +1,252 @@
// <auto-generated />
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("20260908172441_StayExportFile_AddColumn_ExportDetails")]
partial class StayExportFile_AddColumn_ExportDetails
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.7")
.HasAnnotation("Proxies:ChangeTracking", false)
.HasAnnotation("Proxies:CheckEquality", false)
.HasAnnotation("Proxies:LazyLoading", true)
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("GuestDocumentStayEntry", b =>
{
b.Property<long>("StayEntryId")
.HasColumnType("bigint");
b.Property<long>("GuestDocumentsId")
.HasColumnType("bigint");
b.Property<int>("GuestDocumentsVersion")
.HasColumnType("integer");
b.HasKey("StayEntryId", "GuestDocumentsId", "GuestDocumentsVersion");
b.HasIndex("GuestDocumentsId", "GuestDocumentsVersion");
b.ToTable("GuestDocumentStayEntry");
});
modelBuilder.Entity("Somnia.GuestLog.Models.Entities.GuestDocument", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<int>("Version")
.HasColumnType("integer");
b.Property<DateOnly>("BirthDate")
.HasColumnType("date");
b.Property<DateTime>("CreateTime")
.HasColumnType("timestamp(3) with time zone");
b.Property<string>("DocumentNum")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("DocumentType")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("EmailAddress")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Gender")
.HasMaxLength(1)
.HasColumnType("character varying(1)");
b.Property<bool>("IsLastVersion")
.HasColumnType("boolean");
b.Property<DateOnly>("IssueDate")
.HasColumnType("date");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Nationality")
.IsRequired()
.HasMaxLength(3)
.HasColumnType("character varying(3)");
b.Property<string>("PhoneNumber")
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("SupportNum")
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<string>("Surname")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Surname2")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<DateTime>("UpdateTime")
.HasColumnType("timestamp(3) with time zone");
b.ComplexProperty(typeof(Dictionary<string, object>), "AddressDetails", "Somnia.GuestLog.Models.Entities.GuestDocument.AddressDetails#GuestAddress", b1 =>
{
b1.Property<string>("Address")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b1.Property<string>("AddressCity")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b1.Property<string>("AddressCountry")
.IsRequired()
.HasMaxLength(3)
.HasColumnType("character varying(3)");
b1.Property<string>("AddressProvince")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b1.Property<string>("AddressState")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b1.Property<string>("ZipCode")
.HasMaxLength(12)
.HasColumnType("character varying(12)");
});
b.HasKey("Id", "Version");
b.ToTable("GuestDocument", (string)null);
});
modelBuilder.Entity("Somnia.GuestLog.Models.Entities.StayEntry", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreateTime")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("DateIn")
.HasColumnType("timestamp(0) with time zone");
b.Property<DateTime>("DateOut")
.HasColumnType("timestamp(0) with time zone");
b.Property<string>("PaymentMethod")
.IsRequired()
.HasMaxLength(5)
.HasColumnType("character varying(5)");
b.Property<int>("RoomCount")
.HasColumnType("integer");
b.Property<string>("RoomNumber")
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<DateTime>("UpdateTime")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("StayEntry", (string)null);
});
modelBuilder.Entity("Somnia.GuestLog.Models.Entities.StayExportFile", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("ExportDetails")
.HasColumnType("jsonb");
b.Property<int>("ExportFormat")
.HasColumnType("integer");
b.Property<int>("ExportResult")
.HasColumnType("integer");
b.Property<DateTime>("ExportTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("ExportedPath")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.ComplexProperty(typeof(Dictionary<string, object>), "ExportParams", "Somnia.GuestLog.Models.Entities.StayExportFile.ExportParams#StayExportParams", b1 =>
{
b1.IsRequired();
b1.Property<DateTime?>("ExportPeriodEnd");
b1.Property<DateTime?>("ExportPeriodStart");
b1.Property<int>("ExportType");
b1
.ToJson("ExportParams")
.HasColumnType("jsonb");
});
b.HasKey("Id");
b.ToTable("StayExportFile", (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("GuestDocumentsId", "GuestDocumentsVersion")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Somnia.GuestLog.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class StayExportFile_AddColumn_ExportDetails : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "ExportDetails",
table: "StayExportFile",
type: "jsonb",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ExportDetails",
table: "StayExportFile");
}
}
}
@@ -0,0 +1,252 @@
// <auto-generated />
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("20260910093310_GuestDocument_IssueDate_MakeNullable")]
partial class GuestDocument_IssueDate_MakeNullable
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.7")
.HasAnnotation("Proxies:ChangeTracking", false)
.HasAnnotation("Proxies:CheckEquality", false)
.HasAnnotation("Proxies:LazyLoading", true)
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("GuestDocumentStayEntry", b =>
{
b.Property<long>("StayEntryId")
.HasColumnType("bigint");
b.Property<long>("GuestDocumentsId")
.HasColumnType("bigint");
b.Property<int>("GuestDocumentsVersion")
.HasColumnType("integer");
b.HasKey("StayEntryId", "GuestDocumentsId", "GuestDocumentsVersion");
b.HasIndex("GuestDocumentsId", "GuestDocumentsVersion");
b.ToTable("GuestDocumentStayEntry");
});
modelBuilder.Entity("Somnia.GuestLog.Models.Entities.GuestDocument", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<int>("Version")
.HasColumnType("integer");
b.Property<DateOnly>("BirthDate")
.HasColumnType("date");
b.Property<DateTime>("CreateTime")
.HasColumnType("timestamp(3) with time zone");
b.Property<string>("DocumentNum")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("DocumentType")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("EmailAddress")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Gender")
.HasMaxLength(1)
.HasColumnType("character varying(1)");
b.Property<bool>("IsLastVersion")
.HasColumnType("boolean");
b.Property<DateOnly?>("IssueDate")
.HasColumnType("date");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Nationality")
.IsRequired()
.HasMaxLength(3)
.HasColumnType("character varying(3)");
b.Property<string>("PhoneNumber")
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("SupportNum")
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<string>("Surname")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Surname2")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<DateTime>("UpdateTime")
.HasColumnType("timestamp(3) with time zone");
b.ComplexProperty(typeof(Dictionary<string, object>), "AddressDetails", "Somnia.GuestLog.Models.Entities.GuestDocument.AddressDetails#GuestAddress", b1 =>
{
b1.Property<string>("Address")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b1.Property<string>("AddressCity")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b1.Property<string>("AddressCountry")
.IsRequired()
.HasMaxLength(3)
.HasColumnType("character varying(3)");
b1.Property<string>("AddressProvince")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b1.Property<string>("AddressState")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b1.Property<string>("ZipCode")
.HasMaxLength(12)
.HasColumnType("character varying(12)");
});
b.HasKey("Id", "Version");
b.ToTable("GuestDocument", (string)null);
});
modelBuilder.Entity("Somnia.GuestLog.Models.Entities.StayEntry", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreateTime")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("DateIn")
.HasColumnType("timestamp(0) with time zone");
b.Property<DateTime>("DateOut")
.HasColumnType("timestamp(0) with time zone");
b.Property<string>("PaymentMethod")
.IsRequired()
.HasMaxLength(5)
.HasColumnType("character varying(5)");
b.Property<int>("RoomCount")
.HasColumnType("integer");
b.Property<string>("RoomNumber")
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<DateTime>("UpdateTime")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("StayEntry", (string)null);
});
modelBuilder.Entity("Somnia.GuestLog.Models.Entities.StayExportFile", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("ExportDetails")
.HasColumnType("jsonb");
b.Property<int>("ExportFormat")
.HasColumnType("integer");
b.Property<int>("ExportResult")
.HasColumnType("integer");
b.Property<DateTime>("ExportTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("ExportedPath")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.ComplexProperty(typeof(Dictionary<string, object>), "ExportParams", "Somnia.GuestLog.Models.Entities.StayExportFile.ExportParams#StayExportParams", b1 =>
{
b1.IsRequired();
b1.Property<DateTime?>("ExportPeriodEnd");
b1.Property<DateTime?>("ExportPeriodStart");
b1.Property<int>("ExportType");
b1
.ToJson("ExportParams")
.HasColumnType("jsonb");
});
b.HasKey("Id");
b.ToTable("StayExportFile", (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("GuestDocumentsId", "GuestDocumentsVersion")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,42 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Somnia.GuestLog.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class GuestDocument_IssueDate_MakeNullable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<DateOnly>(
name: "IssueDate",
table: "GuestDocument",
type: "date",
nullable: true,
oldClrType: typeof(DateOnly),
oldType: "date");
//Make null any IssueDate column with "default" value:
migrationBuilder.Sql("""UPDATE "GuestDocument" SET "IssueDate" = NULL WHERE "IssueDate" = '1970-01-01';""");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
//Set value to all IssueDate null columns.
migrationBuilder.Sql("""UPDATE "GuestDocument" SET "IssueDate" = '1970-01-01' WHERE "IssueDate" IS NULL;""");
migrationBuilder.AlterColumn<DateOnly>(
name: "IssueDate",
table: "GuestDocument",
type: "date",
nullable: false,
oldClrType: typeof(DateOnly),
oldType: "date",
oldNullable: true);
}
}
}
@@ -19,6 +19,9 @@ namespace Somnia.GuestLog.Infrastructure.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.7")
.HasAnnotation("Proxies:ChangeTracking", false)
.HasAnnotation("Proxies:CheckEquality", false)
.HasAnnotation("Proxies:LazyLoading", true)
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -28,34 +31,45 @@ namespace Somnia.GuestLog.Infrastructure.Migrations
b.Property<long>("StayEntryId")
.HasColumnType("bigint");
b.Property<string>("GuestDocumentsDocumentType")
.HasColumnType("character varying(8)");
b.Property<long>("GuestDocumentsId")
.HasColumnType("bigint");
b.Property<string>("GuestDocumentsDocumentNum")
.HasColumnType("character varying(64)");
b.Property<int>("GuestDocumentsVersion")
.HasColumnType("integer");
b.HasKey("StayEntryId", "GuestDocumentsDocumentType", "GuestDocumentsDocumentNum");
b.HasKey("StayEntryId", "GuestDocumentsId", "GuestDocumentsVersion");
b.HasIndex("GuestDocumentsDocumentType", "GuestDocumentsDocumentNum");
b.HasIndex("GuestDocumentsId", "GuestDocumentsVersion");
b.ToTable("GuestDocumentStayEntry");
});
modelBuilder.Entity("Somnia.GuestLog.Models.Entities.GuestDocument", b =>
{
b.Property<string>("DocumentType")
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
b.Property<string>("DocumentNum")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<int>("Version")
.HasColumnType("integer");
b.Property<DateOnly>("BirthDate")
.HasColumnType("date");
b.Property<DateTime>("CreateTime")
.HasColumnType("timestamp with time zone");
.HasColumnType("timestamp(3) with time zone");
b.Property<string>("DocumentNum")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("DocumentType")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("EmailAddress")
.HasMaxLength(256)
@@ -65,7 +79,10 @@ namespace Somnia.GuestLog.Infrastructure.Migrations
.HasMaxLength(1)
.HasColumnType("character varying(1)");
b.Property<DateOnly>("IssueDate")
b.Property<bool>("IsLastVersion")
.HasColumnType("boolean");
b.Property<DateOnly?>("IssueDate")
.HasColumnType("date");
b.Property<string>("Name")
@@ -96,7 +113,7 @@ namespace Somnia.GuestLog.Infrastructure.Migrations
.HasColumnType("character varying(64)");
b.Property<DateTime>("UpdateTime")
.HasColumnType("timestamp with time zone");
.HasColumnType("timestamp(3) with time zone");
b.ComplexProperty(typeof(Dictionary<string, object>), "AddressDetails", "Somnia.GuestLog.Models.Entities.GuestDocument.AddressDetails#GuestAddress", b1 =>
{
@@ -126,7 +143,7 @@ namespace Somnia.GuestLog.Infrastructure.Migrations
.HasColumnType("character varying(12)");
});
b.HasKey("DocumentType", "DocumentNum");
b.HasKey("Id", "Version");
b.ToTable("GuestDocument", (string)null);
});
@@ -168,6 +185,50 @@ namespace Somnia.GuestLog.Infrastructure.Migrations
b.ToTable("StayEntry", (string)null);
});
modelBuilder.Entity("Somnia.GuestLog.Models.Entities.StayExportFile", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("ExportDetails")
.HasColumnType("jsonb");
b.Property<int>("ExportFormat")
.HasColumnType("integer");
b.Property<int>("ExportResult")
.HasColumnType("integer");
b.Property<DateTime>("ExportTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("ExportedPath")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.ComplexProperty(typeof(Dictionary<string, object>), "ExportParams", "Somnia.GuestLog.Models.Entities.StayExportFile.ExportParams#StayExportParams", b1 =>
{
b1.IsRequired();
b1.Property<DateTime?>("ExportPeriodEnd");
b1.Property<DateTime?>("ExportPeriodStart");
b1.Property<int>("ExportType");
b1
.ToJson("ExportParams")
.HasColumnType("jsonb");
});
b.HasKey("Id");
b.ToTable("StayExportFile", (string)null);
});
modelBuilder.Entity("GuestDocumentStayEntry", b =>
{
b.HasOne("Somnia.GuestLog.Models.Entities.StayEntry", null)
@@ -178,7 +239,7 @@ namespace Somnia.GuestLog.Infrastructure.Migrations
b.HasOne("Somnia.GuestLog.Models.Entities.GuestDocument", null)
.WithMany()
.HasForeignKey("GuestDocumentsDocumentType", "GuestDocumentsDocumentNum")
.HasForeignKey("GuestDocumentsId", "GuestDocumentsVersion")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
@@ -1,3 +1,5 @@
using Somnia.GuestLog.Enums.Services;
namespace Somnia.GuestLog.Models.Configuration;
public class AddressQueryConfig
@@ -5,18 +7,10 @@ public class AddressQueryConfig
public const string SectionName = "AddressQuery";
public bool EnableAddressQuery { get; set; }
public AddressQueryProviderEnum? Provider { get; set; }
public AddressQueryProvider? Provider { get; set; }
public string? ServiceURL { get; set; }
public string? ApiKey { get; set; }
public string? UserAgent { get; set; }
public string? ResultLang { get; set; }
public enum AddressQueryProviderEnum
{
Nominatim,
GoogleApiV4
}
}
@@ -0,0 +1,10 @@
namespace Somnia.GuestLog.Models.Configuration;
public class StayExportConfig
{
public const string SectionName = "StayExport";
public bool EnableStayExport { get; set; } = false;
public string? ExportStorageConnectionString { get; set; }
public bool UseSubdirectoryByFormat { get; set; }
}
@@ -0,0 +1,9 @@
namespace Somnia.GuestLog.Models.Configuration;
public class StayExportSpainParteViajerosConfig
{
public const string SectionName = "StayExportSpainParteViajeros";
public bool InternetAvailable { get; set; } = false;
public string? CodigoEstablecimiento { get; set; }
}
@@ -10,6 +10,8 @@ public class GuestDocFilterDTO
public string? PhoneNumber { get; set; }
public bool OnlyLastVersion { get; set; }
public bool IsEmpty => string.IsNullOrEmpty(DocumentType)
&& string.IsNullOrEmpty(DocumentNumber)
@@ -2,6 +2,9 @@ namespace Somnia.GuestLog.Models.DTOs;
public class GuestDocumentDTO
{
public long? Id { get; set; }
public int? Version { get; set; }
public string? DocumentType { get; set; }
public string? DocumentNum { get; set; }
@@ -33,5 +36,6 @@ public class GuestDocumentDTO
public DateTime? UpdateTime { get; set; }
public string FullKey => $"{DocumentType}-{DocumentNum}";
public string FullKey => $"{Id}-{Version}";
public string FullDocument => $"{DocumentType ?? "???"}-{DocumentNum ?? "???"}";
}
+44
View File
@@ -0,0 +1,44 @@
using Somnia.GuestLog.Enums.Services;
namespace Somnia.GuestLog.Models.DTOs;
public interface IResult
{
bool IsSuccess { get; }
ResultCode ResultCode { get; }
object[]? Context { get; }
}
public sealed record Result(
bool IsSuccess,
ResultCode ResultCode,
params object[]? Context
) : IResult
{
public static Result Ok() => new(IsSuccess: true, ResultCode: ResultCode.Ok, Context: null);
public static Result OkWithContext(params object[] context)
=> new(IsSuccess: true, ResultCode: ResultCode.Ok, Context: context);
public static Result Fail(ResultCode resultCode) => new(IsSuccess: false, resultCode, Context: null);
public static Result FailWithContext(ResultCode resultCode, params object[] context)
=> new(IsSuccess: false, resultCode, context);
public static Result FailUnknownError()
=> new(IsSuccess: false, ResultCode: ResultCode.UnknownError, Context: null);
public bool IsUnknownError => !IsSuccess && ResultCode == ResultCode.UnknownError;
}
public sealed record Result<T>(
bool IsSuccess,
T? Value,
ResultCode ResultCode,
params object[]? Context
) : IResult
{
public static Result<T> Ok(T value) => new(IsSuccess: true, value, ResultCode: ResultCode.Ok, Context: null);
public static Result<T> Fail(ResultCode resultCode) => new(IsSuccess: false, Value: default, resultCode, Context: null);
public static Result<T> FailWithContext(ResultCode resultCode, params object[] context)
=> new(IsSuccess: false, Value: default, resultCode, context);
public bool IsUnknownError => !IsSuccess && ResultCode == ResultCode.UnknownError;
}
@@ -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; }
}
@@ -0,0 +1,29 @@
using Somnia.GuestLog.Enums.Models;
namespace Somnia.GuestLog.Models.DTOs;
public class StayExportFileDTO
{
public long? Id { get; set; }
public StayExportFormat? ExportFormat { get; set; }
public StayExportParamsDTO? ExportParams { get; set; }
public StayExportFileResult? ExportResult { get; set; }
public string? ExportedPath { get; set; }
public string? ExportDetails { get; set; }
public DateTime? ExportTime { get; set; }
}
public class StayExportParamsDTO
{
public StayExportParamType? ExportType { get; set; }
public DateTime? ExportPeriodStart { get; set; }
public DateTime? ExportPeriodEnd { get; set; }
}
@@ -1,3 +1,5 @@
using Somnia.GuestLog.Infrastructure;
namespace Somnia.GuestLog.Models.Entities;
public class GuestAddress : BaseEntity
@@ -9,5 +11,5 @@ public class GuestAddress : BaseEntity
public required string AddressCountry { get; set; }
public string? ZipCode { get; set; }
public override string GetFullKey() => "N/A";
public override string GetFullKey() => DbConsts.PK_NotApply;
}
@@ -2,10 +2,13 @@ namespace Somnia.GuestLog.Models.Entities;
public class GuestDocument : BaseEntity
{
public long Id { get; set; }
public int Version { get; set; }
public required string DocumentType { get; set; }
public required string DocumentNum { get; set; }
public required DateOnly IssueDate { get; set; }
public required DateOnly? IssueDate { get; set; }
public string? SupportNum { get; set; }
public required string Nationality { get; set; }
@@ -26,6 +29,9 @@ public class GuestDocument : BaseEntity
public required DateTime CreateTime { get; set; }
public required DateTime UpdateTime { get; set; }
public required bool IsLastVersion { get; set; }
public override string GetFullKey() => $"{DocumentType}-{DocumentNum}";
public override string GetFullKey() => $"{Id}-{Version}";
public string FullDocument => $"{DocumentType}-{DocumentNum}";
}
@@ -0,0 +1,33 @@
using Somnia.GuestLog.Enums.Models;
namespace Somnia.GuestLog.Models.Entities;
public class StayExportFile : BaseEntity
{
public long Id { get; set; }
public required StayExportFormat ExportFormat { get; set; }
public required StayExportParams ExportParams { get; set; }
public required StayExportFileResult ExportResult { get; set; }
/// <summary>
/// Path to exported data.
/// Should be relative to configured Storage.
/// </summary>
public string? ExportedPath { get; set; }
/// <summary>
/// A JSON column to store details about the exported file.
/// This details can contain information about errors, which stays where included, etc.
/// </summary>
public string? ExportDetails { get; set; }
/// <summary>
/// Date/Time when export file was generated.
/// </summary>
public required DateTime ExportTime { get; set; }
public override string GetFullKey() => $"{Id}";
}
@@ -0,0 +1,19 @@
using Somnia.GuestLog.Enums.Models;
using Somnia.GuestLog.Infrastructure;
namespace Somnia.GuestLog.Models.Entities;
/// <summary>
/// This class store params used on StayExport.
/// Different params apply for different export types.
/// </summary>
public class StayExportParams : BaseEntity
{
public required StayExportParamType ExportType { get; set; }
public DateTime? ExportPeriodStart { get; set; }
public DateTime? ExportPeriodEnd { get; set; }
public override string GetFullKey() => DbConsts.PK_NotApply;
}
+32 -9
View File
@@ -10,9 +10,11 @@ internal static class EntityMapping
{
return new()
{
Id = entity.Id,
Version = entity.Version,
DocumentType = entity.DocumentType,
DocumentNum = entity.DocumentNum,
IssueDate = entity.IssueDate.ToDateTime(TimeOnly.MinValue),
IssueDate = entity.IssueDate?.ToDateTime(TimeOnly.MinValue) ?? null,
SupportNum = entity.SupportNum,
Nationality = entity.Nationality,
Name = entity.Name,
@@ -28,24 +30,45 @@ internal static class EntityMapping
AddressState = entity.AddressDetails?.AddressState ?? "",
AddressCountry = entity.AddressDetails?.AddressCountry! ?? "",
ZipCode = entity.AddressDetails?.ZipCode ?? "",
CreateTime = entity.CreateTime,
UpdateTime = entity.UpdateTime,
CreateTime = entity.CreateTime.ToLocalTime(),
UpdateTime = entity.UpdateTime.ToLocalTime(),
};
}
public static StayEntryDTO MapStayEntryEntityToDTO(StayEntry entity)
public static StayEntryDTO MapStayEntryEntityToDTO(StayEntry entity, bool includeGuests)
{
return new()
{
Id = entity.Id,
DateIn = entity.DateIn,
DateOut = entity.DateOut,
DateIn = entity.DateIn.ToLocalTime(),
DateOut = entity.DateOut.ToLocalTime(),
RoomCount = entity.RoomCount,
RoomNumber = entity.RoomNumber,
PaymentMethod = entity.PaymentMethod,
CreateTime = entity.CreateTime,
UpdateTime = entity.UpdateTime,
GuestDocuments = [.. entity.GuestDocuments.Select(e => MapGuestDocumentEntityToDTO(e))],
CreateTime = entity.CreateTime.ToLocalTime(),
UpdateTime = entity.UpdateTime.ToLocalTime(),
GuestDocuments = includeGuests ? [.. entity.GuestDocuments.Select(e => MapGuestDocumentEntityToDTO(e))] : null,
};
}
public static StayExportFileDTO MapStayExportFileEntityToDTO(StayExportFile entity)
{
StayExportParamsDTO exportParamsDto = new()
{
ExportType = entity.ExportParams.ExportType,
ExportPeriodStart = entity.ExportParams.ExportPeriodStart,
ExportPeriodEnd = entity.ExportParams.ExportPeriodEnd,
};
return new()
{
Id = entity.Id,
ExportFormat = entity.ExportFormat,
ExportParams = exportParamsDto,
ExportResult = entity.ExportResult,
ExportedPath = entity.ExportedPath,
ExportDetails = entity.ExportDetails,
ExportTime = entity.ExportTime.ToLocalTime(),
};
}
@@ -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;
}
}
+82 -3
View File
@@ -1,23 +1,72 @@
using FluentStorage;
using FluentValidation;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Localization;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using MudBlazor.Services;
using MudBlazor.Translations;
using Serilog;
using Serilog.Extensions.Logging;
using Somnia.GuestLog;
using Somnia.GuestLog.Components;
using Somnia.GuestLog.Infrastructure;
using Somnia.GuestLog.Models.Configuration;
using Somnia.GuestLog.Models.DTOs;
using Somnia.GuestLog.Services;
using Somnia.GuestLog.Services.AddressQuery;
using Somnia.GuestLog.Services.Entity;
using Somnia.GuestLog.Services.SpainCodigoMunicipio;
using Somnia.GuestLog.Services.StayExport;
using Somnia.GuestLog.Utils;
using Somnia.GuestLog.Validators;
var builder = WebApplication.CreateBuilder(args);
//Load config files.
builder.Configuration.AddJsonFile("Config/log_Config.json", optional: false, reloadOnChange: false);
builder.Configuration.AddEnvironmentVariables("SOMNIA_");
//Create and configure Logger (Serilog)
var serilog = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration)
.CreateLogger();
builder.Services.AddSerilog(serilog);
var logger = new SerilogLoggerFactory(serilog).CreateLogger<Program>();
logger.LogInformation("\n\n\n -------------- Starting {appName} {appVersion} --------------", AppSettings.AppName, AppSettings.AppVersion);
if (!builder.Environment.IsDevelopment())
{
string dataProtectionPath = builder.Configuration.GetValue<string>("DataProtectionDir", defaultValue: "/SomniaGuestLog/dataprotection");
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(dataProtectionPath))
.SetApplicationName("SomniaGuestLog");
}
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
});
var connectionStringGuestLog = builder.Configuration.GetConnectionString("DbSomniaGuestLog")
?? 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();
@@ -36,27 +85,43 @@ builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddLocalization();
builder.Services.AddMudTranslations();
builder.Services.AddHttpClient();
// Add Validation Services
builder.Services.AddSingleton<IValidator<GuestDocumentDTO>, GuestDocumentDTOValidator>();
builder.Services.AddSingleton<IValidator<GuestDocIdDTO>, GuestDocIdDTOValidator>();
builder.Services.AddSingleton<IValidator<StayEntryDTO>, StayEntryDTOValidator>();
builder.Services.AddSingleton<IValidator<StayExportParamsDTO>, StayExportParamsDTOValidator>();
// Add Entity Services
builder.Services.AddSingleton<IGuestDocumentEntityService, GuestDocumentEntityService>();
builder.Services.AddSingleton<IStayEntryEntityService, StayEntryEntityService>();
builder.Services.AddSingleton<IStayExportFileEntityService, StayExportFileEntityService>();
// Add Services
builder.Services.AddSingleton<IGuestDocumentService, GuestDocumentService>();
builder.Services.AddSingleton<IStayEntryService, StayEntryService>();
builder.Services.AddSingleton<IDataListService, StaticDataListService>();
builder.Services.AddScoped<IUserMessageService, UserMessageService>();
builder.Services.AddSingleton<ILocalizerService, LocalizerService>();
builder.Services.AddSingleton<IStayExportService, StayExportService>();
builder.Services.AddSingleton<ISpainCodigoMunicipioService, SpainCodigoMunicipioCatastroService>();
// Add Factories (and it's services)
builder.Services.AddFactoryDI<IAddressQueryFactory, ConfigFileAddressQueryFactory>();
builder.Services.AddFactoryDI<IStayExportFormatProviderFactory, StayExportFormatProviderFactory>();
// Add Options config
builder.Services.AddOptions<AddressQueryConfig>()
.Bind(builder.Configuration.GetSection(AddressQueryConfig.SectionName));
builder.Services.AddOptions<StayExportConfig>()
.Bind(builder.Configuration.GetSection(StayExportConfig.SectionName));
builder.Services.AddOptions<StayExportSpainParteViajerosConfig>()
.Bind(builder.Configuration.GetSection(StayExportSpainParteViajerosConfig.SectionName));
var app = builder.Build();
app.UseForwardedHeaders();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
@@ -88,4 +153,18 @@ app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
//Activate S3 storage.
AwsS3Storage.Use();
//Run database migrations
bool runMigrationsOnStartup = builder.Configuration.GetValue<bool>("RunDatabaseMigrationOnStartup", defaultValue: true);
if (runMigrationsOnStartup)
{
logger.LogInformation("Running database migrations...");
using var scope = app.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<GuestLogContext>();
await db.Database.MigrateAsync();
logger.LogInformation("Database migrations OK");
}
app.Run();
@@ -0,0 +1,397 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing"
mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework
object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"
msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0"
msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"
msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"
msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="dataList.CountryCode.ABW" xml:space="preserve"><value>Aruba</value></data>
<data name="dataList.CountryCode.AFG" xml:space="preserve"><value>Afganistán</value></data>
<data name="dataList.CountryCode.AGO" xml:space="preserve"><value>Angola</value></data>
<data name="dataList.CountryCode.AIA" xml:space="preserve"><value>Anguila</value></data>
<data name="dataList.CountryCode.ALA" xml:space="preserve"><value>Åland, (las) Islas</value></data>
<data name="dataList.CountryCode.ALB" xml:space="preserve"><value>Albania</value></data>
<data name="dataList.CountryCode.AND" xml:space="preserve"><value>Andorra</value></data>
<data name="dataList.CountryCode.ARE" xml:space="preserve"><value>Emiratos Árabes Unidos (los)</value></data>
<data name="dataList.CountryCode.ARG" xml:space="preserve"><value>Argentina</value></data>
<data name="dataList.CountryCode.ARM" xml:space="preserve"><value>Armenia</value></data>
<data name="dataList.CountryCode.ASM" xml:space="preserve"><value>Samoa Americana</value></data>
<data name="dataList.CountryCode.ATA" xml:space="preserve"><value>Antártida</value></data>
<data name="dataList.CountryCode.ATF" xml:space="preserve"><value>Tierras Australes Francesas (las)</value></data>
<data name="dataList.CountryCode.ATG" xml:space="preserve"><value>Antigua y Barbuda</value></data>
<data name="dataList.CountryCode.AUS" xml:space="preserve"><value>Australia</value></data>
<data name="dataList.CountryCode.AUT" xml:space="preserve"><value>Austria</value></data>
<data name="dataList.CountryCode.AZE" xml:space="preserve"><value>Azerbaiyán</value></data>
<data name="dataList.CountryCode.BDI" xml:space="preserve"><value>Burundi</value></data>
<data name="dataList.CountryCode.BEL" xml:space="preserve"><value>Bélgica</value></data>
<data name="dataList.CountryCode.BEN" xml:space="preserve"><value>Benín</value></data>
<data name="dataList.CountryCode.BES" xml:space="preserve"><value>Bonaire, San Eustaquio y Saba</value></data>
<data name="dataList.CountryCode.BFA" xml:space="preserve"><value>Burkina Faso</value></data>
<data name="dataList.CountryCode.BGD" xml:space="preserve"><value>Bangladés</value></data>
<data name="dataList.CountryCode.BGR" xml:space="preserve"><value>Bulgaria</value></data>
<data name="dataList.CountryCode.BHR" xml:space="preserve"><value>Baréin</value></data>
<data name="dataList.CountryCode.BHS" xml:space="preserve"><value>Bahamas (las)</value></data>
<data name="dataList.CountryCode.BIH" xml:space="preserve"><value>Bosnia y Herzegovina</value></data>
<data name="dataList.CountryCode.BLM" xml:space="preserve"><value>San Barthélemy</value></data>
<data name="dataList.CountryCode.BLR" xml:space="preserve"><value>Belarús</value></data>
<data name="dataList.CountryCode.BLZ" xml:space="preserve"><value>Belice</value></data>
<data name="dataList.CountryCode.BMU" xml:space="preserve"><value>Bermuda</value></data>
<data name="dataList.CountryCode.BOL" xml:space="preserve"><value>Bolivia (Estado Plurinacional de)</value></data>
<data name="dataList.CountryCode.BRA" xml:space="preserve"><value>Brasil</value></data>
<data name="dataList.CountryCode.BRB" xml:space="preserve"><value>Barbados</value></data>
<data name="dataList.CountryCode.BRN" xml:space="preserve"><value>Brunei Darussalam</value></data>
<data name="dataList.CountryCode.BTN" xml:space="preserve"><value>Bhután</value></data>
<data name="dataList.CountryCode.BVT" xml:space="preserve"><value>Bouvet, Isla</value></data>
<data name="dataList.CountryCode.BWA" xml:space="preserve"><value>Botsuana</value></data>
<data name="dataList.CountryCode.CAF" xml:space="preserve"><value>República Centroafricana (la)</value></data>
<data name="dataList.CountryCode.CAN" xml:space="preserve"><value>Canadá</value></data>
<data name="dataList.CountryCode.CCK" xml:space="preserve"><value>Cocos / Keeling, (las) Islas</value></data>
<data name="dataList.CountryCode.CHE" xml:space="preserve"><value>Suiza</value></data>
<data name="dataList.CountryCode.CHL" xml:space="preserve"><value>Chile</value></data>
<data name="dataList.CountryCode.CHN" xml:space="preserve"><value>China</value></data>
<data name="dataList.CountryCode.CIV" xml:space="preserve"><value>Côte d'Ivoire</value></data>
<data name="dataList.CountryCode.CMR" xml:space="preserve"><value>Camerún</value></data>
<data name="dataList.CountryCode.COD" xml:space="preserve"><value>Congo (la República Democrática del)</value></data>
<data name="dataList.CountryCode.COG" xml:space="preserve"><value>Congo (el)</value></data>
<data name="dataList.CountryCode.COK" xml:space="preserve"><value>Cook, (las) Islas</value></data>
<data name="dataList.CountryCode.COL" xml:space="preserve"><value>Colombia</value></data>
<data name="dataList.CountryCode.COM" xml:space="preserve"><value>Comoras (las)</value></data>
<data name="dataList.CountryCode.CPV" xml:space="preserve"><value>Cabo Verde</value></data>
<data name="dataList.CountryCode.CRI" xml:space="preserve"><value>Costa Rica</value></data>
<data name="dataList.CountryCode.CUB" xml:space="preserve"><value>Cuba</value></data>
<data name="dataList.CountryCode.CUW" xml:space="preserve"><value>Curazao</value></data>
<data name="dataList.CountryCode.CXR" xml:space="preserve"><value>Christmas, Isla</value></data>
<data name="dataList.CountryCode.CYM" xml:space="preserve"><value>Caimán, (las) Islas</value></data>
<data name="dataList.CountryCode.CYP" xml:space="preserve"><value>Chipre</value></data>
<data name="dataList.CountryCode.CZE" xml:space="preserve"><value>Chequia</value></data>
<data name="dataList.CountryCode.DEU" xml:space="preserve"><value>Alemania</value></data>
<data name="dataList.CountryCode.DJI" xml:space="preserve"><value>Yibuti</value></data>
<data name="dataList.CountryCode.DMA" xml:space="preserve"><value>Dominica</value></data>
<data name="dataList.CountryCode.DNK" xml:space="preserve"><value>Dinamarca</value></data>
<data name="dataList.CountryCode.DOM" xml:space="preserve"><value>Dominicana, (la) República</value></data>
<data name="dataList.CountryCode.DZA" xml:space="preserve"><value>Argelia</value></data>
<data name="dataList.CountryCode.ECU" xml:space="preserve"><value>Ecuador</value></data>
<data name="dataList.CountryCode.EGY" xml:space="preserve"><value>Egipto</value></data>
<data name="dataList.CountryCode.ERI" xml:space="preserve"><value>Eritrea</value></data>
<data name="dataList.CountryCode.ESH" xml:space="preserve"><value>Sáhara Occidental</value></data>
<data name="dataList.CountryCode.ESP" xml:space="preserve"><value>España</value></data>
<data name="dataList.CountryCode.EST" xml:space="preserve"><value>Estonia</value></data>
<data name="dataList.CountryCode.ETH" xml:space="preserve"><value>Etiopía</value></data>
<data name="dataList.CountryCode.FIN" xml:space="preserve"><value>Finlandia</value></data>
<data name="dataList.CountryCode.FJI" xml:space="preserve"><value>Fiyi</value></data>
<data name="dataList.CountryCode.FLK" xml:space="preserve"><value>Malvinas [Falkland], (las) Islas</value></data>
<data name="dataList.CountryCode.FRA" xml:space="preserve"><value>Francia</value></data>
<data name="dataList.CountryCode.FRO" xml:space="preserve"><value>Feroe, (las) Islas</value></data>
<data name="dataList.CountryCode.FSM" xml:space="preserve"><value>Micronesia (Estados Federados de)</value></data>
<data name="dataList.CountryCode.GAB" xml:space="preserve"><value>Gabón</value></data>
<data name="dataList.CountryCode.GBR" xml:space="preserve"><value>Reino Unido de Gran Bretaña e Irlanda del Norte</value></data>
<data name="dataList.CountryCode.GEO" xml:space="preserve"><value>Georgia</value></data>
<data name="dataList.CountryCode.GGY" xml:space="preserve"><value>Guernsey</value></data>
<data name="dataList.CountryCode.GHA" xml:space="preserve"><value>Ghana</value></data>
<data name="dataList.CountryCode.GIB" xml:space="preserve"><value>Gibraltar</value></data>
<data name="dataList.CountryCode.GIN" xml:space="preserve"><value>Guinea</value></data>
<data name="dataList.CountryCode.GLP" xml:space="preserve"><value>Guadalupe</value></data>
<data name="dataList.CountryCode.GMB" xml:space="preserve"><value>Gambia (la)</value></data>
<data name="dataList.CountryCode.GNB" xml:space="preserve"><value>Guinea-Bissau</value></data>
<data name="dataList.CountryCode.GNQ" xml:space="preserve"><value>Guinea Ecuatorial</value></data>
<data name="dataList.CountryCode.GRC" xml:space="preserve"><value>Grecia</value></data>
<data name="dataList.CountryCode.GRD" xml:space="preserve"><value>Granada</value></data>
<data name="dataList.CountryCode.GRL" xml:space="preserve"><value>Groenlandia</value></data>
<data name="dataList.CountryCode.GTM" xml:space="preserve"><value>Guatemala</value></data>
<data name="dataList.CountryCode.GUF" xml:space="preserve"><value>Guayana Francesa</value></data>
<data name="dataList.CountryCode.GUM" xml:space="preserve"><value>Guam</value></data>
<data name="dataList.CountryCode.GUY" xml:space="preserve"><value>Guyana</value></data>
<data name="dataList.CountryCode.HKG" xml:space="preserve"><value>Hong Kong</value></data>
<data name="dataList.CountryCode.HMD" xml:space="preserve"><value>Heard (Isla) e Islas McDonald</value></data>
<data name="dataList.CountryCode.HND" xml:space="preserve"><value>Honduras</value></data>
<data name="dataList.CountryCode.HRV" xml:space="preserve"><value>Croacia</value></data>
<data name="dataList.CountryCode.HTI" xml:space="preserve"><value>Haití</value></data>
<data name="dataList.CountryCode.HUN" xml:space="preserve"><value>Hungría</value></data>
<data name="dataList.CountryCode.IDN" xml:space="preserve"><value>Indonesia</value></data>
<data name="dataList.CountryCode.IMN" xml:space="preserve"><value>Isla de Man</value></data>
<data name="dataList.CountryCode.IND" xml:space="preserve"><value>India</value></data>
<data name="dataList.CountryCode.IOT" xml:space="preserve"><value>Territorio Británico del Océano Índico (el)</value></data>
<data name="dataList.CountryCode.IRL" xml:space="preserve"><value>Irlanda</value></data>
<data name="dataList.CountryCode.IRN" xml:space="preserve"><value>Irán (República Islámica de)</value></data>
<data name="dataList.CountryCode.IRQ" xml:space="preserve"><value>Irak</value></data>
<data name="dataList.CountryCode.ISL" xml:space="preserve"><value>Islandia</value></data>
<data name="dataList.CountryCode.ISR" xml:space="preserve"><value>Israel</value></data>
<data name="dataList.CountryCode.ITA" xml:space="preserve"><value>Italia</value></data>
<data name="dataList.CountryCode.JAM" xml:space="preserve"><value>Jamaica</value></data>
<data name="dataList.CountryCode.JEY" xml:space="preserve"><value>Jersey</value></data>
<data name="dataList.CountryCode.JOR" xml:space="preserve"><value>Jordania</value></data>
<data name="dataList.CountryCode.JPN" xml:space="preserve"><value>Japón</value></data>
<data name="dataList.CountryCode.KAZ" xml:space="preserve"><value>Kazajistán</value></data>
<data name="dataList.CountryCode.KEN" xml:space="preserve"><value>Kenia</value></data>
<data name="dataList.CountryCode.KGZ" xml:space="preserve"><value>Kirguistán</value></data>
<data name="dataList.CountryCode.KHM" xml:space="preserve"><value>Camboya</value></data>
<data name="dataList.CountryCode.KIR" xml:space="preserve"><value>Kiribati</value></data>
<data name="dataList.CountryCode.KNA" xml:space="preserve"><value>Saint Kitts y Nevis</value></data>
<data name="dataList.CountryCode.KOR" xml:space="preserve"><value>Corea (la República de)</value></data>
<data name="dataList.CountryCode.KWT" xml:space="preserve"><value>Kuwait</value></data>
<data name="dataList.CountryCode.LAO" xml:space="preserve"><value>Lao, (la) República Democrática Popular</value></data>
<data name="dataList.CountryCode.LBN" xml:space="preserve"><value>Líbano</value></data>
<data name="dataList.CountryCode.LBR" xml:space="preserve"><value>Liberia</value></data>
<data name="dataList.CountryCode.LBY" xml:space="preserve"><value>Libia</value></data>
<data name="dataList.CountryCode.LCA" xml:space="preserve"><value>Santa Lucía</value></data>
<data name="dataList.CountryCode.LIE" xml:space="preserve"><value>Liechtenstein</value></data>
<data name="dataList.CountryCode.LKA" xml:space="preserve"><value>Sri Lanka</value></data>
<data name="dataList.CountryCode.LSO" xml:space="preserve"><value>Lesoto</value></data>
<data name="dataList.CountryCode.LTU" xml:space="preserve"><value>Lituania</value></data>
<data name="dataList.CountryCode.LUX" xml:space="preserve"><value>Luxemburgo</value></data>
<data name="dataList.CountryCode.LVA" xml:space="preserve"><value>Letonia</value></data>
<data name="dataList.CountryCode.MAC" xml:space="preserve"><value>Macao</value></data>
<data name="dataList.CountryCode.MAF" xml:space="preserve"><value>San Martín (parte francesa)</value></data>
<data name="dataList.CountryCode.MAR" xml:space="preserve"><value>Marruecos</value></data>
<data name="dataList.CountryCode.MCO" xml:space="preserve"><value>Mónaco</value></data>
<data name="dataList.CountryCode.MDA" xml:space="preserve"><value>Moldova (la República de)</value></data>
<data name="dataList.CountryCode.MDG" xml:space="preserve"><value>Madagascar</value></data>
<data name="dataList.CountryCode.MDV" xml:space="preserve"><value>Maldivas</value></data>
<data name="dataList.CountryCode.MEX" xml:space="preserve"><value>México</value></data>
<data name="dataList.CountryCode.MHL" xml:space="preserve"><value>Marshall, (las) Islas</value></data>
<data name="dataList.CountryCode.MKD" xml:space="preserve"><value>Macedonia del Norte</value></data>
<data name="dataList.CountryCode.MLI" xml:space="preserve"><value>Malí</value></data>
<data name="dataList.CountryCode.MLT" xml:space="preserve"><value>Malta</value></data>
<data name="dataList.CountryCode.MMR" xml:space="preserve"><value>Myanmar</value></data>
<data name="dataList.CountryCode.MNE" xml:space="preserve"><value>Montenegro</value></data>
<data name="dataList.CountryCode.MNG" xml:space="preserve"><value>Mongolia</value></data>
<data name="dataList.CountryCode.MNP" xml:space="preserve"><value>Marianas Septentrionales, (las) Islas</value></data>
<data name="dataList.CountryCode.MOZ" xml:space="preserve"><value>Mozambique</value></data>
<data name="dataList.CountryCode.MRT" xml:space="preserve"><value>Mauritania</value></data>
<data name="dataList.CountryCode.MSR" xml:space="preserve"><value>Montserrat</value></data>
<data name="dataList.CountryCode.MTQ" xml:space="preserve"><value>Martinica</value></data>
<data name="dataList.CountryCode.MUS" xml:space="preserve"><value>Mauricio</value></data>
<data name="dataList.CountryCode.MWI" xml:space="preserve"><value>Malaui</value></data>
<data name="dataList.CountryCode.MYS" xml:space="preserve"><value>Malasia</value></data>
<data name="dataList.CountryCode.MYT" xml:space="preserve"><value>Mayotte</value></data>
<data name="dataList.CountryCode.NAM" xml:space="preserve"><value>Namibia</value></data>
<data name="dataList.CountryCode.NCL" xml:space="preserve"><value>Nueva Caledonia</value></data>
<data name="dataList.CountryCode.NER" xml:space="preserve"><value>Níger (el)</value></data>
<data name="dataList.CountryCode.NFK" xml:space="preserve"><value>Norfolk, Isla</value></data>
<data name="dataList.CountryCode.NGA" xml:space="preserve"><value>Nigeria</value></data>
<data name="dataList.CountryCode.NIC" xml:space="preserve"><value>Nicaragua</value></data>
<data name="dataList.CountryCode.NIU" xml:space="preserve"><value>Niue</value></data>
<data name="dataList.CountryCode.NLD" xml:space="preserve"><value>Países Bajos (los)</value></data>
<data name="dataList.CountryCode.NOR" xml:space="preserve"><value>Noruega</value></data>
<data name="dataList.CountryCode.NPL" xml:space="preserve"><value>Nepal</value></data>
<data name="dataList.CountryCode.NRU" xml:space="preserve"><value>Nauru</value></data>
<data name="dataList.CountryCode.NZL" xml:space="preserve"><value>Nueva Zelandia</value></data>
<data name="dataList.CountryCode.OMN" xml:space="preserve"><value>Omán</value></data>
<data name="dataList.CountryCode.PAK" xml:space="preserve"><value>Pakistán</value></data>
<data name="dataList.CountryCode.PAN" xml:space="preserve"><value>Panamá</value></data>
<data name="dataList.CountryCode.PCN" xml:space="preserve"><value>Pitcairn, (las) Islas</value></data>
<data name="dataList.CountryCode.PER" xml:space="preserve"><value>Perú</value></data>
<data name="dataList.CountryCode.PHL" xml:space="preserve"><value>Filipinas (las)</value></data>
<data name="dataList.CountryCode.PLW" xml:space="preserve"><value>Palau</value></data>
<data name="dataList.CountryCode.PNG" xml:space="preserve"><value>Papúa Nueva Guinea</value></data>
<data name="dataList.CountryCode.POL" xml:space="preserve"><value>Polonia</value></data>
<data name="dataList.CountryCode.PRI" xml:space="preserve"><value>Puerto Rico</value></data>
<data name="dataList.CountryCode.PRK" xml:space="preserve"><value>Corea (la República Popular Democrática de)</value></data>
<data name="dataList.CountryCode.PRT" xml:space="preserve"><value>Portugal</value></data>
<data name="dataList.CountryCode.PRY" xml:space="preserve"><value>Paraguay</value></data>
<data name="dataList.CountryCode.PSE" xml:space="preserve"><value>Palestina, Estado de</value></data>
<data name="dataList.CountryCode.PYF" xml:space="preserve"><value>Polinesia Francesa</value></data>
<data name="dataList.CountryCode.QAT" xml:space="preserve"><value>Catar</value></data>
<data name="dataList.CountryCode.REU" xml:space="preserve"><value>Reunión</value></data>
<data name="dataList.CountryCode.ROU" xml:space="preserve"><value>Rumania</value></data>
<data name="dataList.CountryCode.RUS" xml:space="preserve"><value>Rusia, (la) Federación de</value></data>
<data name="dataList.CountryCode.RWA" xml:space="preserve"><value>Ruanda</value></data>
<data name="dataList.CountryCode.SAU" xml:space="preserve"><value>Arabia Saudita</value></data>
<data name="dataList.CountryCode.SDN" xml:space="preserve"><value>Sudán</value></data>
<data name="dataList.CountryCode.SEN" xml:space="preserve"><value>Senegal</value></data>
<data name="dataList.CountryCode.SGP" xml:space="preserve"><value>Singapur</value></data>
<data name="dataList.CountryCode.SGS" xml:space="preserve"><value>Georgia del Sur (la) y las Islas Sandwich del Sur</value></data>
<data name="dataList.CountryCode.SHN" xml:space="preserve"><value>Santa Elena, Ascensión y Tristán de Acuña</value></data>
<data name="dataList.CountryCode.SJM" xml:space="preserve"><value>Svalbard y Jan Mayen</value></data>
<data name="dataList.CountryCode.SLB" xml:space="preserve"><value>Salomón, (las) Islas</value></data>
<data name="dataList.CountryCode.SLE" xml:space="preserve"><value>Sierra Leona</value></data>
<data name="dataList.CountryCode.SLV" xml:space="preserve"><value>El Salvador</value></data>
<data name="dataList.CountryCode.SMR" xml:space="preserve"><value>San Marino</value></data>
<data name="dataList.CountryCode.SOM" xml:space="preserve"><value>Somalia</value></data>
<data name="dataList.CountryCode.SPM" xml:space="preserve"><value>San Pedro y Miquelón</value></data>
<data name="dataList.CountryCode.SRB" xml:space="preserve"><value>Serbia</value></data>
<data name="dataList.CountryCode.SSD" xml:space="preserve"><value>Sudán del Sur</value></data>
<data name="dataList.CountryCode.STP" xml:space="preserve"><value>Santo Tomé y Príncipe</value></data>
<data name="dataList.CountryCode.SUR" xml:space="preserve"><value>Suriname</value></data>
<data name="dataList.CountryCode.SVK" xml:space="preserve"><value>Eslovaquia</value></data>
<data name="dataList.CountryCode.SVN" xml:space="preserve"><value>Eslovenia</value></data>
<data name="dataList.CountryCode.SWE" xml:space="preserve"><value>Suecia</value></data>
<data name="dataList.CountryCode.SWZ" xml:space="preserve"><value>Eswatini</value></data>
<data name="dataList.CountryCode.SXM" xml:space="preserve"><value>San Martín (parte neerlandesa)</value></data>
<data name="dataList.CountryCode.SYC" xml:space="preserve"><value>Seychelles</value></data>
<data name="dataList.CountryCode.SYR" xml:space="preserve"><value>República Árabe Siria</value></data>
<data name="dataList.CountryCode.TCA" xml:space="preserve"><value>Turcas y Caicos, (las) Islas</value></data>
<data name="dataList.CountryCode.TCD" xml:space="preserve"><value>Chad</value></data>
<data name="dataList.CountryCode.TGO" xml:space="preserve"><value>Togo</value></data>
<data name="dataList.CountryCode.THA" xml:space="preserve"><value>Tailandia</value></data>
<data name="dataList.CountryCode.TJK" xml:space="preserve"><value>Tayikistán</value></data>
<data name="dataList.CountryCode.TKL" xml:space="preserve"><value>Tokelau</value></data>
<data name="dataList.CountryCode.TKM" xml:space="preserve"><value>Turkmenistán</value></data>
<data name="dataList.CountryCode.TLS" xml:space="preserve"><value>Timor-Leste</value></data>
<data name="dataList.CountryCode.TON" xml:space="preserve"><value>Tonga</value></data>
<data name="dataList.CountryCode.TTO" xml:space="preserve"><value>Trinidad y Tabago</value></data>
<data name="dataList.CountryCode.TUN" xml:space="preserve"><value>Túnez</value></data>
<data name="dataList.CountryCode.TUR" xml:space="preserve"><value>Turquía</value></data>
<data name="dataList.CountryCode.TUV" xml:space="preserve"><value>Tuvalu</value></data>
<data name="dataList.CountryCode.TWN" xml:space="preserve"><value>Taiwán (Provincia de China)</value></data>
<data name="dataList.CountryCode.TZA" xml:space="preserve"><value>Tanzania, República Unida de</value></data>
<data name="dataList.CountryCode.UGA" xml:space="preserve"><value>Uganda</value></data>
<data name="dataList.CountryCode.UKR" xml:space="preserve"><value>Ucrania</value></data>
<data name="dataList.CountryCode.UMI" xml:space="preserve"><value>Islas menores alejadas de Estados Unidos (las)</value></data>
<data name="dataList.CountryCode.URY" xml:space="preserve"><value>Uruguay</value></data>
<data name="dataList.CountryCode.USA" xml:space="preserve"><value>Estados Unidos de América (los)</value></data>
<data name="dataList.CountryCode.UZB" xml:space="preserve"><value>Uzbekistán</value></data>
<data name="dataList.CountryCode.VAT" xml:space="preserve"><value>Santa Sede (la)</value></data>
<data name="dataList.CountryCode.VCT" xml:space="preserve"><value>San Vincente y las Granadinas</value></data>
<data name="dataList.CountryCode.VEN" xml:space="preserve"><value>Venezuela (República Bolivariana de)</value></data>
<data name="dataList.CountryCode.VGB" xml:space="preserve"><value>Vírgenes Británicas, (las) Islas</value></data>
<data name="dataList.CountryCode.VIR" xml:space="preserve"><value>Vírgenes de los Estados Unidos, (las) Islas</value></data>
<data name="dataList.CountryCode.VNM" xml:space="preserve"><value>Viet Nam</value></data>
<data name="dataList.CountryCode.VUT" xml:space="preserve"><value>Vanuatu</value></data>
<data name="dataList.CountryCode.WLF" xml:space="preserve"><value>Wallis y Futuna</value></data>
<data name="dataList.CountryCode.WSM" xml:space="preserve"><value>Samoa</value></data>
<data name="dataList.CountryCode.YEM" xml:space="preserve"><value>Yemen</value></data>
<data name="dataList.CountryCode.ZAF" xml:space="preserve"><value>Sudáfrica</value></data>
<data name="dataList.CountryCode.ZMB" xml:space="preserve"><value>Zambia</value></data>
<data name="dataList.CountryCode.ZWE" xml:space="preserve"><value>Zimbabue</value></data>
<data name="dataList.DocumentType.DNI" xml:space="preserve"><value>DNI</value></data>
<data name="dataList.DocumentType.NIE" xml:space="preserve"><value>NIE</value></data>
<data name="dataList.DocumentType.PASSPORT" xml:space="preserve"><value>Pasaporte</value></data>
<data name="dataList.DocumentType.ID" xml:space="preserve"><value>ID Card</value></data>
<data name="dataList.DocumentType.OTHER" xml:space="preserve"><value>Otro</value></data>
<data name="dataList.GenderOption.M" xml:space="preserve"><value>Hombre</value></data>
<data name="dataList.GenderOption.F" xml:space="preserve"><value>Mujer</value></data>
<data name="dataList.GenderOption.O" xml:space="preserve"><value>Otro</value></data>
<data name="dataList.PaymentMethod.EFECT" xml:space="preserve"><value>Efectivo</value></data>
<data name="dataList.PaymentMethod.TARJT" xml:space="preserve"><value>Tarjeta</value></data>
<data name="dataList.PaymentMethod.TRANS" xml:space="preserve"><value>Transferencia</value></data>
<data name="dataList.PaymentMethod.MOVIL" xml:space="preserve"><value>Pago por Móvil</value></data>
<data name="dataList.PaymentMethod.OTRO" xml:space="preserve"><value>Otro</value></data>
</root>
@@ -123,25 +123,147 @@
Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="dto.GuestDocument.Id" xml:space="preserve"><value>ID</value></data>
<data name="dto.GuestDocument.Version" xml:space="preserve"><value>Versión</value></data>
<data name="dto.GuestDocument.DocumentType" xml:space="preserve"><value>Tipo Documento</value></data>
<data name="dto.GuestDocument.DocumentNum" xml:space="preserve"><value>Número Documento</value></data>
<data name="dto.GuestDocument.IssueDate" xml:space="preserve"><value>Fecha emisión</value></data>
<data name="dto.GuestDocument.IssueDate" xml:space="preserve"><value>Fecha Emisión</value></data>
<data name="dto.GuestDocument.SupportNum" xml:space="preserve"><value>Número Soporte</value></data>
<data name="dto.GuestDocument.Nationality" xml:space="preserve"><value>Nacionalidad</value></data>
<data name="dto.GuestDocument.Name" xml:space="preserve"><value>Nombre</value></data>
<data name="dto.GuestDocument.Surname" xml:space="preserve"><value>Apellido 1</value></data>
<data name="dto.GuestDocument.Surname" xml:space="preserve"><value>Apellido</value></data>
<data name="dto.GuestDocument.Surname2" xml:space="preserve"><value>Apellido 2</value></data>
<data name="dto.GuestDocument.BirthDate" xml:space="preserve"><value>Fecha Nacimiento</value></data>
<data name="dto.GuestDocument.Gender" xml:space="preserve"><value>Sexo</value></data>
<data name="dto.GuestDocument.PhoneNumber" xml:space="preserve"><value>Número Teléfono</value></data>
<data name="dto.GuestDocument.EmailAddress" xml:space="preserve"><value>Dirección Email</value></data>
<data name="dto.GuestDocument.Address" xml:space="preserve"><value>Dirección</value></data>
<data name="dto.GuestDocument.AddressCity" xml:space="preserve"><value>Ciudad</value></data>
<data name="dto.GuestDocument.AddressProvince" xml:space="preserve"><value>Provincia</value></data>
<data name="dto.GuestDocument.AddressState" xml:space="preserve"><value>Región</value></data>
<data name="dto.GuestDocument.AddressCountry" xml:space="preserve"><value>País</value></data>
<data name="dto.GuestDocument.ZipCode" xml:space="preserve"><value>Código Postal</value></data>
<data name="dto.GuestDocument.EmailAddress" xml:space="preserve"><value>Correo Electrónico</value></data>
<data name="dto.GuestDocument.CreateTime" xml:space="preserve"><value>Fecha Creación</value></data>
<data name="dto.GuestDocument.UpdateTime" xml:space="preserve"><value>Fecha Modificación</value></data>
<data name="dto.StayEntry.Id" xml:space="preserve"><value>Id</value></data>
<data name="dto.StayEntry.DateIn" xml:space="preserve"><value>Fecha Entrada</value></data>
<data name="dto.StayEntry.DateOut" xml:space="preserve"><value>Fecha Salida</value></data>
<data name="dto.StayEntry.RoomCount" xml:space="preserve"><value>Cantidad Habitaciones</value></data>
<data name="dto.StayEntry.RoomNumber" xml:space="preserve"><value>Número Habitaciones</value></data>
<data name="dto.StayEntry.PaymentMethod" xml:space="preserve"><value>Forma de Pago</value></data>
<data name="dto.StayEntry.GuestDocuments" xml:space="preserve"><value>Huéspedes</value></data>
<data name="dto.StayEntry.CreateTime" xml:space="preserve"><value>Fecha Creación</value></data>
<data name="dto.StayEntry.UpdateTime" xml:space="preserve"><value>Fecha Modificación</value></data>
<data name="dto.StayExportFile.Id" xml:space="preserve"><value>Id</value></data>
<data name="dto.StayExportFile.ExportFormat" xml:space="preserve"><value>Formato Exportación</value></data>
<data name="dto.StayExportFile.ExportParams" xml:space="preserve"><value>Parámetros Exportación</value></data>
<data name="dto.StayExportFile.ExportResult" xml:space="preserve"><value>Resultado Exportación</value></data>
<data name="dto.StayExportFile.ExportedPath" xml:space="preserve"><value>Ruta Exportación</value></data>
<data name="dto.StayExportFile.ExportDetails" xml:space="preserve"><value>Detalles Exportación</value></data>
<data name="dto.StayExportFile.ExportTime" xml:space="preserve"><value>Fecha Exportación</value></data>
<data name="dto.StayExportParams.ExportType" xml:space="preserve"><value>Tipo Exportación</value></data>
<data name="dto.StayExportParams.ExportPeriodStart" xml:space="preserve"><value>Fecha Desde</value></data>
<data name="dto.StayExportParams.ExportPeriodEnd" xml:space="preserve"><value>Fecha Hasta</value></data>
<data name="tmp" xml:space="preserve"><value>TMP</value></data>
<data name="general.actions" xml:space="preserve"><value>Acciones</value></data>
<data name="general.reloadData" xml:space="preserve"><value>Recargar Datos</value></data>
<data name="general.inProgressMsg" xml:space="preserve"><value>En Progreso...</value></data>
<data name="general.ConfigureColumns" xml:space="preserve"><value>Configurar Columnas</value></data>
<data name="general.confirmDeleteMsg" xml:space="preserve"><value>¡Eliminar no se puede deshacer!</value></data>
<data name="general.confirmTitle" xml:space="preserve"><value>¿Confirmar?</value></data>
<data name="general.btnDelete" xml:space="preserve"><value>Eliminar</value></data>
<data name="general.btnCreate" xml:space="preserve"><value>Crear</value></data>
<data name="general.btnEdit" xml:space="preserve"><value>Editar</value></data>
<data name="general.btnReturn" xml:space="preserve"><value>Volver</value></data>
<data name="general.btnClose" xml:space="preserve"><value>Cerrar</value></data>
<data name="general.btnSearch" xml:space="preserve"><value>Buscar</value></data>
<data name="general.btnSave" xml:space="preserve"><value>Guaradar</value></data>
<data name="general.btnCancel" xml:space="preserve"><value>Cancelar</value></data>
<data name="general.noResults" xml:space="preserve"><value>No hay resultados</value></data>
<data name="general.showAllElements" xml:space="preserve"><value>Mostrar todos los elementos</value></data>
<data name="general.wildcardHelperText" xml:space="preserve"><value>Usa '%' como comodín</value></data>
<data name="general.dateStart" xml:space="preserve"><value>Fecha Inicio</value></data>
<data name="general.dateEnd" xml:space="preserve"><value>Fecha Fin</value></data>
<data name="general.loading" xml:space="preserve"><value>Cargando...</value></data>
<data name="general.msgEditSaved" xml:space="preserve"><value>Cambios Guardados...</value></data>
<data name="general.msgEditCanceled" xml:space="preserve"><value>¡Edición cancelada!</value></data>
<data name="general.languageSelector" xml:space="preserve"><value>Idioma</value></data>
<data name="general.address.address" xml:space="preserve"><value>Dirección</value></data>
<data name="general.address.city" xml:space="preserve"><value>Ciudad</value></data>
<data name="general.address.province" xml:space="preserve"><value>Provincia</value></data>
<data name="general.address.state" xml:space="preserve"><value>Región</value></data>
<data name="general.address.country" xml:space="preserve"><value>País</value></data>
<data name="general.address.zipCode" xml:space="preserve"><value>Código Postal</value></data>
<!-- Common codes -->
<data name="enum.ResultCode.Ok" xml:space="preserve"><value>OK</value></data>
<data name="enum.ResultCode.UnknownError" xml:space="preserve"><value>Error desconocido</value></data>
<data name="enum.ResultCode.FailedValidation" xml:space="preserve"><value>Fallo de validación de datos</value></data>
<data name="enum.ResultCode.NotFound" xml:space="preserve"><value>¡No encontrado!</value></data>
<data name="enum.ResultCode.FileStorageError" xml:space="preserve"><value>Error de almacenamiento de ficheros</value></data>
<!-- Specific codes -->
<data name="enum.ResultCode.GuestInStay" xml:space="preserve"><value>Huésped (ID: '{0}') está en una Estancia.</value></data>
<data name="enum.ResultCode.GuestCurrentlyInStay" xml:space="preserve"><value>Huésped (ID: '{0}') ya está en una estancia en el periodo: {1}</value></data>
<data name="enum.StayExportFileResult.OK" xml:space="preserve"><value>OK</value></data>
<data name="enum.StayExportFileResult.NoData" xml:space="preserve"><value>Sin datos</value></data>
<data name="enum.StayExportFileResult.ExportError" xml:space="preserve"><value>Error de Exportación</value></data>
<data name="enum.StayExportFileResult.UploadError" xml:space="preserve"><value>Error de ficheros</value></data>
<data name="enum.StayExportFormat.GuestLogJson" xml:space="preserve"><value>GuestLog JSON</value></data>
<data name="enum.StayExportFormat.SpainParteViajeros" xml:space="preserve"><value>Spain Parte Viajeros</value></data>
<data name="enum.StayExportParamType.PeriodDateIn" xml:space="preserve"><value>'Date Entrada' Estancia</value></data>
<data name="navMenu.GuestDocumentList" xml:space="preserve"><value>Huéspedes</value></data>
<data name="navMenu.StayEntryList" xml:space="preserve"><value>Estancias</value></data>
<data name="navMenu.StayExport" xml:space="preserve"><value>Exportación de Estancias</value></data>
<data name="page.GuestDocList.pageTitle" xml:space="preserve"><value>Documentos Huéspedes</value></data>
<data name="page.GuestDocList.documentList" xml:space="preserve"><value>Lista de Documentos</value></data>
<data name="page.GuestDocList.btnNewGuest" xml:space="preserve"><value>Nuevo Huésped</value></data>
<data name="page.GuestDocNew.msgAlreadyExists" xml:space="preserve"><value>¡Ya existe un huésped con este documento!</value></data>
<data name="page.GuestDocNew.btnGoExistingDoc" xml:space="preserve"><value>Ver huésped existente</value></data>
<data name="page.GuestDocDetail.msgEditEnabled" xml:space="preserve"><value>Se ha entrado en modo edición</value></data>
<data name="page.GuestDocDetail.msgConfirmSaveEdit" xml:space="preserve"><value>Guardar cambios en el documento</value></data>
<data name="page.GuestDocDetail.msgConfirmCancelEdit" xml:space="preserve"><value>Cancelar edición. Se perderán los cambios</value></data>
<data name="page.GuestDocDetail.msgEditError" xml:space="preserve"><value>Error al guardar el documento '{0}'</value></data>
<data name="page.StayEntryList.pageTitle" xml:space="preserve"><value>Lista Estancias</value></data>
<data name="page.StayEntryList.entryList" xml:space="preserve"><value>Lista Estancias</value></data>
<data name="page.StayEntryList.btnNewStay" xml:space="preserve"><value>Nueva Estancia</value></data>
<data name="page.StayEntryNew.pageTitle" xml:space="preserve"><value>Nueva Estancia</value></data>
<data name="page.StayExportList.pageTitle" xml:space="preserve"><value>Exportación de Estancias</value></data>
<data name="page.StayExportList.exportList" xml:space="preserve"><value>Listado de Exportaciones</value></data>
<data name="page.StayExportList.btnNewExport" xml:space="preserve"><value>Nueva Exportación</value></data>
<data name="page.StayExportNew.pageTitle" xml:space="preserve"><value>Nueva Exportación de Estancias</value></data>
<data name="control.GuestDocForm.sectionDocument" xml:space="preserve"><value>Datos Documento</value></data>
<data name="control.GuestDocForm.sectionGuest" xml:space="preserve"><value>Datos Huésped</value></data>
<data name="control.GuestDocForm.sectionContact" xml:space="preserve"><value>Datos de contacto</value></data>
<data name="control.GuestDocForm.sectionAddress" xml:space="preserve"><value>Datos Dirección</value></data>
<data name="control.GuestDocForm.btnSearchAddress" xml:space="preserve"><value>Buscar Dirección</value></data>
<data name="control.StayEntryForm.sectionDetails" xml:space="preserve"><value>Detalles estancia</value></data>
<data name="control.StayEntryForm.sectionGuests" xml:space="preserve"><value>Lista de Huéspedes</value></data>
<data name="control.StayEntryForm.btnAddGuest" xml:space="preserve"><value>Añadir Huésped</value></data>
<data name="control.AddressQueryDialog.dialogTitle" xml:space="preserve"><value>Buscar dirección</value></data>
<data name="control.AddressQueryDialog.searchText" xml:space="preserve"><value>Buscar dirección</value></data>
<data name="control.AddressQueryDialog.btnSearch" xml:space="preserve"><value>Buscar dirección</value></data>
<data name="control.AddressQueryDialog.btnSelectAddress" xml:space="preserve"><value>Usar dirección</value></data>
<data name="control.GuestDocumentSearchDialog.dialogTitle" xml:space="preserve"><value>Buscar Huésped</value></data>
<data name="control.GuestDocumentSearchDialog.btnSelectDocument" xml:space="preserve"><value>Usar Huésped</value></data>
<data name="control.GuestDocumentSearchDialog.txtSuggestedGuests" xml:space="preserve"><value>Huéspedes sugeridos</value></data>
</root>
@@ -122,6 +122,8 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="dto.GuestDocument.Id" xml:space="preserve"><value>ID</value></data>
<data name="dto.GuestDocument.Version" xml:space="preserve"><value>Version</value></data>
<data name="dto.GuestDocument.DocumentType" xml:space="preserve"><value>Document Type</value></data>
<data name="dto.GuestDocument.DocumentNum" xml:space="preserve"><value>Document Number</value></data>
<data name="dto.GuestDocument.IssueDate" xml:space="preserve"><value>Issue Date</value></data>
@@ -147,6 +149,18 @@
<data name="dto.StayEntry.CreateTime" xml:space="preserve"><value>Creation Time</value></data>
<data name="dto.StayEntry.UpdateTime" xml:space="preserve"><value>Update Time</value></data>
<data name="dto.StayExportFile.Id" xml:space="preserve"><value>Id</value></data>
<data name="dto.StayExportFile.ExportFormat" xml:space="preserve"><value>Export Format</value></data>
<data name="dto.StayExportFile.ExportParams" xml:space="preserve"><value>Export Params</value></data>
<data name="dto.StayExportFile.ExportResult" xml:space="preserve"><value>Export Result</value></data>
<data name="dto.StayExportFile.ExportedPath" xml:space="preserve"><value>Export Path</value></data>
<data name="dto.StayExportFile.ExportDetails" xml:space="preserve"><value>Export Details</value></data>
<data name="dto.StayExportFile.ExportTime" xml:space="preserve"><value>Export Time</value></data>
<data name="dto.StayExportParams.ExportType" xml:space="preserve"><value>Export Type</value></data>
<data name="dto.StayExportParams.ExportPeriodStart" xml:space="preserve"><value>Date From</value></data>
<data name="dto.StayExportParams.ExportPeriodEnd" xml:space="preserve"><value>Date To</value></data>
<data name="tmp" xml:space="preserve"><value>TMP</value></data>
<data name="general.actions" xml:space="preserve"><value>Actions</value></data>
@@ -166,6 +180,12 @@
<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.loading" xml:space="preserve"><value>Loading...</value></data>
<data name="general.msgEditSaved" xml:space="preserve"><value>Changes saved...</value></data>
<data name="general.msgEditCanceled" xml:space="preserve"><value>Edit Canceled!</value></data>
<data name="general.languageSelector" xml:space="preserve"><value>Language</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 +194,33 @@
<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>
<!-- Common codes -->
<data name="enum.ResultCode.Ok" xml:space="preserve"><value>OK</value></data>
<data name="enum.ResultCode.UnknownError" xml:space="preserve"><value>Unknown Error</value></data>
<data name="enum.ResultCode.FailedValidation" xml:space="preserve"><value>Failed entity validation</value></data>
<data name="enum.ResultCode.NotFound" xml:space="preserve"><value>Not found</value></data>
<data name="enum.ResultCode.FileStorageError" xml:space="preserve"><value>File Storage Error</value></data>
<!-- Specific codes -->
<data name="enum.ResultCode.GuestInStay" xml:space="preserve"><value>Guest (ID: '{0}') is in a stay.</value></data>
<data name="enum.ResultCode.GuestCurrentlyInStay" xml:space="preserve"><value>Guest (ID: '{0}') is already in one stay during the period: {1}</value></data>
<data name="enum.StayExportFileResult.OK" xml:space="preserve"><value>OK</value></data>
<data name="enum.StayExportFileResult.NoData" xml:space="preserve"><value>No Data</value></data>
<data name="enum.StayExportFileResult.ExportError" xml:space="preserve"><value>Export Error</value></data>
<data name="enum.StayExportFileResult.UploadError" xml:space="preserve"><value>File Error</value></data>
<data name="enum.StayExportFormat.GuestLogJson" xml:space="preserve"><value>GuestLog JSON</value></data>
<data name="enum.StayExportFormat.SpainParteViajeros" xml:space="preserve"><value>Spain Parte Viajeros</value></data>
<data name="enum.StayExportParamType.PeriodDateIn" xml:space="preserve"><value>Stay 'Date In' Period</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="navMenu.StayExport" xml:space="preserve"><value>Stay Export</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>
@@ -181,12 +228,24 @@
<data name="page.GuestDocNew.msgAlreadyExists" xml:space="preserve"><value>This Guest Document Already exists!!</value></data>
<data name="page.GuestDocNew.btnGoExistingDoc" xml:space="preserve"><value>Go to existing document</value></data>
<data name="page.GuestDocDetail.msgEditEnabled" xml:space="preserve"><value>Entered Edit mode</value></data>
<data name="page.GuestDocDetail.msgConfirmSaveEdit" xml:space="preserve"><value>Save changes to GuestDocument</value></data>
<data name="page.GuestDocDetail.msgConfirmCancelEdit" xml:space="preserve"><value>Cancel edit. Changes will be lost!</value></data>
<data name="page.GuestDocDetail.msgEditError" xml:space="preserve"><value>Error saving Document '{0}'</value></data>
<data name="page.StayEntryList.pageTitle" xml:space="preserve"><value>Stay Entries</value></data>
<data name="page.StayEntryList.entryList" xml:space="preserve"><value>Entry List</value></data>
<data name="page.StayEntryList.btnNewStay" xml:space="preserve"><value>New Stay</value></data>
<data name="page.StayEntryNew.pageTitle" xml:space="preserve"><value>New Stay</value></data>
<data name="page.StayExportList.pageTitle" xml:space="preserve"><value>Stay Export Files</value></data>
<data name="page.StayExportList.exportList" xml:space="preserve"><value>Export List</value></data>
<data name="page.StayExportList.btnNewExport" xml:space="preserve"><value>New Export</value></data>
<data name="page.StayExportNew.pageTitle" xml:space="preserve"><value>New Stay Export</value></data>
<data name="control.GuestDocForm.sectionDocument" xml:space="preserve"><value>Document Details</value></data>
<data name="control.GuestDocForm.sectionGuest" xml:space="preserve"><value>Guest Details</value></data>
<data name="control.GuestDocForm.sectionContact" xml:space="preserve"><value>Contact Details</value></data>
@@ -204,5 +263,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>
@@ -1,4 +1,5 @@
using Microsoft.Extensions.Options;
using Somnia.GuestLog.Enums.Services;
using Somnia.GuestLog.Models.Configuration;
using Somnia.GuestLog.Utils.DI;
@@ -11,8 +12,8 @@ public class ConfigFileAddressQueryFactory(
{
public static void RegisterFactoryItems(IServiceCollection services)
{
services.AddKeyedTransient<IAddressQueryService, NominatimAddressQueryService>(AddressQueryConfig.AddressQueryProviderEnum.Nominatim);
services.AddKeyedTransient<IAddressQueryService, GoogleApiAddressQueryService>(AddressQueryConfig.AddressQueryProviderEnum.GoogleApiV4);
services.AddKeyedTransient<IAddressQueryService, NominatimAddressQueryService>(AddressQueryProvider.Nominatim);
services.AddKeyedTransient<IAddressQueryService, GoogleApiAddressQueryService>(AddressQueryProvider.GoogleApiV4);
}
public IAddressQueryService GetService()
@@ -43,8 +43,8 @@ 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,
AddressProvince = result.Address.Province,
AddressCity = result.Address.City ?? result.Address.Municipality ?? result.Address.Town ?? result.Address.Village,
AddressProvince = result.Address.Province ?? result.Address.State_district ?? result.Address.County,
AddressState = result.Address.State,
AddressCountry = result.Address.Country,
ZipCode = result.Address.Postcode,
@@ -97,6 +97,8 @@ public class NominatimAddressQueryService(
string? Village = null,
string? Town = null,
string? Province = null,
string? State_district = null,
string? County = null,
string? State = null,
string? Country = null,
string? Postcode = null,
@@ -0,0 +1,301 @@
using FluentValidation;
using Microsoft.EntityFrameworkCore;
using Somnia.GuestLog.Enums.Services;
using Somnia.GuestLog.Infrastructure;
using Somnia.GuestLog.Models;
using Somnia.GuestLog.Models.DTOs;
using Somnia.GuestLog.Models.Entities;
using Somnia.GuestLog.Utils;
namespace Somnia.GuestLog.Services.Entity;
public class GuestDocumentEntityService(
ILogger<GuestDocumentEntityService> logger,
IDbContextFactory<GuestLogContext> contextFactory,
IValidator<GuestDocumentDTO> dtoValidator,
IStayEntryEntityService stayEntityService
) : IGuestDocumentEntityService
{
public async Task<Result> CreateDocumentAsync(GuestDocumentDTO dto)
{
ArgumentNullException.ThrowIfNull(dto);
var valResult = dtoValidator.Validate(dto);
if (!valResult.IsValid)
{
logger.LogWarning("GuestDocumentService - CreateDocument: Dto validation failed. Fix the errors: {errorList}", valResult.Errors);
return Result.Fail(ResultCode.FailedValidation);
}
//Dto must be valid, so null warings shouldn't matter.
GuestAddress addressItem = new()
{
Address = dto.Address,
AddressCity = dto.AddressCity,
AddressProvince = dto.AddressProvince,
AddressState = dto.AddressState,
AddressCountry = dto.AddressCountry!,
ZipCode = dto.ZipCode,
};
DateTime createTime = DateTime.UtcNow;
GuestDocument dbItem = new()
{
Version = GuestConsts.GuestDocument_FirstVersion,
DocumentType = dto.DocumentType!,
DocumentNum = dto.DocumentNum!,
IssueDate = dto.IssueDate != null ? DateOnly.FromDateTime(dto.IssueDate!.Value) : null,
SupportNum = dto.SupportNum,
Nationality = dto.Nationality!,
Name = dto.Name!,
Surname = dto.Surname!,
Surname2 = dto.Surname2,
BirthDate = DateOnly.FromDateTime(dto.BirthDate!.Value),
Gender = dto.Gender,
PhoneNumber = dto.PhoneNumber,
EmailAddress = dto.EmailAddress,
AddressDetails = addressItem,
CreateTime = createTime,
UpdateTime = createTime,
IsLastVersion = true, //On creation, created entity is last version.
};
try
{
using var dbContext = contextFactory.CreateDbContext();
await dbContext.AddAsync(dbItem);
await dbContext.SaveChangesAsync();
logger.LogInformation("GuestDocumentService - CreateDocument: GuestDocument Created: '{id}'", dbItem.GetFullKey());
return Result.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "GuestDocumentService - CreateDocument: Error creating GuestDocument: '{id}'", dbItem.GetFullKey());
return Result.FailUnknownError();
}
}
public async Task<Result> UpdateDocumentAsync(GuestDocumentDTO dto)
{
var valResult = dtoValidator.Validate(dto);
if (!valResult.IsValid)
{
logger.LogWarning("GuestDocumentService - UpdateDocument: Dto validation failed. Fix the errors: {errorList}", valResult.Errors);
return Result.Fail(ResultCode.FailedValidation);
}
try
{
using var dbContext = contextFactory.CreateDbContext();
//Get current entity from DB.
GuestDocument? dbItem = await dbContext.FindAsync<GuestDocument>(dto.Id, dto.Version);
if (dbItem == null)
{
logger.LogWarning("GuestDocumentService - UpdateDocument: Error updating GuestDocument (ID: '{id}'): GuestDocument not found!", dto.FullKey);
return Result.Fail(ResultCode.NotFound);
}
//Dto must be valid, so null warings shouldn't matter.
GuestAddress addressItem = new()
{
Address = dto.Address,
AddressCity = dto.AddressCity,
AddressProvince = dto.AddressProvince,
AddressState = dto.AddressState,
AddressCountry = dto.AddressCountry!,
ZipCode = dto.ZipCode,
};
DateTime updateTime = DateTime.UtcNow;
GuestDocument updatedItem = new()
{
Id = dbItem.Id,
Version = dbItem.Version,
DocumentType = dbItem.DocumentType,
DocumentNum = dbItem.DocumentNum,
IssueDate = dto.IssueDate != null ? DateOnly.FromDateTime(dto.IssueDate!.Value) : null,
SupportNum = dto.SupportNum,
Nationality = dto.Nationality!,
Name = dto.Name!,
Surname = dto.Surname!,
Surname2 = dto.Surname2,
BirthDate = DateOnly.FromDateTime(dto.BirthDate!.Value),
Gender = dto.Gender,
PhoneNumber = dto.PhoneNumber,
EmailAddress = dto.EmailAddress,
AddressDetails = addressItem,
CreateTime = dbItem.CreateTime,
UpdateTime = updateTime,
IsLastVersion = dbItem.IsLastVersion,
};
dbContext.Entry(dbItem).CurrentValues.SetValues(updatedItem);
var result = await dbContext.SaveChangesAsync();
if (result != 1)
{
logger.LogWarning("GuestDocumentService - UpdateDocument: Unexpected updated entity count: {count}", result);
}
logger.LogInformation("GuestDocumentService - UpdateDocument: GuestDocument Updated OK {id}", dbItem.GetFullKey());
return Result.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "GuestDocumentService - UpdateDocument: Error updating GuestDocument: '{id}'", dto.FullKey);
return Result.FailUnknownError();
}
}
public Task<Result> DeleteDocumentAsync(GuestDocumentDTO dto)
{
if (dto.Id is null)
{
throw new InvalidOperationException("GuestDocumentService - DeleteDocument: A DTO without ID can't be deleted!");
}
return DeleteDocumentAsync(dto.Id.Value);
}
public async Task<Result> DeleteDocumentAsync(long id)
{
try
{
using var dbContext = contextFactory.CreateDbContext();
//Check if GuestDocument is used in a Stay.
bool isUsed = await stayEntityService.IsGuestInStay(id);
if (isUsed)
{
logger.LogWarning("GuestDocumentService - DeleteDocument: Error deleting GuestDocument. Guest (ID: '{ID}') is in a Stay", id);
return Result.FailWithContext(ResultCode.GuestInStay, id);
}
int deletedCount = await dbContext.GuestDocuments
.Where(e => e.Id == id)
.ExecuteDeleteAsync();
if (deletedCount == 0)
{
logger.LogWarning("GuestDocumentService - DeleteDocument: Error deleting GuestDocument. Entity not found with ID: '{ID}'", id);
return Result.Fail(ResultCode.NotFound);
}
logger.LogInformation(
"GuestDocumentService - DeleteDocument: GuestDocument deleted with Id: '{id}'. Versions deleted: '{Count}'",
id, deletedCount);
return Result.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "GuestDocumentService - CreateDocument: Error deleting GuestDocument: '{id}'", id);
return Result.FailUnknownError();
}
}
public async Task<GuestDocumentDTO?> GetLastVersionDtoByIdAsync(long id)
{
using var dbContext = contextFactory.CreateDbContext();
GuestDocument? dbItem = await dbContext.GuestDocuments
.Where(e => e.Id == id)
.OrderByDescending(e => e.Version)
.FirstOrDefaultAsync();
return dbItem is not null ? GetDtoFromDb(dbItem) : null;
}
public async Task<GuestDocumentDTO?> GetLastVersionDtoByDocumentAsync(string docType, string docNum)
{
using var dbContext = contextFactory.CreateDbContext();
GuestDocument? dbItem = await dbContext.GuestDocuments
.Where(e => e.DocumentType == docType && e.DocumentNum == docNum)
.OrderByDescending(e => e.Version)
.FirstOrDefaultAsync();
return dbItem is not null ? GetDtoFromDb(dbItem) : null;
}
public async Task<IEnumerable<GuestDocumentDTO>> GetDtoListAsync(bool onlyLastVersion)
{
using var dbContext = contextFactory.CreateDbContext();
// TEMP solution
var query = dbContext.GuestDocuments.AsQueryable();
if (onlyLastVersion)
{
query = query.Where(e => e.IsLastVersion == true);
}
var dbList = await query.OrderByDescending(e => e.UpdateTime)
.ToListAsync();
// TODO: Waiting for EF Core Window function improvements:
// See expected translation: https://github.com/dotnet/efcore/issues/36380#issuecomment-3082788964
// See window function status: https://github.com/dotnet/efcore/issues/12747
// var dbList = await dbContext.GuestDocuments
// .GroupBy(e => e.Id)
// .Select(g => g.OrderByDescending(e => e.Version).First())
// .ToListAsync();
return dbList.Select(GetDtoFromDb);
}
/// <summary>
/// Get GuestDocumentDTO from DB with the given filters.
/// Result count can be limited for perfomance reasons.
/// </summary>
/// <param name="filter">Filters to use</param>
/// <param name="limit">Result count. Set to 0 for unlimited results</param>
/// <returns>Filtered GuestDocumentDTO result list</returns>
public async Task<IEnumerable<GuestDocumentDTO>> GetDtoListByFilterAsync(GuestDocFilterDTO filter, int limit = 0)
{
using var dbContext = contextFactory.CreateDbContext();
var query = dbContext.GuestDocuments.AsQueryable();
query = query.WhereIf(!string.IsNullOrEmpty(filter.DocumentType),
e => e.DocumentType == filter.DocumentType);
query = query.WhereIf(!string.IsNullOrEmpty(filter.DocumentNumber),
e => e.DocumentNum == filter.DocumentNumber);
query = query.WhereIf(!string.IsNullOrEmpty(filter.Name),
e => EF.Functions.Like(e.Name, filter.Name));
query = query.WhereIf(!string.IsNullOrEmpty(filter.Surname),
e => EF.Functions.Like(e.Surname, filter.Surname));
// TEMP while waiting for EF Core Window function improvements
query = query.WhereIf(filter.OnlyLastVersion,
e => e.IsLastVersion == true);
query = query.OrderByDescending(e => e.UpdateTime);
// TODO: Waiting for EF Core Window function improvements:
// See: https://github.com/dotnet/efcore/issues/27470
// See window function status: https://github.com/dotnet/efcore/issues/12747
// For Ordering (or operations post-grouping), see: https://github.com/dotnet/efcore/pull/38687
// query = query
// .OrderByDescending(e => e.UpdateTime)
// .GroupBy(e => e.Id)
// .Select(g => g.OrderByDescending(e => e.Version).First());
if (limit > 0)
{
query = query.Take(limit);
}
var dbList = await query.ToListAsync();
return dbList.Select(x => GetDtoFromDb(x));
}
#region Internal Methods
private static GuestDocumentDTO GetDtoFromDb(GuestDocument dbItem)
{
return EntityMapping.MapGuestDocumentEntityToDTO(dbItem);
}
#endregion
}
@@ -0,0 +1,28 @@
using Somnia.GuestLog.Models.DTOs;
namespace Somnia.GuestLog.Services.Entity;
public interface IGuestDocumentEntityService
{
public Task<Result> CreateDocumentAsync(GuestDocumentDTO dto);
public Task<Result> UpdateDocumentAsync(GuestDocumentDTO dto);
/// <summary>
/// Same behavior as <see cref="DeleteDocumentAsync(long)"/>
/// </summary>
public Task<Result> DeleteDocumentAsync(GuestDocumentDTO dto);
/// <summary>
/// Delete documents by ID.
/// This operation will delete all versions of a document.
/// </summary>
/// <param name="id">ID of the document to Delete</param>
public Task<Result> DeleteDocumentAsync(long id);
public Task<GuestDocumentDTO?> GetLastVersionDtoByIdAsync(long id);
public Task<GuestDocumentDTO?> GetLastVersionDtoByDocumentAsync(string docType, string docNum);
public Task<IEnumerable<GuestDocumentDTO>> GetDtoListAsync(bool onlyLastVersion);
public Task<IEnumerable<GuestDocumentDTO>> GetDtoListByFilterAsync(GuestDocFilterDTO filter, int limit = 0);
}
@@ -0,0 +1,22 @@
using Somnia.GuestLog.Models.DTOs;
using Somnia.GuestLog.Models.QueryConfigs;
namespace Somnia.GuestLog.Services.Entity;
public interface IStayEntryEntityService
{
public Task<Result> CreateEntryAsync(StayEntryDTO dto);
public Task<Result> UpdateEntryAsync(StayEntryDTO updatedDto);
public Task<Result> DeleteEntryAsync(StayEntryDTO dto);
public Task<Result> DeleteEntryAsync(long id);
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> IsGuestInStay(long guestDocId);
public Task<bool> IsGuestInStayInPeriod(long guestDocId, DateTime startTime, DateTime endTime);
public Task<IEnumerable<StayEntryDTO>> GetStaysWithGuest(long guestDocId, StayEntryQueryConfig? queryConfig = null);
}
@@ -0,0 +1,19 @@
using Somnia.GuestLog.Models.DTOs;
namespace Somnia.GuestLog.Services.Entity;
public interface IStayExportFileEntityService
{
public Task<Result> CreateStayExportAsync(StayExportFileDTO dto);
public Task<Result> UpdateStayExportAsync(StayExportFileDTO dto);
/// <summary>
/// Delete StayExport entry from DB.
/// Exported File won't be deleted from Storage.
/// </summary>
public Task<Result> DeleteStayExportAsync(StayExportFileDTO dto);
public Task<StayExportFileDTO?> GetDtoByIdAsync(long id);
//public Task<IEnumerable<StayExportFileDTO>> GetDtoListByFilterAsync(StayExportFileFilterDTO filter, int limit = 0);
public Task<IEnumerable<StayExportFileDTO>> GetDtoListAsync(int limit = 0);
}
@@ -0,0 +1,218 @@
using FluentValidation;
using Microsoft.EntityFrameworkCore;
using Somnia.GuestLog.Enums.Services;
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.Entity;
public class StayEntryEntityService(
ILogger<StayEntryEntityService> logger,
IDbContextFactory<GuestLogContext> contextFactory,
IValidator<StayEntryDTO> dtoValidator
) : IStayEntryEntityService
{
public async Task<Result> 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 Result.Fail(ResultCode.FailedValidation);
}
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 guest from db.
GuestDocument dbGuest = await dbContext.GuestDocuments
.Where(gd => gd.Id == guestDto.Id && gd.Version == guestDto.Version)
.SingleAsync();
//Check if Guest is already in other stay during the same period.
bool isInOtherStay = await this.IsGuestInStayInPeriod(dbGuest.Id, dbStay.DateIn, dbStay.DateOut);
if (isInOtherStay)
{
string period = $"{dbStay.DateIn}-{dbStay.DateOut}";
return Result.FailWithContext(ResultCode.GuestCurrentlyInStay, dbGuest.FullDocument, period);
}
//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 Result.OkWithContext(dbStay.GetFullKey());
}
catch (Exception ex)
{
logger.LogError(ex, "StayEntryService - CreateEntryAsync: Error creating StayEntry");
return Result.FailUnknownError();
}
}
public Task<Result> UpdateEntryAsync(StayEntryDTO updatedDto)
{
throw new NotImplementedException();
}
public async Task<Result> DeleteEntryAsync(StayEntryDTO dto)
{
if (dto.Id is null)
{
throw new InvalidOperationException("StayEntryService - DeleteEntry: A DTO without key can't be deleted!");
}
return await DeleteEntryAsync(dto.Id.Value);
}
public async Task<Result> 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 Result.Fail(ResultCode.NotFound);
}
try
{
dbContext.StayEntries.Remove(dbItem);
await dbContext.SaveChangesAsync();
logger.LogInformation("StayEntryService - DeleteEntry: StayEntry deleted: '{id}'", dbItem.GetFullKey());
return Result.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "StayEntryService - DeleteEntry: Error deleting StayEntry: '{id}'", dbItem.GetFullKey());
return Result.FailUnknownError();
}
}
public async Task<StayEntryDTO?> GetDtoByIdAsync(long id, StayEntryQueryConfig? queryConfig = null)
{
using var dbContext = contextFactory.CreateDbContext();
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(StayEntryQueryConfig? queryConfig = null)
{
using var dbContext = contextFactory.CreateDbContext();
var dbList = await dbContext.StayEntries
.AddQueryConfig(queryConfig)
.OrderByDescending(e => e.UpdateTime)
.ToListAsync();
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);
query = query.OrderByDescending(e => e.DateIn)
.ThenByDescending(e => e.UpdateTime);
if (limit > 0)
{
query = query.Take(limit);
}
var dbList = await query
.ToListAsync();
return dbList.Select(x => GetDtoFromDb(x, queryConfig));
}
public async Task<bool> IsGuestInStay(long guestDocId)
{
using var dbContext = contextFactory.CreateDbContext();
bool rs = await dbContext.StayEntries
.AnyAsync(s => s.GuestDocuments.Any(g => g.Id == guestDocId));
return rs;
}
public async Task<bool> IsGuestInStayInPeriod(long guestDocId, 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.Id == guestDocId)
);
return rs;
}
public async Task<IEnumerable<StayEntryDTO>> GetStaysWithGuest(long guestDocId, StayEntryQueryConfig? queryConfig = null)
{
using var dbContext = contextFactory.CreateDbContext();
var rs = await dbContext.StayEntries
.Where(s => s.GuestDocuments.Any(g => g.Id == guestDocId))
.AddQueryConfig(queryConfig)
.OrderByDescending(e => e.DateIn)
.ToListAsync();
return rs.Select(e => GetDtoFromDb(e, queryConfig));
}
#region Internal Methods
private static StayEntryDTO GetDtoFromDb(StayEntry dbItem, StayEntryQueryConfig? queryConfig)
{
queryConfig ??= StayEntryQueryConfig.DefaultQueryConfig;
return EntityMapping.MapStayEntryEntityToDTO(dbItem, queryConfig.IncludeGuests);
}
#endregion
}
@@ -0,0 +1,141 @@
using FluentValidation;
using Microsoft.EntityFrameworkCore;
using Somnia.GuestLog.Enums.Services;
using Somnia.GuestLog.Infrastructure;
using Somnia.GuestLog.Models;
using Somnia.GuestLog.Models.DTOs;
using Somnia.GuestLog.Models.Entities;
using Somnia.GuestLog.Utils;
namespace Somnia.GuestLog.Services.Entity;
public class StayExportFileEntityService(
ILogger<StayExportFileEntityService> logger,
IDbContextFactory<GuestLogContext> contextFactory
) : IStayExportFileEntityService
{
public async Task<Result> CreateStayExportAsync(StayExportFileDTO dto)
{
ArgumentNullException.ThrowIfNull(dto, nameof(dto));
//Check for null values in the DTO.
if (dto.ExportFormat == null || dto.ExportResult == null || dto.ExportTime == null)
{
throw new InvalidOperationException("StayExportFile dto contains null fields");
}
if (dto.ExportParams == null)
{
throw new InvalidOperationException("StayExportFile dto contains null ExportParams");
}
StayExportParams exportParams = GetExportParamsFromDTO(dto.ExportParams);
StayExportFile dbItem = new()
{
ExportFormat = dto.ExportFormat.Value,
ExportParams = exportParams,
ExportResult = dto.ExportResult.Value,
ExportedPath = dto.ExportedPath,
ExportDetails = dto.ExportDetails,
ExportTime = dto.ExportTime?.ToUniversalTime() ?? DateTime.UtcNow,
};
try
{
using var dbContext = contextFactory.CreateDbContext();
await dbContext.AddAsync(dbItem);
await dbContext.SaveChangesAsync();
logger.LogInformation(
"StayExportFileService - CreateStayExportAsync: StayExport Created (ID: '{id}') with result: '{result}'",
dbItem.GetFullKey(), dbItem.ExportResult);
return Result.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "StayExportFileService - CreateStayExportAsync: Error creating StayExport");
return Result.FailUnknownError();
}
}
public Task<Result> UpdateStayExportAsync(StayExportFileDTO dto)
{
throw new NotImplementedException();
}
public async Task<Result> DeleteStayExportAsync(StayExportFileDTO dto)
{
try
{
using var dbContext = contextFactory.CreateDbContext();
int deletedCount = await dbContext.StayExportFiles
.Where(e => e.Id == dto.Id)
.ExecuteDeleteAsync();
if (deletedCount == 0)
{
logger.LogWarning("StayExportFileService - DeleteStayExportAsync: Error deleting StayExport. Entity not found with ID: '{ID}'", dto.Id);
return Result.Fail(ResultCode.NotFound);
}
logger.LogInformation(
"StayExportFileService - DeleteStayExportAsync: StayExport deleted with Id: '{id}'.", dto.Id);
return Result.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "StayExportFileService - DeleteStayExportAsync: Error deleting StayExport");
return Result.FailUnknownError();
}
}
public async Task<StayExportFileDTO?> GetDtoByIdAsync(long id)
{
using var dbContext = contextFactory.CreateDbContext();
StayExportFile? dbItem = await dbContext.StayExportFiles
.Where(e => e.Id == id)
.FirstOrDefaultAsync();
return dbItem is not null ? GetDtoFromDb(dbItem) : null;
}
public async Task<IEnumerable<StayExportFileDTO>> GetDtoListAsync(int limit = 0)
{
using var dbContext = contextFactory.CreateDbContext();
IQueryable<StayExportFile> query = dbContext.StayExportFiles
.OrderByDescending(e => e.ExportTime)
.WithLimit(limit);
var dbList = await query.ToListAsync();
return dbList.Select(e => GetDtoFromDb(e));
}
#region Internal Methods
private static StayExportFileDTO GetDtoFromDb(StayExportFile dbItem)
{
return EntityMapping.MapStayExportFileEntityToDTO(dbItem);
}
private static StayExportParams GetExportParamsFromDTO(StayExportParamsDTO dto)
{
if (dto.ExportType == null)
{
throw new InvalidOperationException("Dto with invalid data");
}
return new()
{
ExportType = dto.ExportType.Value,
ExportPeriodStart = dto.ExportPeriodStart?.ToUniversalTime(),
ExportPeriodEnd = dto.ExportPeriodEnd?.ToUniversalTime()
};
}
#endregion
}
@@ -1,177 +0,0 @@
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;
namespace Somnia.GuestLog.Services;
public class GuestDocumentService(
ILogger<GuestDocumentService> logger,
IDbContextFactory<GuestLogContext> contextFactory,
IValidator<GuestDocumentDTO> dtoValidator
) : IGuestDocumentService
{
public async Task<bool> CreateDocumentAsync(GuestDocumentDTO dto)
{
ArgumentNullException.ThrowIfNull(dto);
var valResult = dtoValidator.Validate(dto);
if (!valResult.IsValid)
{
logger.LogWarning("GuestDocumentService - CreateDocument: Dto validation failed. Fix the errors: {errorList}", valResult.Errors);
return false;
}
//Dto must be valid, so null warings shouldn't matter.
GuestAddress addressItem = new()
{
Address = dto.Address,
AddressCity = dto.AddressCity,
AddressProvince = dto.AddressProvince,
AddressState = dto.AddressState,
AddressCountry = dto.AddressCountry!,
ZipCode = dto.ZipCode,
};
DateTime createTime = DateTime.UtcNow;
GuestDocument dbItem = new()
{
DocumentType = dto.DocumentType!,
DocumentNum = dto.DocumentNum!,
IssueDate = DateOnly.FromDateTime(dto.IssueDate!.Value),
SupportNum = dto.SupportNum,
Nationality = dto.Nationality!,
Name = dto.Name!,
Surname = dto.Surname!,
Surname2 = dto.Surname2,
BirthDate = DateOnly.FromDateTime(dto.BirthDate!.Value),
Gender = dto.Gender,
PhoneNumber = dto.PhoneNumber,
EmailAddress = dto.EmailAddress,
AddressDetails = addressItem,
CreateTime = createTime,
UpdateTime = createTime,
};
try
{
using var dbContext = contextFactory.CreateDbContext();
await dbContext.AddAsync(dbItem);
await dbContext.SaveChangesAsync();
logger.LogInformation("GuestDocumentService - CreateDocument: GuestDocument Created: '{id}'", dbItem.GetFullKey());
return true;
}
catch (Exception ex)
{
logger.LogError(ex, "GuestDocumentService - CreateDocument: Error creating GuestDocument: '{id}'", dbItem.GetFullKey());
return false;
}
}
public Task<bool> UpdateDocumentAsync(GuestDocumentDTO dto)
{
throw new NotImplementedException();
}
public Task<bool> DeleteDocumentAsync(GuestDocumentDTO dto)
{
if (dto.DocumentType is null || dto.DocumentNum is null)
{
throw new InvalidOperationException("GuestDocumentService - DeleteDocument: A DTO without key can't be deleted!");
}
return DeleteDocumentAsync(dto.DocumentType, dto.DocumentNum);
}
public async Task<bool> DeleteDocumentAsync(string docType, string docNum)
{
using var dbContext = contextFactory.CreateDbContext();
GuestDocument? dbItem = await dbContext.GuestDocuments.FindAsync(docType, docNum);
if (dbItem is null)
{
logger.LogWarning("GuestDocumentService - DeleteDocument: Error deleting GuestDocument. Entity not found with key: '{key}-{key2}'", docType, docNum);
return false;
}
try
{
dbContext.GuestDocuments.Remove(dbItem);
await dbContext.SaveChangesAsync();
logger.LogInformation("GuestDocumentService - DeleteDocument: GuestDocument deleted: '{id}'", dbItem.GetFullKey());
return true;
}
catch (Exception ex)
{
logger.LogError(ex, "GuestDocumentService - CreateDocument: Error deleting GuestDocument: '{id}'", dbItem.GetFullKey());
return false;
}
}
public async Task<GuestDocumentDTO?> GetDtoByIdAsync(string docType, string docNum)
{
using var dbContext = contextFactory.CreateDbContext();
GuestDocument? dbItem = await dbContext.GuestDocuments.FindAsync(docType, docNum);
return dbItem is not null ? GetDtoFromDb(dbItem) : null;
}
public async Task<IEnumerable<GuestDocumentDTO>> GetDtoListAsync()
{
using var dbContext = contextFactory.CreateDbContext();
var dbList = await dbContext.GuestDocuments
.OrderByDescending(e => e.UpdateTime)
.ToListAsync();
return dbList.Select(GetDtoFromDb);
}
/// <summary>
/// Get GuestDocumentDTO from DB with the given filters.
/// Result count can be limited for perfomance reasons.
/// </summary>
/// <param name="filter">Filters to use</param>
/// <param name="limit">Result count. Set to 0 for unlimited results</param>
/// <returns>Filtered GuestDocumentDTO result list</returns>
public async Task<IEnumerable<GuestDocumentDTO>> GetDtoListByFilterAsync(GuestDocFilterDTO filter, int limit = 0)
{
using var dbContext = contextFactory.CreateDbContext();
var query = dbContext.GuestDocuments.AsQueryable();
query = query.WhereIf(!string.IsNullOrEmpty(filter.DocumentType),
e => e.DocumentType == filter.DocumentType);
query = query.WhereIf(!string.IsNullOrEmpty(filter.DocumentNumber),
e => e.DocumentNum == filter.DocumentNumber);
query = query.WhereIf(!string.IsNullOrEmpty(filter.Name),
e => EF.Functions.Like(e.Name, filter.Name));
query = query.WhereIf(!string.IsNullOrEmpty(filter.Surname),
e => EF.Functions.Like(e.Surname, filter.Surname));
query = query.OrderByDescending(e => e.UpdateTime);
if (limit > 0)
{
query = query.Take(limit);
}
var dbList = await query.ToListAsync();
return dbList.Select(x => GetDtoFromDb(x));
}
#region Internal Methods
private static GuestDocumentDTO GetDtoFromDb(GuestDocument dbItem)
{
return EntityMapping.MapGuestDocumentEntityToDTO(dbItem);
}
#endregion
}
@@ -1,16 +0,0 @@
using Somnia.GuestLog.Models.DTOs;
namespace Somnia.GuestLog.Services;
public interface IGuestDocumentService
{
public Task<bool> CreateDocumentAsync(GuestDocumentDTO dto);
public Task<bool> UpdateDocumentAsync(GuestDocumentDTO dto);
public Task<bool> DeleteDocumentAsync(GuestDocumentDTO dto);
public Task<bool> DeleteDocumentAsync(string docType, string docNum);
public Task<GuestDocumentDTO?> GetDtoByIdAsync(string docType, string docNum);
public Task<IEnumerable<GuestDocumentDTO>> GetDtoListByFilterAsync(GuestDocFilterDTO filter, int limit = 0);
public Task<IEnumerable<GuestDocumentDTO>> GetDtoListAsync();
}
@@ -0,0 +1,7 @@
namespace Somnia.GuestLog.Services;
public interface ILocalizerService
{
public string GetEnumValue<TEnum>(TEnum? enumValue) where TEnum : struct, Enum;
public string GetEnumValue<TEnum>(TEnum enumValue) where TEnum : struct, Enum;
}
@@ -1,19 +0,0 @@
using Somnia.GuestLog.Models.DTOs;
namespace Somnia.GuestLog.Services;
public interface IStayEntryService
{
public Task<bool> CreateEntryAsync(StayEntryDTO dto);
public Task<bool> UpdateEntryAsync(StayEntryDTO updatedDto);
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<bool> IsGuestInStayInPeriod(string docType, string docNum, DateTime startTime, DateTime endTime);
public Task<IEnumerable<StayEntryDTO>> GetStaysWithGuest(string docType, string docNum);
}
@@ -0,0 +1,12 @@
using Somnia.GuestLog.Enums.UI;
using IResult = Somnia.GuestLog.Models.DTOs.IResult;
namespace Somnia.GuestLog.Services;
public interface IUserMessageService
{
public void ShowMessage(string text, MessageKind kind, params object[] parameters);
public void ShowResult(IResult resultDto);
public void ShowResultWithMessage(IResult resultDto, string message);
}
@@ -0,0 +1,37 @@
using Microsoft.Extensions.Localization;
using Somnia.GuestLog.Resources;
namespace Somnia.GuestLog.Services;
public class LocalizerService(
IServiceProvider serviceProvider
) : ILocalizerService
{
private IStringLocalizer<LanguageLocale> Localizer
{
get
{
field ??= serviceProvider.GetRequiredService<IStringLocalizer<LanguageLocale>>();
return field;
}
}
public string GetEnumValue<TEnum>(TEnum? enumValue) where TEnum : struct, Enum
{
if (enumValue == null)
{
return GuestConsts.EmptyField;
}
return GetEnumValue(enumValue.Value);
}
public string GetEnumValue<TEnum>(TEnum enumValue) where TEnum : struct, Enum
{
string enumName = typeof(TEnum).Name;
string? strEnumValue = Enum.GetName(enumValue);
string localeKey = $"enum.{enumName}.{strEnumValue}";
return Localizer[localeKey];
}
}
@@ -0,0 +1,6 @@
namespace Somnia.GuestLog.Services.SpainCodigoMunicipio;
public interface ISpainCodigoMunicipioService
{
public Task<string?> GetCodigoMunicipioAsync(string municipio, string province);
}
@@ -0,0 +1,123 @@
using System.Xml.Serialization;
using Microsoft.AspNetCore.WebUtilities;
namespace Somnia.GuestLog.Services.SpainCodigoMunicipio;
public class SpainCodigoMunicipioCatastroService(
ILogger<SpainCodigoMunicipioCatastroService> logger,
IHttpClientFactory httpFactory
) : ISpainCodigoMunicipioService
{
private const string Api_Host = "https://ovc.catastro.meh.es";
private const string Api_Address = "OVCServWeb/OVCWcfCallejero/COVCCallejero.svc/rest/ObtenerMunicipios";
private const string Api_Param_Provincia = "Provincia";
private const string Api_Param_Municipio = "Municipio";
//Result Cache.
readonly Dictionary<(string provincia, string municipio), string> codigoMunicipioCache = [];
public async Task<string?> GetCodigoMunicipioAsync(string municipio, string province)
{
var cacheKey = (province.ToUpper(), municipio.ToUpper());
//Try get from cache.
if (codigoMunicipioCache.TryGetValue(cacheKey, out string? cacheCode))
{
logger.LogInformation("SpainCodigoMunicipioCatastroService - Municipio found in cache for: '{key}'", cacheKey);
return cacheCode;
}
try
{
string uri = GetRequestUri(municipio, province);
logger.LogInformation("SpainCodigoMunicipioCatastroService - Getting municipios from API with query: '{addr}'", uri);
var httpClient = httpFactory.CreateClient();
using var rsStream = await httpClient.GetStreamAsync(uri);
XmlSerializer serializer = new(typeof(CatastroApiResult));
if (serializer.Deserialize(rsStream) is not CatastroApiResult rs)
{
logger.LogError("SpainCodigoMunicipioCatastroService - Result can't be deserialized!");
return null;
}
if (rs.municipios.Count == 0)
{
logger.LogWarning("SpainCodigoMunicipioCatastroService - No municipios found for query: '{addr}'", uri);
return null;
}
if (rs.municipios.Count > 1)
{
logger.LogWarning("SpainCodigoMunicipioCatastroService - More than 1 municipio was returned. First one will be used!");
}
CatastroApiResultCodigoIne? codIne = rs.municipios.First().codigoIne;
if (codIne == null || codIne.codigoMunicipio == null || codIne.codigoProvincia == null)
{
logger.LogWarning("SpainCodigoMunicipioCatastroService - Returned Municipio without data!");
return null;
}
string finalCode = $"{codIne.codigoProvincia.PadLeft(2, '0')}{codIne.codigoMunicipio.PadLeft(3, '0')}";
codigoMunicipioCache.Add(cacheKey, finalCode);
return finalCode;
}
catch (Exception ex)
{
logger.LogError(ex, "SpainCodigoMunicipioCatastroService - Error getting codigo municipio from catastro API!");
return null;
}
}
#region Internal Methods
private static string GetRequestUri(string municipio, string province)
{
string baseAddress = $"{Api_Host}/{Api_Address}";
return QueryHelpers.AddQueryString(baseAddress, [
new KeyValuePair<string, string?>(Api_Param_Provincia, province),
new KeyValuePair<string, string?>(Api_Param_Municipio, municipio),
]);
}
#endregion
#region API Model
[XmlRoot(ElementName = "consulta_municipiero", Namespace = "http://www.catastro.meh.es/")]
public class CatastroApiResult()
{
[XmlArray(ElementName = "municipiero")]
[XmlArrayItem(ElementName = "muni")]
public required List<CatastroApiResultMunicipio> municipios;
}
public class CatastroApiResultMunicipio()
{
[XmlElement(ElementName = "nm")]
public string? municipio;
[XmlElement(ElementName = "loine")]
public CatastroApiResultCodigoIne? codigoIne;
}
public class CatastroApiResultCodigoIne
{
[XmlElement(ElementName = "cp")]
public string? codigoProvincia;
[XmlElement(ElementName = "cm")]
public string? codigoMunicipio;
}
#endregion
}
@@ -1,162 +0,0 @@
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<StayEntryService> logger,
IDbContextFactory<GuestLogContext> contextFactory,
IValidator<StayEntryDTO> dtoValidator
) : IStayEntryService
{
public async Task<bool> 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<bool> UpdateEntryAsync(StayEntryDTO updatedDto)
{
throw new NotImplementedException();
}
public Task<bool> 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<bool> 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<StayEntryDTO?> 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<IEnumerable<StayEntryDTO>> GetDtoListAsync()
{
using var dbContext = contextFactory.CreateDbContext();
var dbList = await dbContext.StayEntries
.OrderByDescending(e => e.UpdateTime)
.ToListAsync();
return dbList.Select(GetDtoFromDb);
}
public async Task<bool> 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<IEnumerable<StayEntryDTO>> 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
}
@@ -0,0 +1,19 @@
using Somnia.GuestLog.Enums.Models;
using Somnia.GuestLog.Models.DTOs;
namespace Somnia.GuestLog.Services.StayExport;
public interface IStayExportFormatProvider
{
public StayExportFormat GetFormat();
public string GetFilename(DateTime exportTime);
/// <summary>
/// Export Data with the corresponding format.
/// Data is copied to given stream (no other operations are performed to the stream).
/// </summary>
/// <param name="stayList">List of Stays to export</param>
/// <param name="dataStream">Stream to copy exported data.</param>
public Task<(StayExportFileResult fileResult, string? exportDetails)> ExportDataAsync(IEnumerable<StayEntryDTO> stayList, Stream dataStream);
}
@@ -0,0 +1,11 @@
using Somnia.GuestLog.Enums.Models;
namespace Somnia.GuestLog.Services.StayExport;
public interface IStayExportFormatProviderFactory
{
/// <summary>
/// Returns the StayExport service implementation given current configuration.
/// </summary>
public IStayExportFormatProvider GetProvider(StayExportFormat exportFormat);
}
@@ -0,0 +1,14 @@
using Somnia.GuestLog.Enums.Models;
using Somnia.GuestLog.Models.DTOs;
namespace Somnia.GuestLog.Services.StayExport;
public interface IStayExportService
{
public Task<Result<StayExportFileDTO>> ExportStaysByParamsAsync(StayExportFormat format, StayExportParamsDTO exportParams);
public Task<Result> GetExportedFileAsync(StayExportFileDTO item, Stream dataStream);
public Task<Result> DeleteExportedFileAsync(StayExportFileDTO item);
}
@@ -0,0 +1,30 @@
using Microsoft.Extensions.Options;
using Somnia.GuestLog.Enums.Models;
using Somnia.GuestLog.Models.Configuration;
using Somnia.GuestLog.Utils.DI;
namespace Somnia.GuestLog.Services.StayExport;
public class StayExportFormatProviderFactory(
IServiceProvider serviceProvider,
IOptions<StayExportConfig> config
) : IStayExportFormatProviderFactory, IFactoryDI
{
public static void RegisterFactoryItems(IServiceCollection services)
{
services.AddKeyedSingleton<IStayExportFormatProvider, StayExportProviderJson>(StayExportFormat.GuestLogJson);
services.AddKeyedSingleton<IStayExportFormatProvider, StayExportProviderSpainParteViajeros>(StayExportFormat.SpainParteViajeros);
}
public IStayExportFormatProvider GetProvider(StayExportFormat exportFormat)
{
if (config.Value.EnableStayExport == false)
{
throw new InvalidOperationException("StayExport is disabled. Enable and configure it first!");
}
return serviceProvider.GetKeyedService<IStayExportFormatProvider>(exportFormat)
?? throw new InvalidOperationException($"StayExport service not found for ExportFormat: '{exportFormat}'");
}
}
@@ -0,0 +1,30 @@
using System.Text.Json;
using Somnia.GuestLog.Enums.Models;
using Somnia.GuestLog.Models.DTOs;
namespace Somnia.GuestLog.Services.StayExport;
public class StayExportProviderJson : IStayExportFormatProvider
{
private const string fileExtension = "json";
public StayExportFormat GetFormat() => StayExportFormat.GuestLogJson;
public string GetFilename(DateTime exportTime)
=> $"{Enum.GetName(GetFormat())}_{exportTime.ToFileTimeUtc()}.{fileExtension}";
public async Task<(StayExportFileResult fileResult, string? exportDetails)> ExportDataAsync(IEnumerable<StayEntryDTO> stayList, Stream dataStream)
{
if (!stayList.Any())
{
return (fileResult: StayExportFileResult.NoData, exportDetails: null);
}
// Convert to JSON.
await JsonSerializer.SerializeAsync(dataStream, stayList);
List<string> exportDetails = [.. stayList.Where(s => s.Id != null).Select(s => s.Id?.ToString() ?? "")];
string details = JsonSerializer.Serialize(exportDetails);
return (fileResult: StayExportFileResult.OK, exportDetails: details);
}
}
@@ -0,0 +1,421 @@
using System.Diagnostics;
using System.Text.Json;
using System.Xml.Serialization;
using FluentValidation;
using Microsoft.Extensions.Options;
using Somnia.GuestLog.Enums.Models;
using Somnia.GuestLog.Models.Configuration;
using Somnia.GuestLog.Models.DTOs;
using Somnia.GuestLog.Services.SpainCodigoMunicipio;
namespace Somnia.GuestLog.Services.StayExport;
public class StayExportProviderSpainParteViajeros(
ILogger<StayExportProviderSpainParteViajeros> logger,
ISpainCodigoMunicipioService codigoMunicipioService,
IOptions<StayExportSpainParteViajerosConfig> configOptions
) : IStayExportFormatProvider
{
private const string fileExtension = "xml";
private const string ExportDetailsResult_Error = "Error";
private const string ExportDetailsResult_Ok = "Ok";
private const string PersonRole = "VI";
private const string PersonRelation = "OT";
private const string PersonTipoDoc_NIF = "NIF";
private const string PersonTipoDoc_NIE = "NIE";
private const string PersonTipoDoc_PAS = "PAS";
private const string PersonTipoDoc_OTRO = "OTRO";
private const string PersonDireccionPais_ESP = "ESP";
private const string XmlNamespace_Peticion = "http://www.neg.hospedajes.mir.es/altaParteHospedaje";
private const string XmlNamespace_Peticion_Prefix = "ns2";
private readonly StayExportSpainParteViajerosConfig config = configOptions.Value;
public StayExportFormat GetFormat() => StayExportFormat.SpainParteViajeros;
public string GetFilename(DateTime exportTime)
=> $"{Enum.GetName(GetFormat())}_{exportTime.ToFileTimeUtc()}.{fileExtension}";
public async Task<(StayExportFileResult fileResult, string? exportDetails)> ExportDataAsync(IEnumerable<StayEntryDTO> stayList, Stream dataStream)
{
if (string.IsNullOrEmpty(config.CodigoEstablecimiento))
{
logger.LogError("StayExportProviderSpainParteViajeros - Config error: CodigoEstablecimiento is not configured!");
return (fileResult: StayExportFileResult.ExportError, exportDetails: null);
}
if (!stayList.Any())
{
return (fileResult: StayExportFileResult.NoData, exportDetails: null);
}
List<StayExportDetails> stayExportDetails = [];
ComunicacionValidator comunicacionValidator = new();
List<Comunicacion> comunicacionsList = [];
foreach (StayEntryDTO stay in stayList)
{
(Comunicacion? comunicacion, string errorMsg) = await GetComunicacionFromStay(stay);
if (comunicacion == null)
{
logger.LogWarning("StayExportProviderSpainParteViajeros - Stay (ID: {id}) can't be exported: Error: {code}", stay.Id, errorMsg);
stayExportDetails.Add(new(StayId: stay.Id ?? -1, ExportResult: ExportDetailsResult_Error, Details: errorMsg));
continue;
}
var errors = comunicacionValidator.Validate(comunicacion);
if (!errors.IsValid)
{
logger.LogWarning("StayExportProviderSpainParteViajeros - Stay (ID: {id}) is invalid: Errors: {errors}", stay.Id, errors.Errors);
stayExportDetails.Add(new(StayId: stay.Id ?? -1, ExportResult: ExportDetailsResult_Error, Details: errors.ToString()));
continue;
}
comunicacionsList.Add(comunicacion);
logger.LogInformation("StayExportProviderSpainParteViajeros - Stay (ID: {id}) Exported OK", stay.Id);
stayExportDetails.Add(new(StayId: stay.Id ?? -1, ExportResult: ExportDetailsResult_Ok, Details: null));
}
Peticion peticion = new()
{
solicitud = new()
{
codigoEstablecimiento = config.CodigoEstablecimiento,
comunicacion = comunicacionsList,
}
};
//Create XmlSerializerNamespaces to add prefix-namespace pairs.
var docNamespaces = new XmlSerializerNamespaces();
docNamespaces.Add(XmlNamespace_Peticion_Prefix, XmlNamespace_Peticion);
XmlSerializer serializer = new(typeof(Peticion));
serializer.Serialize(stream: dataStream, o: peticion, namespaces: docNamespaces);
string details = JsonSerializer.Serialize(stayExportDetails);
return (fileResult: StayExportFileResult.OK, exportDetails: details);
}
#region Internal Methods
private async Task<(Comunicacion? comunicacion, string errorMsg)> GetComunicacionFromStay(StayEntryDTO stay)
{
Debug.Assert(stay.Id != null
&& stay.DateIn != null
&& stay.DateOut != null
&& stay.GuestDocuments != null
);
//Call Fn to validate Stay.
Contrato contrato = new()
{
referencia = stay.Id.Value.ToString(),
fechaContrato = stay.DateIn.Value.Date,
fechaEntrada = stay.DateIn.Value,
fechaSalida = stay.DateOut.Value,
internet = config.InternetAvailable,
numHabitaciones = stay.RoomCount ?? -1,
numPersonas = stay.GuestDocuments.Count,
pago = new()
{
tipoPago = GetTipoPagoFromStay(stay)!
}
};
List<Persona> personasList = [];
foreach (GuestDocumentDTO doc in stay.GuestDocuments)
{
Direccion dir = new()
{
direccion = doc.Address!,
codigoPostal = doc.ZipCode!,
pais = doc.AddressCountry!,
};
//Get Municipio given current country.
if (doc.AddressCountry != PersonDireccionPais_ESP)
{
List<string?> addressParts = [doc.AddressCity, doc.AddressProvince, doc.AddressState];
dir.nombreMunicipio = string.Join(", ", addressParts.Where(p => !string.IsNullOrEmpty(p)));
}
if (doc.AddressCountry == PersonDireccionPais_ESP)
{
dir.codigoMunicipio = await GetMunicipioFromGuestAddressAsync(doc);
if (dir.codigoMunicipio == null)
{
return (
comunicacion: null,
errorMsg: $"Can't get codigo municipio for {doc.AddressProvince}-{doc.AddressCity} in doc {doc.FullDocument}"
);
}
}
Persona persona = new()
{
rol = PersonRole,
nombre = doc.Name!,
apellido1 = doc.Surname!,
apellido2 = doc.Surname2,
tipoDocumento = GetTipoDocumentoFromGuest(doc)!,
numeroDocumento = doc.DocumentNum!,
soporteDocumento = doc.SupportNum,
fechaNacimiento = doc.BirthDate?.Date ?? DateTime.MaxValue,
nacionalidad = doc.Nationality!,
sexo = GetSexoFromGuest(doc)!,
telefono = doc.PhoneNumber,
correo = doc.EmailAddress,
parentesco = PersonRelation,
direccion = dir,
};
personasList.Add(persona);
}
return (
comunicacion: new()
{
contrato = contrato,
persona = personasList
},
errorMsg: "OK");
}
private static string? GetTipoDocumentoFromGuest(GuestDocumentDTO doc)
{
return doc.DocumentType switch
{
GuestConsts.DocumentType_DNI => PersonTipoDoc_NIF,
GuestConsts.DocumentType_NIE => PersonTipoDoc_NIE,
GuestConsts.DocumentType_PASSPORT => PersonTipoDoc_PAS,
null => null,
_ => PersonTipoDoc_OTRO
};
}
private static string? GetSexoFromGuest(GuestDocumentDTO doc)
{
return doc.Gender switch
{
GuestConsts.GenderOptions_MALE => "H",
GuestConsts.GenderOptions_FEMALE => "M",
null => null,
_ => "O"
};
}
private static string? GetTipoPagoFromStay(StayEntryDTO stay)
{
return stay.PaymentMethod;
}
private async Task<string?> GetMunicipioFromGuestAddressAsync(GuestDocumentDTO doc)
{
if (string.IsNullOrEmpty(doc.AddressCity) || string.IsNullOrEmpty(doc.AddressProvince))
{
logger.LogWarning("Found doc ({docNum}) without full address", doc.FullDocument);
return null;
}
//Get codMunicipio from API.
return await codigoMunicipioService.GetCodigoMunicipioAsync(doc.AddressCity, doc.AddressProvince);
}
#endregion
#region Validator
private sealed class ComunicacionValidator : AbstractValidator<Comunicacion>
{
//TODO: Validate values in list for: Pago.tipoPago, Persona.sexo, Persona.tipoDocumento
public ComunicacionValidator()
{
RuleFor(comm => comm.contrato)
.NotNull()
.ChildRules(contrato =>
{
contrato.RuleFor(p => p.referencia)
.NotEmpty()
.MaximumLength(50);
contrato.RuleFor(p => p.fechaEntrada)
.LessThan(p => p.fechaSalida);
contrato.RuleFor(p => p.fechaSalida)
.GreaterThan(p => p.fechaEntrada);
contrato.RuleFor(p => p.numHabitaciones)
.GreaterThanOrEqualTo(0);
//BLOQUE PAGO
contrato.RuleFor(p => p.pago)
.ChildRules(e =>
{
e.RuleFor(p => p.tipoPago)
.NotEmpty();
});
});
RuleForEach(comm => comm.persona)
.ChildRules(person =>
{
person.RuleFor(p => p.rol)
.Equal(PersonRole);
person.RuleFor(p => p.nombre)
.NotEmpty()
.MaximumLength(50);
person.RuleFor(p => p.apellido1)
.NotEmpty()
.MaximumLength(50);
person.RuleFor(p => p.apellido2)
.MaximumLength(50);
person.RuleFor(p => p.tipoDocumento)
.NotEmpty()
.MaximumLength(5);
person.RuleFor(p => p.numeroDocumento)
.NotEmpty()
.MaximumLength(15);
person.RuleFor(p => p.soporteDocumento)
.NotEmpty()
.When(p => p.tipoDocumento == PersonTipoDoc_NIF || p.tipoDocumento == PersonTipoDoc_NIE);
person.RuleFor(p => p.fechaNacimiento)
.NotEmpty();
person.RuleFor(p => p.sexo)
.MaximumLength(1);
person.RuleFor(p => p.telefono)
.MaximumLength(20);
person.RuleFor(p => p.correo)
.MaximumLength(250);
person.RuleFor(p => p.direccion)
.ChildRules(d =>
{
d.RuleFor(p => p.direccion)
.NotEmpty()
.MaximumLength(100);
d.RuleFor(p => p.codigoMunicipio)
.NotEmpty()
.When(p => p.pais == PersonDireccionPais_ESP)
.MaximumLength(5);
d.RuleFor(p => p.nombreMunicipio)
.NotEmpty()
.When(p => p.pais != PersonDireccionPais_ESP)
.MaximumLength(100);
d.RuleFor(p => p.codigoPostal)
.NotEmpty()
.MaximumLength(20);
d.RuleFor(p => p.pais)
.NotEmpty()
.Length(3);
});
});
RuleForEach(comm => comm.persona)
.Must(p => !string.IsNullOrEmpty(p.telefono) || !string.IsNullOrEmpty(p.correo))
.WithMessage($"It's required to have at least one of the following fields: '{nameof(Persona.telefono)}', '{nameof(Persona.correo)}'");
}
}
#endregion
#region Export Data Classes
private record StayExportDetails(long StayId, string ExportResult, string? Details);
[XmlRoot(ElementName = "peticion", Namespace = XmlNamespace_Peticion)]
public class Peticion
{
[XmlElement(Namespace = "")]
public required Solicitud solicitud;
}
[XmlRoot(ElementName = "solicitud")]
public class Solicitud
{
public required string codigoEstablecimiento;
[XmlElement(ElementName = "comunicacion")]
public required List<Comunicacion> comunicacion;
}
public class Comunicacion
{
[XmlElement(ElementName = "contrato")]
public required Contrato contrato;
[XmlElement(ElementName = "persona")]
public required List<Persona> persona;
}
public class Contrato
{
public required string referencia;
[XmlElement(DataType = "date")]
public required DateTime fechaContrato;
[XmlElement(DataType = "dateTime")]
public required DateTime fechaEntrada;
[XmlElement(DataType = "dateTime")]
public required DateTime fechaSalida;
public required int numPersonas;
public int? numHabitaciones;
public bool? internet;
//BLOQUE PAGO
public required Pago pago;
}
public class Persona
{
public required string rol;
public required string nombre;
public required string apellido1;
public string? apellido2;
public required string tipoDocumento;
public required string numeroDocumento;
public string? soporteDocumento;
[XmlElement(DataType = "date")]
public required DateTime fechaNacimiento;
public required string nacionalidad;
public required string sexo;
// BLOQUE DIRECCION
public required Direccion direccion;
public string? telefono;
public string? correo;
public required string parentesco;
}
public class Pago
{
public required string tipoPago;
// [XmlElement(DataType = "date")]
// public required DateTime fechaPago;
public string? medioPago;
public string? titular;
public string? caducidadTarjeta;
}
public class Direccion
{
public required string direccion;
public string? codigoMunicipio;
public string? nombreMunicipio;
public required string codigoPostal;
public required string pais;
}
#endregion
}
@@ -0,0 +1,247 @@
using System.Diagnostics;
using FluentStorage;
using FluentStorage.Storage;
using Microsoft.Extensions.Options;
using Somnia.GuestLog.Enums.Models;
using Somnia.GuestLog.Enums.Services;
using Somnia.GuestLog.Models.Configuration;
using Somnia.GuestLog.Models.DTOs;
using Somnia.GuestLog.Models.QueryConfigs;
using Somnia.GuestLog.Services.Entity;
namespace Somnia.GuestLog.Services.StayExport;
public class StayExportService(
ILogger<StayExportProviderJson> logger,
IOptions<StayExportConfig> exportConfig,
IStayExportFileEntityService stayExportEntityService,
IStayEntryEntityService stayEntityService,
IStayExportFormatProviderFactory providerFactory
) : IStayExportService
{
private static readonly StayEntryQueryConfig queryConfig =
new(IncludeGuests: true, UseSplitQueries: true, DisableTracking: true);
private readonly StayExportConfig config = exportConfig.Value;
public async Task<Result<StayExportFileDTO>> ExportStaysByParamsAsync(StayExportFormat format, StayExportParamsDTO exportParams)
{
//Get data to export.
IEnumerable<StayEntryDTO>? stayList;
try
{
stayList = await GetStaysToExportByParamsAsync(exportParams);
}
catch (Exception ex)
{
logger.LogError(ex, "JsonStayExportService - ExportStaysByParams: Error retriving stays to export!");
return Result<StayExportFileDTO>.Fail(ResultCode.UnknownError);
}
//Create initial StayExportFile
DateTime exportTime = DateTime.UtcNow;
StayExportFileDTO stayExport = new()
{
ExportFormat = format,
ExportParams = exportParams,
ExportTime = exportTime,
};
//Export data.
using Stream exportedDataStream = new MemoryStream();
//Generate data to export.
StayExportFileResult exportResult;
string? exportFilename = null;
try
{
//Create Format provider and export data to stream.
var provider = providerFactory.GetProvider(format);
(exportResult, stayExport.ExportDetails) = await provider.ExportDataAsync(stayList, exportedDataStream);
// "Rewind" stream so it can be read from the begining when uploading.
exportedDataStream.Seek(0, SeekOrigin.Begin);
exportFilename = provider.GetFilename(exportTime);
}
catch (Exception ex)
{
logger.LogError(ex, "JsonStayExportService - ExportStaysByParams: Error exporting data!");
exportResult = StayExportFileResult.ExportError;
}
//If data exported OK, save to file.
if (exportResult == StayExportFileResult.OK)
{
try
{
Debug.Assert(exportFilename != null);
string exportPathSubdir = "";
if (config.UseSubdirectoryByFormat)
{
exportPathSubdir = Enum.GetName(format) ?? "";
}
string exportPath = Path.Combine(exportPathSubdir, exportFilename);
//Store data.
using IStore exportStore = GetExportStore();
await exportStore.SetObject(objectPath: exportPath, dataStream: exportedDataStream);
//Update path in item.
stayExport.ExportedPath = exportPath;
}
catch (Exception ex)
{
logger.LogError(ex, "JsonStayExportService - ExportStaysByParams: Error Uploading exported data!");
exportResult = StayExportFileResult.UploadError;
}
}
//Store StayExport in DB.
stayExport.ExportResult = exportResult;
Result? createResult = null;
try
{
createResult = await stayExportEntityService.CreateStayExportAsync(stayExport);
}
catch (Exception ex)
{
logger.LogError(ex, "JsonStayExportService - ExportStaysByParams: Error storing StayExportFile entity to DB: {stayExport}", stayExport);
}
//If everything is OK, return OK.
if (createResult != null && createResult.ResultCode == ResultCode.Ok)
{
return Result<StayExportFileDTO>.Ok(stayExport);
}
//Saving export to DB failed. Delete exported file (if exported)
if (!string.IsNullOrEmpty(stayExport.ExportedPath))
{
try
{
using IStore exportStore = GetExportStore();
await exportStore.DeleteObject(objectPath: stayExport.ExportedPath);
}
catch (Exception ex)
{
logger.LogError(ex, "JsonStayExportService - ExportStaysByParams: Error deleting exported file after db saving error: {exportedPath}", stayExport.ExportedPath);
}
}
logger.LogError(
"JsonStayExportService - ExportStaysByDateIn: Error (code '{code}') storing StayExportFile entity to DB: {stayExport}",
createResult?.ResultCode, stayExport);
return Result<StayExportFileDTO>.Fail(ResultCode.UnknownError);
}
public async Task<Result> GetExportedFileAsync(StayExportFileDTO item, Stream dataStream)
{
if (string.IsNullOrEmpty(item.ExportedPath))
{
logger.LogWarning("Trying to get exported file without path!! Exported file can't be loaded!");
return Result.Fail(ResultCode.NotFound);
}
try
{
IStore store = GetExportStore();
await store.GetObject(item.ExportedPath, dataStream);
}
catch (Exception ex)
{
logger.LogError(ex, "Error getting exported file '{id}' from path '{path}'", item.Id, item.ExportedPath);
return Result.FailUnknownError();
}
return Result.Ok();
}
public async Task<Result> DeleteExportedFileAsync(StayExportFileDTO item)
{
//Try to delete existing file.
if (!string.IsNullOrEmpty(item.ExportedPath))
{
bool fileDeletedFromStore = await DeleteExportFileFromStoreAsync(item);
if (!fileDeletedFromStore)
{
return Result.Fail(ResultCode.FileStorageError);
}
}
//Delete entry from DB.
Result deleteResult = await stayExportEntityService.DeleteStayExportAsync(item);
if (!deleteResult.IsSuccess)
{
return Result.Fail(deleteResult.ResultCode);
}
return Result.Ok();
}
#region Internal Methods
private async Task<IEnumerable<StayEntryDTO>> GetStaysToExportByParamsAsync(StayExportParamsDTO exportParams)
{
//Todo - Improve type selection (when there are more types).
if (exportParams.ExportType == StayExportParamType.PeriodDateIn)
{
if (exportParams.ExportPeriodStart == null || exportParams.ExportPeriodEnd == null)
{
throw new InvalidOperationException($"StayExportFile - ExportType {exportParams.ExportType} require date start/end. Null dates found");
}
//Get Stays in period -> Local time should be used so it match with entities local-time.
StayEntryFilterDTO filter = new()
{
DateInStart = exportParams.ExportPeriodStart,
DateInEnd = exportParams.ExportPeriodEnd
};
return await stayEntityService.GetDtoListByFilterAsync(filter, limit: 0, queryConfig: queryConfig);
}
throw new InvalidOperationException($"StayExportFile - Error getting stays using params: export type invalid: {exportParams.ExportType}");
}
private async Task<bool> DeleteExportFileFromStoreAsync(StayExportFileDTO item)
{
Debug.Assert(!string.IsNullOrEmpty(item.ExportedPath));
try
{
IStore store = GetExportStore();
bool objectExist = await store.ObjectExists(item.ExportedPath);
if (!objectExist)
{
logger.LogWarning("StayExportService - File for item {id} not found in store! Skip delete!", item.Id);
return true;
}
await store.DeleteObject(item.ExportedPath);
logger.LogInformation("StayExportService - File for item {id} deleted from path {path}", item.Id, item.ExportedPath);
return true;
}
catch (Exception ex)
{
logger.LogError(ex, "StayExportService - Error deleting export file of item {id}", item.Id);
return false;
}
}
private IStore GetExportStore()
{
string? storageConnString = config.ExportStorageConnectionString;
if (string.IsNullOrEmpty(storageConnString))
{
throw new InvalidOperationException("No Stay Export Storage configured. Stays can't be exported");
}
return StorageFactory.FromConnectionString(storageConnString);
}
#endregion
}
@@ -0,0 +1,78 @@
using MudBlazor;
using Somnia.GuestLog.Enums.Services;
using Somnia.GuestLog.Enums.UI;
using Somnia.GuestLog.Models.DTOs;
using IResult = Somnia.GuestLog.Models.DTOs.IResult;
namespace Somnia.GuestLog.Services;
public class UserMessageService(
ILocalizerService Localizer,
ISnackbar snackbar
) : IUserMessageService
{
public void ShowMessage(string text, MessageKind kind, params object[] parameters)
{
string formatedMsg = GetFormatedMessage(text, parameters);
ShowSnackbar(formatedMsg, kind);
}
public void ShowResult(IResult resultDto)
{
ShowResultWithMessage(resultDto, message: null);
}
public void ShowResultWithMessage(IResult resultDto, string? message)
{
string resultMsg = GetMsgForResultCode(resultDto.ResultCode);
resultMsg = GetFormatedMessage(resultMsg, resultDto.Context);
string finalMsg = message is not null ? $"{message.Trim().TrimEnd(':')}: {resultMsg}" : resultMsg;
//Show success when OK, error for UnknownErrors and warning otherwise.
MessageKind kind = resultDto switch
{
{ IsSuccess: true } => MessageKind.Success,
{ ResultCode: ResultCode.UnknownError } => MessageKind.Error,
_ => MessageKind.Warning
};
ShowSnackbar(finalMsg, kind);
}
#region Internal Methods
private void ShowSnackbar(string msg, MessageKind kind)
{
snackbar.Add(message: msg, severity: GetSnackbarSeveryForMessageKind(kind));
}
private string GetMsgForResultCode(ResultCode resultCode)
{
return Localizer.GetEnumValue(resultCode);
}
private static string GetFormatedMessage(string template, params object[]? parameters)
{
if (parameters is null || parameters.Length == 0)
{
return template;
}
return string.Format(template, parameters);
}
private static MudBlazor.Severity GetSnackbarSeveryForMessageKind(MessageKind kind) => kind switch
{
MessageKind.Info => MudBlazor.Severity.Info,
MessageKind.Warning => MudBlazor.Severity.Warning,
MessageKind.Error => MudBlazor.Severity.Error,
MessageKind.Success => MudBlazor.Severity.Success,
MessageKind.Normal => MudBlazor.Severity.Normal,
_ => throw new ArgumentOutOfRangeException(nameof(kind), $"Not expected MessageKind value: {kind}")
};
#endregion
}
@@ -7,9 +7,24 @@
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
</PropertyGroup>
<PropertyGroup>
<PublishProfile>DefaultContainer</PublishProfile>
<ContainerImageFormat>OCI</ContainerImageFormat>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentStorage" />
<PackageReference Include="FluentStorage.AWS" />
<PackageReference Include="FluentValidation" />
<PackageReference Include="MudBlazor" />
<PackageReference Include="MudBlazor.Translations" />
<PackageReference Include="Serilog" />
<PackageReference Include="Serilog.Enrichers.Process" />
<PackageReference Include="Serilog.Enrichers.Thread" />
<PackageReference Include="Serilog.Extensions.Hosting" />
<PackageReference Include="Serilog.Settings.Configuration" />
<PackageReference Include="Serilog.Sinks.Console" />
<PackageReference Include="Serilog.Sinks.File" />
</ItemGroup>
<ItemGroup>
@@ -24,6 +39,7 @@
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
</ItemGroup>
@@ -8,4 +8,9 @@ public static class QueryExtensions
{
return condition ? source.Where(predicate) : source;
}
public static IQueryable<T> WithLimit<T>(this IQueryable<T> source, int limit)
{
return limit > 0 ? source.Take(limit) : source;
}
}
+3 -1
View File
@@ -11,7 +11,9 @@ public static partial class RegexUtils
public static partial Regex RegexNieFormat();
[GeneratedRegex("^[A-Z]{3}[0-9]{6}$")]
public static partial Regex RegexGuestDocSuportNumber();
public static partial Regex RegexGuestDocSupportNumberDNI();
[GeneratedRegex("^E[0-9]{8}$")]
public static partial Regex RegexGuestDocSupportNumberNIE();
[GeneratedRegex(@"^\+?\d{1,4}?[-.\s]?\(?\d{1,3}?\)?[-.\s]?\d{1,4}[-.\s]?\d{1,4}[-.\s]?\d{1,9}$")]
public static partial Regex RegexPhoneNumber();
@@ -22,14 +22,22 @@ public class GuestDocumentDTOValidator : ValueValidator<GuestDocumentDTO>
.Length(6, 32);
RuleFor(x => x.IssueDate)
.Cascade(CascadeMode.Stop)
.NotEmpty()
.LessThanOrEqualTo(DateTime.Today);
.When(x => x.Nationality != GuestConsts.CountryCode_ESP)
.LessThanOrEqualTo(x => DateTime.Today);
RuleFor(x => x.SupportNum)
.Cascade(CascadeMode.Stop)
.NotEmpty()
.Matches(RegexUtils.RegexGuestDocSuportNumber()).WithMessage("Invalid Format. Expected 'AAA000000'.")
.When(x => x.Nationality == GuestConsts.CountryCode_ESP);
.Matches(RegexUtils.RegexGuestDocSupportNumberDNI()).WithMessage("Invalid Format for DNI. Expected 'AAA000000'.")
.When(x => x.DocumentType == GuestConsts.DocumentType_DNI);
RuleFor(x => x.SupportNum)
.Cascade(CascadeMode.Stop)
.NotEmpty()
.Matches(RegexUtils.RegexGuestDocSupportNumberNIE()).WithMessage("Invalid Format for NIE. Expected 'E00000000'.")
.When(x => x.DocumentType == GuestConsts.DocumentType_NIE);
RuleFor(x => x.Name)
.NotEmpty()
@@ -41,7 +49,7 @@ public class GuestDocumentDTOValidator : ValueValidator<GuestDocumentDTO>
RuleFor(x => x.BirthDate)
.NotEmpty()
.LessThanOrEqualTo(DateTime.Today);
.LessThanOrEqualTo(x => DateTime.Today);
RuleFor(x => x.Nationality)
.Cascade(CascadeMode.Stop)
@@ -58,6 +66,12 @@ public class GuestDocumentDTOValidator : ValueValidator<GuestDocumentDTO>
.Matches(RegexUtils.RegexPhoneNumber()).WithMessage("Invalid format")
.When(x => !string.IsNullOrEmpty(x.PhoneNumber));
// Phone is required if mail is empty.
RuleFor(x => x.PhoneNumber)
.NotNull()
.When(x => string.IsNullOrEmpty(x.EmailAddress))
.WithMessage("At least 1 contact field (phone or email) is required!");
RuleFor(x => x.AddressCountry)
.NotEmpty()
.Must(val => val != null && dataListService.IsValueInList(GuestConsts.DataList_CountryCodes, val));
@@ -10,11 +10,11 @@ public class StayEntryDTOValidator : ValueValidator<StayEntryDTO>
{
RuleFor(e => e.DateIn)
.NotEmpty()
.LessThanOrEqualTo(DateTime.Today.AddDays(1).AddSeconds(-1));
.LessThanOrEqualTo(x => DateTime.Today.AddDays(1).AddSeconds(-1));
RuleFor(e => e.DateOut)
.NotEmpty()
.GreaterThan(DateTime.Today);
.GreaterThan(e => e.DateIn);
RuleFor(e => e.RoomCount)
.NotEmpty()
@@ -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();
}
}
@@ -0,0 +1,23 @@
using FluentValidation;
using Somnia.GuestLog.Enums.Models;
using Somnia.GuestLog.Models.DTOs;
namespace Somnia.GuestLog.Validators;
public class StayExportParamsDTOValidator : ValueValidator<StayExportParamsDTO>
{
public StayExportParamsDTOValidator()
{
RuleFor(e => e.ExportType)
.NotEmpty();
RuleFor(e => e.ExportPeriodStart)
.NotEmpty()
.When(e => e.ExportType == StayExportParamType.PeriodDateIn);
RuleFor(e => e.ExportPeriodEnd)
.NotEmpty()
.When(e => e.ExportType == StayExportParamType.PeriodDateIn);
}
}

Some files were not shown because too many files have changed in this diff Show More