Compare commits

...
12 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
48 changed files with 1885 additions and 153 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 -10
View File
@@ -6,26 +6,34 @@
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Extensions.MudBlazor.StaticInput" Version="4.1.0" />
<PackageVersion Include="FluentStorage" Version="8.0.16" />
<PackageVersion Include="FluentStorage.AWS" Version="8.0.10" />
<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="Microsoft.EntityFrameworkCore.Proxies" 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
}
@@ -17,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"
@@ -81,6 +90,9 @@
private List<AddressResultDTO> addressResults = [];
private AddressResultDTO? SelectedAddressResult { get; set; }
private bool _isDataLoading = false;
private string _lastSearchText = "";
private void Cancel()
{
@@ -99,16 +111,20 @@
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)
{
@@ -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
}
@@ -21,12 +21,14 @@
<MudDivider DividerType="DividerType.Middle" Class="mb-4" />
<MudStack Row style="@FieldMarginY">
<MudDatePicker @ref="_datePickerDateIn" @bind-Date="Model.DateIn" For="@(() => Model.DateIn)"
<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 @ref="_datePickerDateOut" @bind-Date="Model.DateOut" For="@(() => Model.DateOut)"
<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
@@ -125,8 +127,6 @@
private MudForm _form = null!;
private MudDatePicker _datePickerDateIn = null!;
private MudDatePicker _datePickerDateOut = null!;
private const string FieldMarginY = "margin-block: 0.5em";
private IEnumerable<string> PaymentMethodSet
@@ -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
@@ -9,6 +9,7 @@
@inject NavigationManager Navigation
@inject IGuestDocumentEntityService guestDocService
@inject IStayEntryEntityService stayService
@inject IDialogService DialogService
<PageTitle>Somnia - Guest Docs - Details</PageTitle>
@@ -16,14 +17,28 @@
<MudStack Row="true" Class="my-4">
<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)
@@ -32,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>
@@ -87,8 +103,16 @@
IEnumerable<StayEntryDTO> guestStayList = [];
bool guestStaysLoading = true;
private bool _isEditMode = false;
protected override async Task OnInitializedAsync()
{
//Get model from db.
await LoadGuestDocumentAsync();
}
private async Task LoadGuestDocumentAsync()
{
//Get model from db.
documentModel = await guestDocService.GetLastVersionDtoByIdAsync(docId);
@@ -121,9 +145,68 @@
#region Event Handlers
private void BtnEdit()
private void BtnEnableEdit()
{
userMessage.ShowMessage("Not implemented yet!", MessageKind.Info);
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()
@@ -43,8 +43,9 @@
</MudStack>
</ToolBarContent>
<Columns>
<PropertyColumn Property="x => x.Id" Title="@LanguageLocale.dto_GuestDocument_Id" />
<PropertyColumn Property="x => x.Version" Title="@LanguageLocale.dto_GuestDocument_Version" />
<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" />
@@ -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
@@ -129,6 +132,8 @@
private async Task BtnSaveNewGuest()
{
Debug.Assert(currentPageMode == ePageMode.createNew && _form != null);
bool isFormValid = await _form.ValidateForm();
if (!isFormValid)
{
@@ -144,13 +149,13 @@
Result createResult = await guestDocService.CreateDocumentAsync(model);
if (!createResult.IsSuccess)
{
string errorMsg = $"Error creating Document '{model.DocumentNum}'";
string errorMsg = $"Error creating Document '{model.FullDocument}'";
userMessage.ShowResultWithMessage(createResult, errorMsg);
loadingDialog.Close();
return;
}
userMessage.ShowMessage($"Document '{model.DocumentNum}' Created!", MessageKind.Success);
userMessage.ShowMessage($"Document '{model.FullDocument}' Created!", MessageKind.Success);
Navigation.NavigateTo(GuestConsts.Page_GuestDocs_List);
}
@@ -60,7 +60,7 @@
return;
}
userMessage.ShowMessage($"StayEntry Created!", MessageKind.Success);
userMessage.ShowMessage("StayEntry Created: (Id: {0})", MessageKind.Success, createResult.Context ?? ["???"]);
Navigation.NavigateTo(GuestConsts.Page_StayEntry_List);
}
@@ -52,8 +52,13 @@
</TemplateColumn>
<PropertyColumn Property="x => Localizer.GetEnumValue(x.ExportResult)"
Title="@LanguageLocale.dto_StayExportFile_ExportResult" />
<PropertyColumn Property="x => x.ExportedPath" Title="@LanguageLocale.dto_StayExportFile_ExportedPath" Hidden />
<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">
@@ -118,7 +123,21 @@
{
string jsonParams = JsonSerializer.Serialize(dto.ExportParams, options: new() { WriteIndented = true });
await DialogService.ShowMessageBoxAsync("Details", jsonParams);
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)
@@ -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"
}
}
]
}
}
@@ -6,5 +6,7 @@ public static class DbConsts
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";
}
@@ -25,6 +25,9 @@ public class StayExportFileEntityConfig : IEntityTypeConfiguration<StayExportFil
builder.Property(e => e.ExportedPath)
.HasMaxLength(256);
builder.Property(e => e.ExportDetails)
.HasColumnType(DbConsts.DbType_jsonb);
builder.Property(e => e.ExportTime)
.IsRequired()
@@ -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);
}
}
}
@@ -82,7 +82,7 @@ namespace Somnia.GuestLog.Infrastructure.Migrations
b.Property<bool>("IsLastVersion")
.HasColumnType("boolean");
b.Property<DateOnly>("IssueDate")
b.Property<DateOnly?>("IssueDate")
.HasColumnType("date");
b.Property<string>("Name")
@@ -193,6 +193,9 @@ namespace Somnia.GuestLog.Infrastructure.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("ExportDetails")
.HasColumnType("jsonb");
b.Property<int>("ExportFormat")
.HasColumnType("integer");
@@ -16,6 +16,8 @@ public sealed record Result(
) : 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)
@@ -14,6 +14,8 @@ public class StayExportFileDTO
public string? ExportedPath { get; set; }
public string? ExportDetails { get; set; }
public DateTime? ExportTime { get; set; }
}
@@ -8,7 +8,7 @@ public class GuestDocument : BaseEntity
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; }
@@ -33,4 +33,5 @@ public class GuestDocument : BaseEntity
public override string GetFullKey() => $"{Id}-{Version}";
public string FullDocument => $"{DocumentType}-{DocumentNum}";
}
@@ -17,6 +17,11 @@ public class StayExportFile : BaseEntity
/// </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.
+7 -6
View File
@@ -14,7 +14,7 @@ internal static class EntityMapping
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,
@@ -30,8 +30,8 @@ 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(),
};
}
@@ -45,8 +45,8 @@ internal static class EntityMapping
RoomCount = entity.RoomCount,
RoomNumber = entity.RoomNumber,
PaymentMethod = entity.PaymentMethod,
CreateTime = entity.CreateTime,
UpdateTime = entity.UpdateTime,
CreateTime = entity.CreateTime.ToLocalTime(),
UpdateTime = entity.UpdateTime.ToLocalTime(),
GuestDocuments = includeGuests ? [.. entity.GuestDocuments.Select(e => MapGuestDocumentEntityToDTO(e))] : null,
};
}
@@ -67,7 +67,8 @@ internal static class EntityMapping
ExportParams = exportParamsDto,
ExportResult = entity.ExportResult,
ExportedPath = entity.ExportedPath,
ExportTime = entity.ExportTime,
ExportDetails = entity.ExportDetails,
ExportTime = entity.ExportTime.ToLocalTime(),
};
}
+52
View File
@@ -1,8 +1,15 @@
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;
@@ -17,6 +24,34 @@ 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 =>
@@ -50,6 +85,7 @@ builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddLocalization();
builder.Services.AddMudTranslations();
builder.Services.AddHttpClient();
// Add Validation Services
@@ -84,6 +120,8 @@ builder.Services.AddOptions<StayExportSpainParteViajerosConfig>()
var app = builder.Build();
app.UseForwardedHeaders();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
@@ -115,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>
@@ -154,6 +154,7 @@
<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>
@@ -181,6 +182,10 @@
<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>
@@ -223,6 +228,11 @@
<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>
@@ -44,7 +44,7 @@ public class GuestDocumentEntityService(
Version = GuestConsts.GuestDocument_FirstVersion,
DocumentType = dto.DocumentType!,
DocumentNum = dto.DocumentNum!,
IssueDate = DateOnly.FromDateTime(dto.IssueDate!.Value),
IssueDate = dto.IssueDate != null ? DateOnly.FromDateTime(dto.IssueDate!.Value) : null,
SupportNum = dto.SupportNum,
Nationality = dto.Nationality!,
Name = dto.Name!,
@@ -76,9 +76,77 @@ public class GuestDocumentEntityService(
}
}
public Task<Result> UpdateDocumentAsync(GuestDocumentDTO dto)
public async Task<Result> UpdateDocumentAsync(GuestDocumentDTO dto)
{
throw new NotImplementedException();
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)
@@ -56,7 +56,7 @@ public class StayEntryEntityService(
if (isInOtherStay)
{
string period = $"{dbStay.DateIn}-{dbStay.DateOut}";
return Result.FailWithContext(ResultCode.GuestCurrentlyInStay, dbGuest.GetFullKey(), period);
return Result.FailWithContext(ResultCode.GuestCurrentlyInStay, dbGuest.FullDocument, period);
}
//Add dbGuest to dbStay.
@@ -67,7 +67,7 @@ public class StayEntryEntityService(
await dbContext.SaveChangesAsync();
logger.LogInformation("StayEntryService - CreateEntryAsync: StayEntry Created: '{id}'", dbStay.GetFullKey());
return Result.Ok();
return Result.OkWithContext(dbStay.GetFullKey());
}
catch (Exception ex)
{
@@ -155,7 +155,8 @@ public class StayEntryEntityService(
query = query.WhereIf(!string.IsNullOrEmpty(filter.PaymentMethod),
e => e.PaymentMethod == filter.PaymentMethod);
query = query.OrderByDescending(e => e.DateIn);
query = query.OrderByDescending(e => e.DateIn)
.ThenByDescending(e => e.UpdateTime);
if (limit > 0)
{
@@ -36,6 +36,7 @@ public class StayExportFileEntityService(
ExportParams = exportParams,
ExportResult = dto.ExportResult.Value,
ExportedPath = dto.ExportedPath,
ExportDetails = dto.ExportDetails,
ExportTime = dto.ExportTime?.ToUniversalTime() ?? DateTime.UtcNow,
};
@@ -45,12 +46,14 @@ public class StayExportFileEntityService(
await dbContext.AddAsync(dbItem);
await dbContext.SaveChangesAsync();
logger.LogInformation("StayExportFileService - CreateStayExportAsync: GuestDocument Created: '{id}'", dbItem.GetFullKey());
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 GuestDocument");
logger.LogError(ex, "StayExportFileService - CreateStayExportAsync: Error creating StayExport");
return Result.FailUnknownError();
}
}
@@ -82,7 +85,7 @@ public class StayExportFileEntityService(
}
catch (Exception ex)
{
logger.LogError(ex, "StayExportFileService - DeleteStayExportAsync: Error creating GuestDocument");
logger.LogError(ex, "StayExportFileService - DeleteStayExportAsync: Error deleting StayExport");
return Result.FailUnknownError();
}
}
@@ -63,7 +63,7 @@ public class SpainCodigoMunicipioCatastroService(
return null;
}
string finalCode = $"{codIne.codigoProvincia}{codIne.codigoMunicipio}";
string finalCode = $"{codIne.codigoProvincia.PadLeft(2, '0')}{codIne.codigoMunicipio.PadLeft(3, '0')}";
codigoMunicipioCache.Add(cacheKey, finalCode);
return finalCode;
@@ -14,6 +14,6 @@ public interface IStayExportFormatProvider
/// </summary>
/// <param name="stayList">List of Stays to export</param>
/// <param name="dataStream">Stream to copy exported data.</param>
public Task<StayExportFileResult> ExportDataAsync(IEnumerable<StayEntryDTO> stayList, Stream dataStream);
public Task<(StayExportFileResult fileResult, string? exportDetails)> ExportDataAsync(IEnumerable<StayEntryDTO> stayList, Stream dataStream);
}
@@ -13,15 +13,18 @@ public class StayExportProviderJson : IStayExportFormatProvider
=> $"{Enum.GetName(GetFormat())}_{exportTime.ToFileTimeUtc()}.{fileExtension}";
public async Task<StayExportFileResult> ExportDataAsync(IEnumerable<StayEntryDTO> stayList, Stream dataStream)
public async Task<(StayExportFileResult fileResult, string? exportDetails)> ExportDataAsync(IEnumerable<StayEntryDTO> stayList, Stream dataStream)
{
if (!stayList.Any())
{
return StayExportFileResult.NoData;
return (fileResult: StayExportFileResult.NoData, exportDetails: null);
}
// Convert to JSON.
await JsonSerializer.SerializeAsync(dataStream, stayList);
return StayExportFileResult.OK;
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);
}
}
@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Text.Json;
using System.Xml.Serialization;
using FluentValidation;
using Microsoft.Extensions.Options;
@@ -17,6 +18,9 @@ public class StayExportProviderSpainParteViajeros(
{
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";
@@ -28,35 +32,39 @@ public class StayExportProviderSpainParteViajeros(
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> ExportDataAsync(IEnumerable<StayEntryDTO> stayList, Stream dataStream)
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 StayExportFileResult.ExportError;
return (fileResult: StayExportFileResult.ExportError, exportDetails: null);
}
if (!stayList.Any())
{
return StayExportFileResult.NoData;
return (fileResult: StayExportFileResult.NoData, exportDetails: null);
}
List<StayExportDetails> stayExportDetails = [];
ComunicacionValidator comunicacionValidator = new();
List<Comunicacion> comunicacionsList = [];
foreach (StayEntryDTO stay in stayList)
{
(Comunicacion? comunicacion, string code) = await GetComunicacionFromStay(stay);
(Comunicacion? comunicacion, string errorMsg) = await GetComunicacionFromStay(stay);
if (comunicacion == null)
{
logger.LogWarning("StayExportProviderSpainParteViajeros - Stay (ID: {id}) can't be exported: Code: {code}", stay.Id, code);
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;
}
@@ -64,11 +72,13 @@ public class StayExportProviderSpainParteViajeros(
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()
@@ -80,15 +90,20 @@ public class StayExportProviderSpainParteViajeros(
}
};
XmlSerializer serializer = new(typeof(Peticion));
serializer.Serialize(dataStream, peticion);
//Create XmlSerializerNamespaces to add prefix-namespace pairs.
var docNamespaces = new XmlSerializerNamespaces();
docNamespaces.Add(XmlNamespace_Peticion_Prefix, XmlNamespace_Peticion);
return StayExportFileResult.OK;
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 code)> GetComunicacionFromStay(StayEntryDTO stay)
private async Task<(Comunicacion? comunicacion, string errorMsg)> GetComunicacionFromStay(StayEntryDTO stay)
{
Debug.Assert(stay.Id != null
&& stay.DateIn != null
@@ -136,7 +151,7 @@ public class StayExportProviderSpainParteViajeros(
{
return (
comunicacion: null,
code: "NoCodigoMunicipio"
errorMsg: $"Can't get codigo municipio for {doc.AddressProvince}-{doc.AddressCity} in doc {doc.FullDocument}"
);
}
}
@@ -167,7 +182,7 @@ public class StayExportProviderSpainParteViajeros(
contrato = contrato,
persona = personasList
},
code: "OK");
errorMsg: "OK");
}
private static string? GetTipoDocumentoFromGuest(GuestDocumentDTO doc)
@@ -187,7 +202,7 @@ public class StayExportProviderSpainParteViajeros(
return doc.Gender switch
{
GuestConsts.GenderOptions_MALE => "H",
GuestConsts.GenderOptions_FEMALE => "F",
GuestConsts.GenderOptions_FEMALE => "M",
null => null,
_ => "O"
};
@@ -202,6 +217,7 @@ public class StayExportProviderSpainParteViajeros(
{
if (string.IsNullOrEmpty(doc.AddressCity) || string.IsNullOrEmpty(doc.AddressProvince))
{
logger.LogWarning("Found doc ({docNum}) without full address", doc.FullDocument);
return null;
}
@@ -290,7 +306,7 @@ public class StayExportProviderSpainParteViajeros(
.NotEmpty()
.When(p => p.pais != PersonDireccionPais_ESP)
.MaximumLength(100);
d.RuleFor(p => p.codigoMunicipio)
d.RuleFor(p => p.codigoPostal)
.NotEmpty()
.MaximumLength(20);
d.RuleFor(p => p.pais)
@@ -309,9 +325,12 @@ public class StayExportProviderSpainParteViajeros(
#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;
}
@@ -319,17 +338,20 @@ public class StayExportProviderSpainParteViajeros(
public class Solicitud
{
public required string codigoEstablecimiento;
[XmlElement(ElementName = "comunicacion")]
public required List<Comunicacion> comunicacion;
}
[XmlRoot(ElementName = "comunicacion")]
public class Comunicacion
{
[XmlElement(ElementName = "contrato")]
public required Contrato contrato;
[XmlElement(ElementName = "persona")]
public required List<Persona> persona;
}
[XmlRoot(ElementName = "contrato")]
public class Contrato
{
public required string referencia;
@@ -351,7 +373,6 @@ public class StayExportProviderSpainParteViajeros(
public required Pago pago;
}
[XmlRoot(ElementName = "persona")]
public class Persona
{
public required string rol;
@@ -367,12 +388,13 @@ public class StayExportProviderSpainParteViajeros(
public required string nacionalidad;
public required string sexo;
// BLOQUE DIRECCION
public required Direccion direccion;
public string? telefono;
public string? correo;
public required string parentesco;
// BLOQUE DIRECCION
public required Direccion direccion;
}
public class Pago
@@ -58,7 +58,7 @@ public class StayExportService(
{
//Create Format provider and export data to stream.
var provider = providerFactory.GetProvider(format);
exportResult = await provider.ExportDataAsync(stayList, exportedDataStream);
(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);
@@ -195,7 +195,7 @@ public class StayExportService(
throw new InvalidOperationException($"StayExportFile - ExportType {exportParams.ExportType} require date start/end. Null dates found");
}
//Get Stays in period.
//Get Stays in period -> Local time should be used so it match with entities local-time.
StayEntryFilterDTO filter = new()
{
DateInStart = exportParams.ExportPeriodStart,
@@ -7,11 +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>
+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,7 +10,7 @@ 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()
@@ -4,5 +4,6 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
},
"RunDatabaseMigrationOnStartup": false
}
+2 -6
View File
@@ -2,12 +2,8 @@
"ConnectionStrings": {
"DbSomniaGuestLog": "Host=localhost;Port=5432;Username=postgres;Password=P@ssw0rd;Database=SomniaGuestLog"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"RunDatabaseMigrationOnStartup": true,
"DataProtectionDir": "/SomniaGuestLog/dataprotection",
"AllowedHosts": "*",
"AddressQuery": {
"EnableAddressQuery": true,
+6
View File
@@ -11,3 +11,9 @@ window.downloadFileFromStream = async (fileName, contentStreamReference) => {
anchorElement.remove();
URL.revokeObjectURL(url);
}
// Function to update Language cookie.
window.setBlazorCultureCookie = async (newCulture) => {
await window.cookieStore.set('.AspNetCore.Culture', newCulture);
console.log("Updated cookie to " + newCulture);
};