Compare commits

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

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

Note: DB changes require recreating db tables, so all data is lost and migration "rollback" is not allowed.
2026-09-01 15:04:38 +02:00
Daniel-Garmig a01eed82e1 GuestLog - Nominatim AddressQueryService fix for Provinces.
- Small fix in Nominatim AddressQueryService to map provinces using fields "state_district" and "county" in results.
2026-09-01 11:01:05 +02:00
Daniel-Garmig b08aaf2d7b Moved and renamed Db/Entity services for better organization. 2026-08-30 20:20:03 +02:00
Daniel-Garmig 4f22cd4541 GuestLog - Moved Enums
- Enums moved to Enum folder (by type).
- ResultCode class changed to enum to ensure type safety in results.
2026-08-25 15:18:02 +02:00
Daniel-Garmig b18bd7caa6 GuestLog - StayEntry pages fixes
- Details/New - Fixed time field binding. Now it binds directly to the Model instead of using temp TimeSpans.
- New - Fixed StayEntry validation to allow creating Stays with a DateOut in the past.
- List - Added ordering to db results so limit behaves in a deterministic way. Fixed default search so stays created today are shown.
2026-08-24 20:50:38 +02:00
Daniel-Garmig 511bd023f2 GuestLog - User message improvements
- Added UserMessageService to have common methods for showing messages and errors to the user.
- Added Result record so services can return status on operation error (so it can be shown in the UI messages).
- Updated services to use the new Result record.
- Updated pages to use the new IUserMessageService.
2026-08-23 21:57:26 +02:00
97 changed files with 4915 additions and 502 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 -8
View File
@@ -6,24 +6,34 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageVersion Include="Extensions.MudBlazor.StaticInput" Version="4.1.0" /> <PackageVersion Include="Extensions.MudBlazor.StaticInput" Version="4.1.0" />
<PackageVersion Include="FluentStorage" Version="8.0.22" />
<PackageVersion Include="FluentStorage.AWS" Version="8.0.21" />
<PackageVersion Include="FluentValidation" Version="12.1.1" /> <PackageVersion Include="FluentValidation" Version="12.1.1" />
<PackageVersion Include="Grpc.AspNetCore" Version="2.64.0" /> <PackageVersion Include="Grpc.AspNetCore" Version="2.64.0" />
<PackageVersion Include="Microsoft.Extensions.Localization" Version="10.0.9" /> <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>
<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" /> <PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.7" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.7" /> <PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.12" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7"> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.12">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageVersion> </PackageVersion>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.7" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.12" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.7" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.12" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Proxies" Version="10.0.7" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Proxies" Version="10.0.12" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" /> <PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+8
View File
@@ -0,0 +1,8 @@
namespace Somnia.GuestLog;
public static class AppSettings
{
public const string AppName = "Somnia-GuestLog-dev";
public const string AppVersion = "v0.0.0-dev"; //2026-09-08
}
+1
View File
@@ -23,6 +23,7 @@
<ReconnectModal /> <ReconnectModal />
<script src="@Assets["_framework/blazor.web.js"]"></script> <script src="@Assets["_framework/blazor.web.js"]"></script>
<script src="@Assets["_content/MudBlazor/MudBlazor.min.js"]"></script> <script src="@Assets["_content/MudBlazor/MudBlazor.min.js"]"></script>
<script src="js/App.js"></script>
</body> </body>
</html> </html>
@@ -1,9 +1,10 @@
@using Somnia.GuestLog.Models.DTOs; @using Somnia.GuestLog.Models.DTOs;
@using Somnia.GuestLog.Services
@using Somnia.GuestLog.Services.AddressQuery; @using Somnia.GuestLog.Services.AddressQuery;
@inject ILogger<AddressQueryDialog> logger; @inject ILogger<AddressQueryDialog> logger;
@inject IAddressQueryFactory addressQueryFactory; @inject IAddressQueryFactory addressQueryFactory;
@inject ISnackbar Snackbar @inject IUserMessageService userMessage
<MudDialog @ref="@DialogRef"> <MudDialog @ref="@DialogRef">
<TitleContent> <TitleContent>
@@ -16,13 +17,22 @@
<MudStack row> <MudStack row>
<MudTextField @bind-Value="SearchText" Label="@LanguageLocale.control_AddressQueryDialog_searchText" <MudTextField @bind-Value="SearchText" Label="@LanguageLocale.control_AddressQueryDialog_searchText"
Required /> Required />
<MudIconButton Icon="@Icons.Material.Filled.Search" OnClick="btnSearchAddress" /> <MudIconButton Icon="@Icons.Material.Filled.Search" OnClick="btnSearchAddress" Disabled="@_isDataLoading" />
</MudStack> </MudStack>
<MudPaper Class="mt-4 pa-2"> <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" <MudList T="AddressResultDTO" @bind-SelectedValue="SelectedAddressResult"
@@ -80,6 +90,9 @@
private List<AddressResultDTO> addressResults = []; private List<AddressResultDTO> addressResults = [];
private AddressResultDTO? SelectedAddressResult { get; set; } private AddressResultDTO? SelectedAddressResult { get; set; }
private bool _isDataLoading = false;
private string _lastSearchText = "";
private void Cancel() private void Cancel()
{ {
@@ -98,21 +111,25 @@
private async Task btnSearchAddress() private async Task btnSearchAddress()
{ {
if (SearchText.Length == 0) if (SearchText.Length == 0 || SearchText == _lastSearchText)
{ {
return; return;
} }
try try
{ {
var queryService = addressQueryFactory.GetService(); _isDataLoading = true;
var queryService = addressQueryFactory.GetService();
addressResults = await queryService.SearchAddressAsync(SearchText); addressResults = await queryService.SearchAddressAsync(SearchText);
_lastSearchText = SearchText;
_isDataLoading = false;
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex, "SearchAddress returned an error!"); logger.LogError(ex, "SearchAddress returned an error!");
Snackbar.Add($"Error searching address: {ex.Message}", Severity.Error); userMessage.ShowMessage($"Error searching address: {ex.Message}", MessageKind.Error);
} }
} }
@@ -120,7 +137,7 @@
{ {
if (SelectedAddressResult is null) if (SelectedAddressResult is null)
{ {
Snackbar.Add("Select an address", Severity.Warning); userMessage.ShowMessage("Select an address", MessageKind.Warning);
return; return;
} }
@@ -1,9 +1,10 @@
@using Somnia.GuestLog.Models.DTOs; @using Somnia.GuestLog.Models.DTOs;
@using Somnia.GuestLog.Services @using Somnia.GuestLog.Services
@using Somnia.GuestLog.Services.Entity
@inject ILogger<AddressQueryDialog> logger; @inject ILogger<AddressQueryDialog> logger;
@inject ISnackbar Snackbar @inject IUserMessageService userMessage
@inject IGuestDocumentService guestDocumentService; @inject IGuestDocumentEntityService guestDocumentService;
<MudDialog Style="max-height: 90%;"> <MudDialog Style="max-height: 90%;">
@@ -77,7 +78,10 @@
private IMudDialogInstance MudDialog { get; set; } = null!; private IMudDialogInstance MudDialog { get; set; } = null!;
private GuestDocFilterDTO filter { get; set; } = new(); private GuestDocFilterDTO filter { get; set; } = new()
{
OnlyLastVersion = true
};
private IEnumerable<GuestDocumentDTO> results = []; private IEnumerable<GuestDocumentDTO> results = [];
private GuestDocumentDTO? SelectedResult { get; set; } private GuestDocumentDTO? SelectedResult { get; set; }
@@ -108,7 +112,7 @@
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex, "GuestDocument Search returned an error!"); logger.LogError(ex, "GuestDocument Search returned an error!");
Snackbar.Add($"Error searching: {ex.Message}", Severity.Error); userMessage.ShowMessage($"Error searching: {ex.Message}", MessageKind.Error);
} }
} }
@@ -119,7 +123,7 @@
{ {
if (filter.IsEmpty) if (filter.IsEmpty)
{ {
Snackbar.Add("Add some search!", Severity.Warning); userMessage.ShowMessage("Search can't be empty!", MessageKind.Warning);
return; return;
} }
@@ -131,7 +135,7 @@
{ {
if (SelectedResult is null) if (SelectedResult is null)
{ {
Snackbar.Add("Select a guest document", Severity.Warning); userMessage.ShowMessage("A guest document must be selected", MessageKind.Warning);
return; return;
} }
@@ -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 IDataListService dataListService;
@inject IValidator<GuestDocumentDTO> guestDocValidator; @inject IValidator<GuestDocumentDTO> guestDocValidator;
@inject IUserMessageService userMessage;
<MudForm @ref="@_form" Model="@Model" Validation="@((guestDocValidator as GuestDocumentDTOValidator)!.ValidateValue)" ValidationDelay="0" <MudForm @ref="@_form" Model="@Model" Validation="@((guestDocValidator as GuestDocumentDTOValidator)!.ValidateValue)"
ReadOnly="@ReadOnly"> ValidationDelay="0" ReadOnly="@ReadOnly">
<MudGrid> <MudGrid>
<MudItem xs="12" md="6"> <MudItem xs="12" md="6">
<MudPaper Class="pa-4" Elevation="3" Height="100%"> <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" /> <MudDivider DividerType="DividerType.Middle" Class="mb-4" />
<MudStack Row style="@FieldMarginY"> <MudStack Row style="@FieldMarginY">
<MudSelect @bind-Value="Model.DocumentType" For="@(() => Model.DocumentType)" Label="@LanguageLocale.dto_GuestDocument_DocumentType" <MudSelect @bind-Value="Model.DocumentType" For="@(() => Model.DocumentType)"
Required ReadOnly="@ReadOnlyDocId"> Label="@LanguageLocale.dto_GuestDocument_DocumentType" Required ReadOnly="@ReadOnlyDocId">
@foreach (string docType in dataListService.GetDataListByName(GuestConsts.DataList_DocumentTypes)) @foreach (string docType in
dataListService.GetDataListByName(GuestConsts.DataList_DocumentTypes))
{ {
<MudSelectItem Value="docType"> <MudSelectItem Value="docType">
@dataListService.GetLocalizedValue(GuestConsts.DataList_DocumentTypes, docType) @dataListService.GetLocalizedValue(GuestConsts.DataList_DocumentTypes, docType)
</MudSelectItem> </MudSelectItem>
} }
</MudSelect> </MudSelect>
<MudTextField @bind-Value="Model.DocumentNum" For="@(() => Model.DocumentNum)" Label="@LanguageLocale.dto_GuestDocument_DocumentNum" <MudTextField @bind-Value="Model.DocumentNum" For="@(() => Model.DocumentNum)"
Required ReadOnly="@ReadOnlyDocId" /> Label="@LanguageLocale.dto_GuestDocument_DocumentNum" Required ReadOnly="@ReadOnlyDocId" />
</MudStack> </MudStack>
<MudStack Row style="@FieldMarginY"> <MudStack Row style="@FieldMarginY">
<MudDatePicker @bind-Date="Model.IssueDate" For="@(() => Model.IssueDate)" Label="@LanguageLocale.dto_GuestDocument_IssueDate" <MudDatePicker @bind-Date="Model.IssueDate" For="@(() => Model.IssueDate)"
Clearable OpenTo="OpenTo.Year" Editable /> Label="@LanguageLocale.dto_GuestDocument_IssueDate" Clearable OpenTo="OpenTo.Year" Editable />
<MudTextField @bind-Value="Model.SupportNum" For="@(() => Model.SupportNum)" Label="@LanguageLocale.dto_GuestDocument_SupportNum" <MudTextField @bind-Value="Model.SupportNum" For="@(() => Model.SupportNum)"
Required="@isSuportNumberRequired" Mask="@(new PatternMask("aaa000000"))" Label="@LanguageLocale.dto_GuestDocument_SupportNum" Required="@isSuportNumberRequired"
Mask="SuportNumberPatternMask"
OnBlur="@(() => Model.SupportNum = Model.SupportNum?.ToUpper())" /> OnBlur="@(() => Model.SupportNum = Model.SupportNum?.ToUpper())" />
</MudStack> </MudStack>
<MudAutocomplete T="string" @bind-Value="Model.Nationality" For="@(() => Model.Nationality)" Label="@LanguageLocale.dto_GuestDocument_Nationality" <MudAutocomplete T="string" @bind-Value="Model.Nationality" For="@(() => Model.Nationality)"
SearchFunc="@SearchCountryCodes" ToStringFunc="@(e=> string.IsNullOrEmpty(e) ? "" : @dataListService.GetLocalizedValue(GuestConsts.DataList_CountryCodes, e))" Label="@LanguageLocale.dto_GuestDocument_Nationality" SearchFunc="@SearchCountryCodes"
SelectValueOnTab="true" AdornmentIcon="@Icons.Material.Filled.Search" ToStringFunc="@(e=> string.IsNullOrEmpty(e) ? "" : @dataListService.GetLocalizedValue(GuestConsts.DataList_CountryCodes, e))"
style="@FieldMarginY" /> SelectValueOnTab="true" AdornmentIcon="@Icons.Material.Filled.Search" style="@FieldMarginY"
Required />
</MudPaper> </MudPaper>
</MudItem> </MudItem>
<MudItem xs="12" md="6"> <MudItem xs="12" md="6">
<MudPaper Class="pa-4" Elevation="3" Height="100%"> <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" /> <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" Label="@LanguageLocale.dto_GuestDocument_Name"
OnBlur="@(() => Model.Name = Model.Name?.ToUpper())" /> 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" Label="@LanguageLocale.dto_GuestDocument_Surname"
OnBlur="@(() => Model.Surname = Model.Surname?.ToUpper())" /> OnBlur="@(() => Model.Surname = Model.Surname?.ToUpper())" />
<MudTextField @bind-Value="Model.Surname2" For="@(() => Model.Surname2)" <MudTextField @bind-Value="Model.Surname2" For="@(() => Model.Surname2)"
@@ -60,11 +68,14 @@
OnBlur="@(() => Model.Surname2 = Model.Surname2?.ToUpper())" /> OnBlur="@(() => Model.Surname2 = Model.Surname2?.ToUpper())" />
<MudStack Row style="@FieldMarginY"> <MudStack Row style="@FieldMarginY">
<MudDatePicker @bind-Date="Model.BirthDate" For="@(() => Model.BirthDate)" Label="@LanguageLocale.dto_GuestDocument_BirthDate" <MudDatePicker @bind-Date="Model.BirthDate" For="@(() => Model.BirthDate)"
Clearable OpenTo="OpenTo.Year" Editable /> Label="@LanguageLocale.dto_GuestDocument_BirthDate" OpenTo="OpenTo.Year" Clearable Required
Editable />
<MudSelect @bind-Value="Model.Gender" For="@(() => Model.Gender)" Label="@LanguageLocale.dto_GuestDocument_Gender"> <MudSelect @bind-Value="Model.Gender" For="@(() => Model.Gender)"
@foreach (string gender in dataListService.GetDataListByName(GuestConsts.DataList_GenderOptions)) Label="@LanguageLocale.dto_GuestDocument_Gender">
@foreach (string gender in
dataListService.GetDataListByName(GuestConsts.DataList_GenderOptions))
{ {
<MudSelectItem Value="gender"> <MudSelectItem Value="gender">
@dataListService.GetLocalizedValue(GuestConsts.DataList_GenderOptions, gender) @dataListService.GetLocalizedValue(GuestConsts.DataList_GenderOptions, gender)
@@ -77,30 +88,37 @@
<MudItem xs="12" md="6"> <MudItem xs="12" md="6">
<MudPaper Class="pa-4" Elevation="3" Height="100%"> <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" /> <MudDivider DividerType="DividerType.Middle" Class="mb-4" />
<MudStack Row> <MudStack Row>
<MudTextField @bind-Value="Model.PhoneNumber" For="@(() => Model.PhoneNumber)" <MudTextField @bind-Value="Model.PhoneNumber" For="@(() => Model.PhoneNumber)"
Label="@LanguageLocale.dto_GuestDocument_PhoneNumber" Label="@LanguageLocale.dto_GuestDocument_PhoneNumber" InputType="InputType.Telephone" />
InputType="InputType.Telephone" />
<MudTextField @bind-Value="Model.EmailAddress" For="@(() => Model.EmailAddress)" <MudTextField @bind-Value="Model.EmailAddress" For="@(() => Model.EmailAddress)"
Label="@LanguageLocale.dto_GuestDocument_EmailAddress" Label="@LanguageLocale.dto_GuestDocument_EmailAddress" InputType="InputType.Email" />
InputType="InputType.Email" />
</MudStack> </MudStack>
</MudPaper> </MudPaper>
</MudItem> </MudItem>
<MudItem xs="12" md="6"> <MudItem xs="12" md="6">
<MudPaper Class="pa-4" Elevation="3" Height="100%"> <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" /> <MudDivider DividerType="DividerType.Middle" Class="mb-4" />
@if (!ReadOnly) @if (!ReadOnly)
{ {
<MudButton OnClick="BtnOpenAddressSearchDialog" Variant="Variant.Outlined" FullWidth="true" Class="mb-2"> <MudStack Row>
<MudButton OnClick="BtnOpenAddressSearchDialog" Variant="Variant.Outlined" FullWidth="true"
Class="mb-2">
@LanguageLocale.control_GuestDocForm_btnSearchAddress @LanguageLocale.control_GuestDocForm_btnSearchAddress
</MudButton> </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)" <MudTextField @bind-Value="Model.Address" For="@(() => Model.Address)"
@@ -116,7 +134,8 @@
<MudStack Row style="@FieldMarginY"> <MudStack Row style="@FieldMarginY">
<MudAutocomplete T="string" @bind-Value="Model.AddressCountry" For="@(() => Model.AddressCountry)" <MudAutocomplete T="string" @bind-Value="Model.AddressCountry" For="@(() => Model.AddressCountry)"
Label="@LanguageLocale.general_address_country" ReadOnly="@ReadOnlyAddress" Label="@LanguageLocale.general_address_country" ReadOnly="@ReadOnlyAddress"
SearchFunc="@SearchCountryCodes" ToStringFunc="@(e=> string.IsNullOrEmpty(e) ? "" : @dataListService.GetLocalizedValue(GuestConsts.DataList_CountryCodes, e))" SearchFunc="@SearchCountryCodes"
ToStringFunc="@(e=> string.IsNullOrEmpty(e) ? "" : @dataListService.GetLocalizedValue(GuestConsts.DataList_CountryCodes, e))"
SelectValueOnTab="true" AdornmentIcon="@Icons.Material.Filled.Search" /> SelectValueOnTab="true" AdornmentIcon="@Icons.Material.Filled.Search" />
<MudTextField @bind-Value="Model.ZipCode" For="@(() => Model.ZipCode)" <MudTextField @bind-Value="Model.ZipCode" For="@(() => Model.ZipCode)"
@@ -155,7 +174,20 @@
private Dictionary<string, string>? countryDict = null; 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 private string CreateTimeString
{ {
get get
@@ -237,6 +269,14 @@
ReadOnlyAddress = ReadOnly; 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 #endregion
} }
@@ -7,7 +7,7 @@
@inject IDataListService dataListService; @inject IDataListService dataListService;
@inject IValidator<StayEntryDTO> stayValidator; @inject IValidator<StayEntryDTO> stayValidator;
@inject IDialogService dialogService; @inject IDialogService dialogService;
@inject ISnackbar snackbar @inject IUserMessageService userMessage
<MudForm @ref="@_form" Model="@Model" Validation="@((stayValidator as StayEntryDTOValidator)!.ValidateValue)" <MudForm @ref="@_form" Model="@Model" Validation="@((stayValidator as StayEntryDTOValidator)!.ValidateValue)"
@@ -21,12 +21,18 @@
<MudDivider DividerType="DividerType.Middle" Class="mb-4" /> <MudDivider DividerType="DividerType.Middle" Class="mb-4" />
<MudStack Row style="@FieldMarginY"> <MudStack Row style="@FieldMarginY">
<MudDatePicker @bind-Date="Model.DateIn" For="@(() => Model.DateIn)" <MudDatePicker Date="Model.DateIn" For="@(() => Model.DateIn)"
Label="@LanguageLocale.dto_StayEntry_DateIn" Clearable OpenTo="OpenTo.Date" Editable /> DateChanged="@((newVal) => Model.DateIn = GetDateTimeFromDateAndTime(newVal, Model.DateIn?.TimeOfDay))"
<MudTimePicker @bind-Time="TimeIn" Label=" " Editable /> Label="@LanguageLocale.dto_StayEntry_DateIn" Clearable OpenTo="OpenTo.Date" Editable
<MudDatePicker @bind-Date="Model.DateOut" For="@(() => Model.DateOut)" AutoClose="true" />
Label="@LanguageLocale.dto_StayEntry_DateOut" Clearable OpenTo="OpenTo.Date" Editable /> <MudTimePicker Time="Model.DateIn?.TimeOfDay" Label=" " Editable
<MudTimePicker @bind-Time="TimeOut" Label=" " Editable /> TimeChanged="@((newVal) => Model.DateIn = GetDateTimeFromDateAndTime(Model.DateIn, newVal))" />
<MudDatePicker Date="Model.DateOut" For="@(() => Model.DateOut)"
DateChanged="@((newVal) => Model.DateOut = GetDateTimeFromDateAndTime(newVal, Model.DateOut?.TimeOfDay))"
Label="@LanguageLocale.dto_StayEntry_DateOut" Clearable OpenTo="OpenTo.Date" Editable
AutoClose="true" />
<MudTimePicker Time="Model.DateOut?.TimeOfDay" Label=" " Editable
TimeChanged="@((newVal) => Model.DateOut = GetDateTimeFromDateAndTime(Model.DateOut, newVal))" />
</MudStack> </MudStack>
<MudStack Row style="@FieldMarginY"> <MudStack Row style="@FieldMarginY">
<MudTextField @bind-Value="Model.RoomCount" For="@(() => Model.RoomCount)" <MudTextField @bind-Value="Model.RoomCount" For="@(() => Model.RoomCount)"
@@ -91,7 +97,7 @@
{ {
<MudTd DataLabel="@LanguageLocale.general_actions"> <MudTd DataLabel="@LanguageLocale.general_actions">
<MudIconButton Icon="@Icons.Material.Filled.Delete" <MudIconButton Icon="@Icons.Material.Filled.Delete"
OnClick="@(() => BtnDeleteGuest(context.DocumentType!, context.DocumentNum!))" /> OnClick="@(() => BtnDeleteGuest(context.Id!.Value, context.Version!.Value))" />
</MudTd> </MudTd>
} }
@if (ReadOnly) @if (ReadOnly)
@@ -113,11 +119,11 @@
[Parameter] [Parameter]
public StayEntryDTO Model { private get; set; } = new(); public StayEntryDTO Model { private get; set; } = new();
[Parameter] [Parameter, EditorRequired]
public bool ReadOnly { get; set; } = false; public FormMode FormMode { get; set; }
private TimeSpan? TimeIn = DateTime.Now.TimeOfDay;
private TimeSpan? TimeOut = new(hours: 12, minutes: 0, seconds: 0); private bool ReadOnly => FormMode == FormMode.ReadOnly;
private MudForm _form = null!; private MudForm _form = null!;
@@ -138,7 +144,20 @@
protected override void OnInitialized() protected override void OnInitialized()
{ {
Model.DateIn = DateTime.Today; //Initialize values when not ReadOnly.
if (FormMode == FormMode.Create)
{
Model.DateIn = DateTime.Now;
DateTime dateOut = DateTime.Today.AddDays(1);
Model.DateOut = new(
year: dateOut.Year, month: dateOut.Month, day: dateOut.Day,
hour: 12, minute: 0, second: 0
);
//Init Guest List
Model.GuestDocuments = [];
}
} }
#region Public Methods #region Public Methods
@@ -149,7 +168,7 @@
if (Model.GuestDocuments is null || Model.GuestDocuments.Count == 0) if (Model.GuestDocuments is null || Model.GuestDocuments.Count == 0)
{ {
snackbar.Add("At least 1 guest is required!", MudBlazor.Severity.Warning); userMessage.ShowMessage("At least 1 guest is required!", MessageKind.Warning);
return false; return false;
} }
@@ -158,10 +177,6 @@
public StayEntryDTO GetModel() public StayEntryDTO GetModel()
{ {
//Update model times.
Model.DateIn = GetDateTimeFromDateAndTime(Model.DateIn, TimeIn);
Model.DateOut = GetDateTimeFromDateAndTime(Model.DateOut, TimeOut);
return Model; return Model;
} }
@@ -185,6 +200,8 @@
private async Task BtnAddGuest() private async Task BtnAddGuest()
{ {
Debug.Assert(Model.GuestDocuments != null);
DialogOptions dialogOptions = new() DialogOptions dialogOptions = new()
{ {
MaxWidth = MaxWidth.Medium, MaxWidth = MaxWidth.Medium,
@@ -205,38 +222,31 @@
if (resultData is null) if (resultData is null)
{ {
snackbar.Add("Error! No Guest Document selected!", MudBlazor.Severity.Error); userMessage.ShowMessage("Error! No Guest Document selected!", MessageKind.Error);
return; return;
} }
//Check if this guest is already added. //Check if this guest is already added.
if (Model.GuestDocuments is not null && Model.GuestDocuments.Count > 0) if (Model.GuestDocuments.Count > 0)
{ {
bool alreadyAdded = Model.GuestDocuments.Any(e => e.DocumentType == resultData.DocumentType bool alreadyAdded = Model.GuestDocuments.Any(e => e.Id == resultData.Id);
&& e.DocumentNum == resultData.DocumentNum);
if (alreadyAdded) if (alreadyAdded)
{ {
snackbar.Add("GuestDocument already added!", MudBlazor.Severity.Warning); userMessage.ShowMessage("GuestDocument already added!", MessageKind.Warning);
return; return;
} }
} }
//Init List.
if (Model.GuestDocuments is null)
{
Model.GuestDocuments = [];
}
Model.GuestDocuments.Add(resultData); Model.GuestDocuments.Add(resultData);
} }
private void BtnDeleteGuest(string docType, string docNum) private void BtnDeleteGuest(long guestDocId, int version)
{ {
Debug.Assert(Model.GuestDocuments is not null); Debug.Assert(Model.GuestDocuments is not null);
//Filter out given GuestDocument from model. //Filter out given GuestDocument from model.
Model.GuestDocuments = Model.GuestDocuments Model.GuestDocuments = Model.GuestDocuments
.Where(e => !(e.DocumentType == docType && e.DocumentNum == docNum)) .Where(e => !(e.Id == guestDocId && e.Version == version))
.ToList(); .ToList();
} }
@@ -19,9 +19,14 @@
<MudToggleIconButton Class="ml-auto" Icon="@Icons.Material.Outlined.LightMode" <MudToggleIconButton Class="ml-auto" Icon="@Icons.Material.Outlined.LightMode"
ToggledIcon="@Icons.Material.Outlined.DarkMode" ToggledColor="Color.Primary" Color="Color.Primary" ToggledIcon="@Icons.Material.Outlined.DarkMode" ToggledColor="Color.Primary" Color="Color.Primary"
Toggled="@_isDarkMode" ToggledChanged="@DarkModeToggle" /> Toggled="@_isDarkMode" ToggledChanged="@DarkModeToggle" />
<LanguageSelector />
</MudAppBar> </MudAppBar>
<MudDrawer id="nav-drawer" @bind-Open="_drawerOpen" ClipMode="DrawerClipMode.Docked" Elevation="2"> <MudDrawer id="nav-drawer" @bind-Open="_drawerOpen" ClipMode="DrawerClipMode.Docked" Elevation="2">
<NavMenu /> <NavMenu />
<MudSpacer />
<MudText Typo="Typo.caption" Class="mx-2" GutterBottom>
@AppSettings.AppName - @AppSettings.AppVersion
</MudText>
</MudDrawer> </MudDrawer>
<MudMainContent Class="pt-16 pa-4"> <MudMainContent Class="pt-16 pa-4">
@Body @Body
@@ -13,4 +13,7 @@
<MudNavLink Href="@GuestConsts.Page_StayEntry_List" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Book"> <MudNavLink Href="@GuestConsts.Page_StayEntry_List" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Book">
@LanguageLocale.navMenu_StayEntryList @LanguageLocale.navMenu_StayEntryList
</MudNavLink> </MudNavLink>
<MudNavLink Href="@GuestConsts.Page_StayExport_List" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.ImportExport">
@LanguageLocale.navMenu_StayExport
</MudNavLink>
</MudNavMenu> </MudNavMenu>
@@ -1,11 +1,12 @@
@page "/debug/addrSearch" @page "/debug/addrSearch"
@using Somnia.GuestLog.Models.DTOs; @using Somnia.GuestLog.Models.DTOs;
@using Somnia.GuestLog.Services;
@using Somnia.GuestLog.Services.AddressQuery; @using Somnia.GuestLog.Services.AddressQuery;
@inject ILogger<AddressQuery> logger; @inject ILogger<AddressQuery> logger;
@inject IAddressQueryFactory addressQueryFactory; @inject IAddressQueryFactory addressQueryFactory;
@inject ISnackbar Snackbar @inject IUserMessageService userMessage
@inject IDialogService DialogService @inject IDialogService DialogService
<PageTitle>DEBUG - Address Query</PageTitle> <PageTitle>DEBUG - Address Query</PageTitle>
@@ -89,7 +90,7 @@
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex, "SearchAddress returned an error!"); logger.LogError(ex, "SearchAddress returned an error!");
Snackbar.Add($"Error searching address: {ex.Message}", Severity.Error); userMessage.ShowMessage($"Error searching address: {ex.Message}", MessageKind.Error);
} }
} }
@@ -1,8 +1,10 @@
@page "/debug/empty" @page "/debug/empty"
@using Somnia.GuestLog.Services
@inject ILogger<Empty> logger; @inject ILogger<Empty> logger;
@inject ISnackbar Snackbar @inject IUserMessageService userMessage
@inject IDialogService DialogService @inject IDialogService DialogService
<PageTitle>DEBUG - Empty</PageTitle> <PageTitle>DEBUG - Empty</PageTitle>
@@ -4,26 +4,41 @@
@using Somnia.GuestLog.Services; @using Somnia.GuestLog.Services;
@using Microsoft.AspNetCore.WebUtilities; @using Microsoft.AspNetCore.WebUtilities;
@inject ISnackbar Snackbar @inject IUserMessageService userMessage
@inject IDialogService DialogService @inject IDialogService DialogService
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject IGuestDocumentService guestDocService @inject IGuestDocumentEntityService guestDocService
@inject IStayEntryService stayService @inject IStayEntryEntityService stayService
@inject IDialogService DialogService
<PageTitle>Somnia - Guest Docs - Details</PageTitle> <PageTitle>Somnia - Guest Docs - Details</PageTitle>
<MudStack Row="true" Class="my-4"> <MudStack Row="true" Class="my-4">
<MudText Typo="Typo.h4" GutterBottom="true">Guest Document - "@docId"</MudText> <MudText Typo="Typo.h4" GutterBottom="true">Guest Document: @docId - @documentModel?.FullDocument</MudText>
<MudSpacer /> <MudSpacer />
@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" <MudButton Variant="Variant.Filled" Color="Color.Success" Size="Size.Small" StartIcon="@Icons.Material.Filled.Edit"
OnClick="() => BtnEdit()" Disabled> OnClick="() => BtnEnableEdit()" Disabled="_isEditMode">
@LanguageLocale.general_btnEdit @LanguageLocale.general_btnEdit
</MudButton> </MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Default" Size="Size.Small" <MudButton Variant="Variant.Filled" Color="Color.Default" Size="Size.Small"
StartIcon="@Icons.Material.Filled.ArrowBack" OnClick="() => BtnReturn()"> StartIcon="@Icons.Material.Filled.ArrowBack" OnClick="() => BtnReturn()">
@LanguageLocale.general_btnReturn @LanguageLocale.general_btnReturn
</MudButton> </MudButton>
}
</MudStack> </MudStack>
@if (documentModel is not null) @if (documentModel is not null)
@@ -32,10 +47,11 @@
Centered> Centered>
<MudTabPanel Text="Details" Icon="@Icons.Material.Filled.Info"> <MudTabPanel Text="Details" Icon="@Icons.Material.Filled.Info">
<GuestDocForm @ref="_form" Model="documentModel" ReadOnly="true" /> <GuestDocForm @ref="_form" Model="documentModel" ReadOnly="!_isEditMode" />
</MudTabPanel> </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> <MudText>Total Stays: @(guestStaysLoading ? "Loading..." : guestStayList.Count())</MudText>
<MudTable Items="@guestStayList" Hover="true" Loading="@guestStaysLoading" Class="mt-2" Elevation="2"> <MudTable Items="@guestStayList" Hover="true" Loading="@guestStaysLoading" Class="mt-2" Elevation="2">
<HeaderContent> <HeaderContent>
@@ -77,7 +93,7 @@
@code { @code {
[Parameter] [Parameter]
public string? docId { get; set; } public long docId { get; set; }
GuestDocForm _form = null!; GuestDocForm _form = null!;
@@ -87,29 +103,22 @@
IEnumerable<StayEntryDTO> guestStayList = []; IEnumerable<StayEntryDTO> guestStayList = [];
bool guestStaysLoading = true; bool guestStaysLoading = true;
private bool _isEditMode = false;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
if (docId is null)
{
Snackbar.Add("No GuestDocument provided!!", Severity.Error);
Navigation.NotFound();
return;
}
GuestDocIdDTO? docIdModel = GuestDocIdDTO.DecodeFromString(docId);
if (docIdModel is null)
{
Snackbar.Add($"Provided GuestDocId can't be decoded: '{docId}'", Severity.Error);
Navigation.NotFound();
return;
}
//Get model from db. //Get model from db.
documentModel = await guestDocService.GetDtoByIdAsync(docIdModel.DocumentType, docIdModel.DocumentNum); await LoadGuestDocumentAsync();
}
private async Task LoadGuestDocumentAsync()
{
//Get model from db.
documentModel = await guestDocService.GetLastVersionDtoByIdAsync(docId);
if (documentModel is null) if (documentModel is null)
{ {
Snackbar.Add($"GuestDocument not found with ID: '{docId}'", Severity.Error); userMessage.ShowMessage($"GuestDocument not found with ID: '{docId}'", MessageKind.Error);
Navigation.NotFound(); Navigation.NotFound();
return; return;
} }
@@ -123,22 +132,81 @@
return; return;
} }
if (documentModel is null || documentModel.DocumentType is null || documentModel.DocumentNum is null) if (documentModel is null || documentModel.Id is null || documentModel.Version is null)
{ {
throw new InvalidOperationException(); throw new InvalidOperationException();
} }
guestStaysLoading = true; guestStaysLoading = true;
guestStayList = await stayService.GetStaysWithGuest(documentModel.DocumentType, documentModel.DocumentNum); guestStayList = await stayService.GetStaysWithGuest(documentModel.Id.Value);
guestStaysLoading = false; guestStaysLoading = false;
} }
#region Event Handlers #region Event Handlers
private void BtnEdit() private void BtnEnableEdit()
{ {
Snackbar.Add("Not implemented yet!", Severity.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() private void BtnReturn()
@@ -3,9 +3,9 @@
@using Somnia.GuestLog.Models.DTOs; @using Somnia.GuestLog.Models.DTOs;
@using Somnia.GuestLog.Services; @using Somnia.GuestLog.Services;
@inject IGuestDocumentService GuestDocService @inject IGuestDocumentEntityService GuestDocService
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject ISnackbar Snackbar @inject IUserMessageService userMessage
@inject IDialogService DialogService @inject IDialogService DialogService
@inject IDataListService dataListService; @inject IDataListService dataListService;
@@ -43,6 +43,9 @@
</MudStack> </MudStack>
</ToolBarContent> </ToolBarContent>
<Columns> <Columns>
<PropertyColumn Property="x => x.Id" Title="@LanguageLocale.dto_GuestDocument_Id" Hideable="false" />
<PropertyColumn Property="x => x.Version" Title="@LanguageLocale.dto_GuestDocument_Version" Hidden
Hideable="true" />
<PropertyColumn Title="@LanguageLocale.dto_GuestDocument_DocumentType" <PropertyColumn Title="@LanguageLocale.dto_GuestDocument_DocumentType"
Property="x => dataListService.GetLocalizedValue(GuestConsts.DataList_DocumentTypes, x.DocumentType)" Property="x => dataListService.GetLocalizedValue(GuestConsts.DataList_DocumentTypes, x.DocumentType)"
Hideable="false" /> Hideable="false" />
@@ -50,6 +53,7 @@
Title="@LanguageLocale.dto_GuestDocument_DocumentNum" /> Title="@LanguageLocale.dto_GuestDocument_DocumentNum" />
<PropertyColumn Property="x => x.Name" Title="@LanguageLocale.dto_GuestDocument_Name" /> <PropertyColumn Property="x => x.Name" Title="@LanguageLocale.dto_GuestDocument_Name" />
<PropertyColumn Property="x => x.Surname" Title="@LanguageLocale.dto_GuestDocument_Surname" /> <PropertyColumn Property="x => x.Surname" Title="@LanguageLocale.dto_GuestDocument_Surname" />
<PropertyColumn Property="x => x.Surname2" Title="@LanguageLocale.dto_GuestDocument_Surname2" />
<PropertyColumn Title="@LanguageLocale.dto_GuestDocument_Nationality" <PropertyColumn Title="@LanguageLocale.dto_GuestDocument_Nationality"
Property="x => dataListService.GetLocalizedValue(GuestConsts.DataList_CountryCodes, x.Nationality)" /> Property="x => dataListService.GetLocalizedValue(GuestConsts.DataList_CountryCodes, x.Nationality)" />
<PropertyColumn Property="x => x.UpdateTime" Title="@LanguageLocale.dto_GuestDocument_UpdateTime" /> <PropertyColumn Property="x => x.UpdateTime" Title="@LanguageLocale.dto_GuestDocument_UpdateTime" />
@@ -101,7 +105,7 @@
if (showMsg) if (showMsg)
{ {
Snackbar.Add("Data loaded from DB", Severity.Info, o => o.VisibleStateDuration = 1000); userMessage.ShowMessage("Data loaded from DB", MessageKind.Info);
} }
_dataLoading = false; _dataLoading = false;
} }
@@ -126,10 +130,9 @@
private async Task BtnViewDetails(GuestDocumentDTO item) private async Task BtnViewDetails(GuestDocumentDTO item)
{ {
GuestDocIdDTO idDto = GuestDocIdDTO.BuildFromDto(item) ?? throw new InvalidOperationException(); string strId = item.Id?.ToString() ?? throw new InvalidOperationException();
string itemId = GuestDocIdDTO.EncodeAsString(idDto); string uri = GuestConsts.Page_GuestDocs_Detail.Replace(GuestConsts.Page_GuestDocs_Detail_Param_DocId, strId);
string uri = GuestConsts.Page_GuestDocs_Detail.Replace(GuestConsts.Page_GuestDocs_Detail_Param_DocId, itemId);
Navigation.NavigateTo(uri); Navigation.NavigateTo(uri);
} }
@@ -142,15 +145,16 @@
return; return;
} }
bool deleted = await GuestDocService.DeleteDocumentAsync(item); Result deleteResult = await GuestDocService.DeleteDocumentAsync(item);
if (!deleted) if (!deleteResult.IsSuccess)
{ {
Snackbar.Add($"Error deleting document {item.DocumentType}-{item.DocumentNum}", Severity.Error); string errorMsg = $"Error deleting document {item.DocumentType}-{item.DocumentNum}";
userMessage.ShowResultWithMessage(deleteResult, errorMsg);
return; return;
} }
Snackbar.Add($"Document {item.DocumentType}-{item.DocumentNum} deleted!", Severity.Success); userMessage.ShowMessage($"Document {item.DocumentType}-{item.DocumentNum} deleted!", MessageKind.Success);
await LoadDataAsync(); await LoadDataAsync(showMsg: false);
} }
private void BtnNewGuest() private void BtnNewGuest()
@@ -4,10 +4,10 @@
@using Somnia.GuestLog.Services; @using Somnia.GuestLog.Services;
@using Somnia.GuestLog.Validators; @using Somnia.GuestLog.Validators;
@inject ISnackbar Snackbar @inject IUserMessageService userMessage
@inject IDialogService DialogService @inject IDialogService DialogService
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject IGuestDocumentService guestDocService @inject IGuestDocumentEntityService guestDocService
@inject IDataListService dataListService; @inject IDataListService dataListService;
@inject FluentValidation.IValidator<GuestDocIdDTO> docIdValidator; @inject FluentValidation.IValidator<GuestDocIdDTO> docIdValidator;
@@ -18,10 +18,13 @@
<MudStack Row="true" Class="my-4"> <MudStack Row="true" Class="my-4">
<MudText Typo="Typo.h4" GutterBottom="true">New Guest Document</MudText> <MudText Typo="Typo.h4" GutterBottom="true">New Guest Document</MudText>
<MudSpacer /> <MudSpacer />
@if (currentPageMode == ePageMode.createNew)
{
<MudButton Variant="Variant.Filled" Color="Color.Success" Size="Size.Small" StartIcon="@Icons.Material.Filled.Add" <MudButton Variant="Variant.Filled" Color="Color.Success" Size="Size.Small" StartIcon="@Icons.Material.Filled.Add"
OnClick="() => BtnSaveNewGuest()"> OnClick="() => BtnSaveNewGuest()">
@LanguageLocale.general_btnCreate @LanguageLocale.general_btnCreate
</MudButton> </MudButton>
}
<MudButton Variant="Variant.Filled" Color="Color.Default" Size="Size.Small" <MudButton Variant="Variant.Filled" Color="Color.Default" Size="Size.Small"
StartIcon="@Icons.Material.Filled.ArrowBack" OnClick="() => BtnReturn()"> StartIcon="@Icons.Material.Filled.ArrowBack" OnClick="() => BtnReturn()">
@LanguageLocale.general_btnReturn @LanguageLocale.general_btnReturn
@@ -82,6 +85,7 @@
ePageMode currentPageMode = ePageMode.selectDocId; ePageMode currentPageMode = ePageMode.selectDocId;
GuestDocIdDTO currentDocId = new() { DocumentNum = "", DocumentType = "" }; GuestDocIdDTO currentDocId = new() { DocumentNum = "", DocumentType = "" };
long? alreadyExistDocId = null;
GuestDocumentDTO newDocumentModel = new(); GuestDocumentDTO newDocumentModel = new();
bool loadingDocId = false; bool loadingDocId = false;
@@ -100,7 +104,8 @@
} }
loadingDocId = true; loadingDocId = true;
GuestDocumentDTO? dto = await guestDocService.GetDtoByIdAsync(currentDocId.DocumentType, currentDocId.DocumentNum); GuestDocumentDTO? dto =
await guestDocService.GetLastVersionDtoByDocumentAsync(currentDocId.DocumentType, currentDocId.DocumentNum);
loadingDocId = false; loadingDocId = false;
if (dto is null) if (dto is null)
@@ -117,6 +122,7 @@
//GuestDoc already exists! //GuestDoc already exists!
currentPageMode = ePageMode.alreadyExistDoc; currentPageMode = ePageMode.alreadyExistDoc;
alreadyExistDocId = dto.Id;
} }
private void BtnReturn() private void BtnReturn()
@@ -126,42 +132,44 @@
private async Task BtnSaveNewGuest() private async Task BtnSaveNewGuest()
{ {
Debug.Assert(currentPageMode == ePageMode.createNew && _form != null);
bool isFormValid = await _form.ValidateForm(); bool isFormValid = await _form.ValidateForm();
if (!isFormValid) if (!isFormValid)
{ {
Snackbar.Add("Invalid Form!", Severity.Error); userMessage.ShowMessage("Invalid Form!", MessageKind.Warning);
return; return;
} }
GuestDocumentDTO model = _form.Model; GuestDocumentDTO model = _form.Model;
Snackbar.Add($"Document '{model.DocumentNum}' creating...", Severity.Info);
//Show Loading dialog... //Show Loading dialog...
var loadingDialog = await DialogService.ShowAsync<LoadingDialog>(); var loadingDialog = await DialogService.ShowAsync<LoadingDialog>();
bool isCreated = await guestDocService.CreateDocumentAsync(model); Result createResult = await guestDocService.CreateDocumentAsync(model);
if (!isCreated) if (!createResult.IsSuccess)
{ {
Snackbar.Add($"Error creating Document '{model.DocumentNum}'", Severity.Error); string errorMsg = $"Error creating Document '{model.FullDocument}'";
userMessage.ShowResultWithMessage(createResult, errorMsg);
loadingDialog.Close(); loadingDialog.Close();
return; return;
} }
Snackbar.Add($"Document '{model.DocumentNum}' Created!", Severity.Success); userMessage.ShowMessage($"Document '{model.FullDocument}' Created!", MessageKind.Success);
Navigation.NavigateTo(GuestConsts.Page_GuestDocs_List); Navigation.NavigateTo(GuestConsts.Page_GuestDocs_List);
} }
private void BtnGoToDocument() private void BtnGoToDocument()
{ {
if (currentPageMode != ePageMode.alreadyExistDoc) if (currentPageMode != ePageMode.alreadyExistDoc || alreadyExistDocId is null)
{ {
Snackbar.Add("Invalid operation!", Severity.Error); userMessage.ShowMessage("Invalid operation!", MessageKind.Error);
return; return;
} }
string docId = GuestDocIdDTO.EncodeAsString(currentDocId);
string uri = GuestConsts.Page_GuestDocs_Detail.Replace("{docId}", docId); string strId = alreadyExistDocId.Value.ToString();
string uri = GuestConsts.Page_GuestDocs_Detail.Replace(GuestConsts.Page_GuestDocs_Detail_Param_DocId, strId);
Navigation.NavigateTo(uri); Navigation.NavigateTo(uri);
} }
@@ -3,11 +3,11 @@
@using Somnia.GuestLog.Models.DTOs; @using Somnia.GuestLog.Models.DTOs;
@using Somnia.GuestLog.Services; @using Somnia.GuestLog.Services;
@inject ISnackbar Snackbar @inject IUserMessageService userMessage
@inject IDialogService DialogService @inject IDialogService DialogService
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject IGuestDocumentService guestDocService @inject IGuestDocumentEntityService guestDocService
@inject IStayEntryService stayService @inject IStayEntryEntityService stayService
<PageTitle>Somnia - Guest Docs - Details</PageTitle> <PageTitle>Somnia - Guest Docs - Details</PageTitle>
@@ -27,7 +27,8 @@
@if (stayModel is not null) @if (stayModel is not null)
{ {
<StayEntryForm Model="stayModel" ReadOnly="true" /> <StayEntryForm Model="stayModel" FormMode="FormMode.ReadOnly" />
<MudText Class="mt-4">@CreateTimeString</MudText>
} }
@@ -41,6 +42,22 @@
StayEntryDTO? stayModel = null; StayEntryDTO? stayModel = null;
private string? CreateTimeString
{
get
{
if (stayModel is null)
{
return null;
}
List<string> textList = [];
textList.Add($"Created at: {stayModel.CreateTime?.ToString() ?? "???"}");
textList.Add($"Last Update Time: {stayModel.UpdateTime?.ToString() ?? "???"}");
return string.Join("; ", textList);
}
}
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
@* if (StayId is default) @* if (StayId is default)
@@ -54,7 +71,7 @@
stayModel = await stayService.GetDtoByIdAsync(StayId, queryConfig: new(IncludeGuests: true)); stayModel = await stayService.GetDtoByIdAsync(StayId, queryConfig: new(IncludeGuests: true));
if (stayModel is null) if (stayModel is null)
{ {
Snackbar.Add($"Stay not found with ID: '{StayId}'", Severity.Error); userMessage.ShowMessage($"Stay not found with ID: '{StayId}'", MessageKind.Error);
Navigation.NotFound(); Navigation.NotFound();
return; return;
} }
@@ -62,7 +79,7 @@
private void BtnEdit() private void BtnEdit()
{ {
Snackbar.Add("Not implemented yet!", Severity.Info); userMessage.ShowMessage("Not implemented yet!", MessageKind.Info);
} }
private void BtnReturn() private void BtnReturn()
@@ -3,9 +3,9 @@
@using Somnia.GuestLog.Models.DTOs; @using Somnia.GuestLog.Models.DTOs;
@using Somnia.GuestLog.Services; @using Somnia.GuestLog.Services;
@inject IStayEntryService StayEntryService @inject IStayEntryEntityService StayEntryService
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject ISnackbar Snackbar @inject IUserMessageService userMessage
@inject IDialogService DialogService @inject IDialogService DialogService
@inject IDataListService dataListService; @inject IDataListService dataListService;
@@ -73,7 +73,7 @@
private StayEntryFilterDTO stayFilter = new StayEntryFilterDTO() private StayEntryFilterDTO stayFilter = new StayEntryFilterDTO()
{ {
DateInStart = DateTime.Today.AddDays(-14), DateInStart = DateTime.Today.AddDays(-14),
DateInEnd = DateTime.Today, DateInEnd = DateTime.Today.AddDays(1),
}; };
private IEnumerable<StayEntryDTO> stayEntryList = []; private IEnumerable<StayEntryDTO> stayEntryList = [];
@@ -96,7 +96,7 @@
if (showMsg) if (showMsg)
{ {
Snackbar.Add("Data loaded from DB", Severity.Info, o => o.VisibleStateDuration = 1000); userMessage.ShowMessage("Data loaded from DB", MessageKind.Info);
} }
_dataLoading = false; _dataLoading = false;
} }
@@ -117,7 +117,7 @@
private async Task BtnViewDetails(StayEntryDTO item) private async Task BtnViewDetails(StayEntryDTO item)
{ {
string itemId = $"{item.Id ?? -1}"; string itemId = item.Id?.ToString() ?? throw new InvalidOperationException();
string uri = GuestConsts.Page_StayEntry_Detail.Replace(GuestConsts.Page_StayEntry_Detail_Param_Id, itemId); string uri = GuestConsts.Page_StayEntry_Detail.Replace(GuestConsts.Page_StayEntry_Detail_Param_Id, itemId);
Navigation.NavigateTo(uri); Navigation.NavigateTo(uri);
} }
@@ -131,14 +131,15 @@
return; return;
} }
bool deleted = await StayEntryService.DeleteEntryAsync(item); Result deleteResult = await StayEntryService.DeleteEntryAsync(item);
if (!deleted) if (!deleteResult.IsSuccess)
{ {
Snackbar.Add($"Error deleting Stay {item.Id}", Severity.Error); string errorMsg = $"Error deleting Stay {item.Id}";
userMessage.ShowResultWithMessage(deleteResult, errorMsg);
return; return;
} }
Snackbar.Add($"Stay {item.Id} deleted!", Severity.Success); userMessage.ShowMessage($"Stay {item.Id} deleted!", MessageKind.Success);
await LoadDataAsync(); await LoadDataAsync();
} }
@@ -4,10 +4,10 @@
@using Somnia.GuestLog.Services; @using Somnia.GuestLog.Services;
@inject ISnackbar Snackbar @inject IUserMessageService userMessage
@inject IDialogService DialogService @inject IDialogService DialogService
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject IStayEntryService stayService @inject IStayEntryEntityService stayService
@inject IDataListService dataListService; @inject IDataListService dataListService;
<PageTitle>Somnia - Stay Entries - New</PageTitle> <PageTitle>Somnia - Stay Entries - New</PageTitle>
@@ -27,7 +27,7 @@
</MudButton> </MudButton>
</MudStack> </MudStack>
<StayEntryForm @ref="_form" Model="@newStayModel" /> <StayEntryForm @ref="_form" Model="@newStayModel" FormMode="FormMode.Create" />
@code { @code {
StayEntryForm _form = null!; StayEntryForm _form = null!;
@@ -45,22 +45,22 @@
bool isFormValid = await _form.ValidateForm(); bool isFormValid = await _form.ValidateForm();
if (!isFormValid) if (!isFormValid)
{ {
Snackbar.Add("Invalid Form!", Severity.Warning); userMessage.ShowMessage("Invalid Form!", MessageKind.Warning);
return; return;
} }
var loadingDialog = await DialogService.ShowAsync<LoadingDialog>(); var loadingDialog = await DialogService.ShowAsync<LoadingDialog>();
StayEntryDTO model = _form.GetModel(); StayEntryDTO model = _form.GetModel();
bool isCreated = await stayService.CreateEntryAsync(model); Result createResult = await stayService.CreateEntryAsync(model);
if (!isCreated) if (!createResult.IsSuccess)
{ {
Snackbar.Add($"Error creating StayEntry", Severity.Error); userMessage.ShowResultWithMessage(createResult, $"Error creating StayEntry");
loadingDialog.Close(); loadingDialog.Close();
return; return;
} }
Snackbar.Add($"StayEntry Created!", Severity.Success); userMessage.ShowMessage("StayEntry Created: (Id: {0})", MessageKind.Success, createResult.Context ?? ["???"]);
Navigation.NavigateTo(GuestConsts.Page_StayEntry_List); Navigation.NavigateTo(GuestConsts.Page_StayEntry_List);
} }
@@ -0,0 +1,197 @@
@attribute [Route($"{GuestConsts.Page_StayExport_List}")]
@using System.Text.Json;
@using Somnia.GuestLog.Models.DTOs;
@using Somnia.GuestLog.Services;
@using Somnia.GuestLog.Services.StayExport;
@inject IStayExportFileEntityService stayExportEntityService
@inject NavigationManager Navigation
@inject IUserMessageService userMessage
@inject IDialogService DialogService
@inject IDataListService dataListService;
@inject ILocalizerService Localizer;
@inject IStayExportService ExportService;
@inject IJSRuntime JS
<PageTitle>Somnia - Stay Export Files</PageTitle>
<MudStack Row="true" Class="my-4">
<MudText Typo="Typo.h4" GutterBottom="true">@LanguageLocale.page_StayExportList_pageTitle</MudText>
<MudSpacer />
<MudButton Variant="Variant.Filled" Color="Color.Primary" Size="Size.Small"
StartIcon="@Icons.Material.Filled.Search" OnClick="() => BtnSearchDataAsync()">
@LanguageLocale.general_btnSearch
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Size="Size.Small" StartIcon="@Icons.Material.Filled.Add"
OnClick="() => BtnNewExport()">
@LanguageLocale.page_StayExportList_btnNewExport
</MudButton>
</MudStack>
@* <MudPaper Class="pa-4 my-4" Elevation="2" Height="100%">
FILTER
</MudPaper> *@
<MudDataGrid @ref="_dataGrid" T="StayExportFileDTO" Items="@stayExportList" Loading="@_dataLoading" ReadOnly="true"
Elevation="2">
<ToolBarContent>
<MudText Typo="Typo.subtitle1">@LanguageLocale.page_StayExportList_exportList</MudText>
</ToolBarContent>
<Columns>
<PropertyColumn Property="x => x.Id" Hideable="false" Title="@LanguageLocale.dto_StayExportFile_Id" />
<PropertyColumn Property="x => Localizer.GetEnumValue(x.ExportFormat)"
Title="@LanguageLocale.dto_StayExportFile_ExportFormat" />
<TemplateColumn Title="@LanguageLocale.dto_StayExportFile_ExportParams">
<CellTemplate>
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Outlined.Info"
OnClick="() => ShowExportParams(context.Item)" />
</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => Localizer.GetEnumValue(x.ExportResult)"
Title="@LanguageLocale.dto_StayExportFile_ExportResult" />
<PropertyColumn Property="x => x.ExportTime" Title="@LanguageLocale.dto_StayExportFile_ExportTime" />
<TemplateColumn Title="@LanguageLocale.dto_StayExportFile_ExportDetails">
<CellTemplate>
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Outlined.Info"
OnClick="() => ShowExportDetails(context.Item)" />
</CellTemplate>
</TemplateColumn>
<TemplateColumn CellClass="d-flex justify-between" Title="Options" StickyRight Hideable="false"
ShowColumnOptions="false">
<CellTemplate>
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Outlined.Download"
OnClick="() => BtnDownloadFile(context.Item)" />
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Outlined.Delete"
OnClick="() => BtnDeleteItem(context.Item)" />
</CellTemplate>
</TemplateColumn>
</Columns>
<PagerContent>
<MudDataGridPager T="StayExportFileDTO" />
</PagerContent>
</MudDataGrid>
<ConfirmDeleteMsgBox @ref="_confirmDeleteMsgBox" />
@code {
private bool _dataLoading = true;
private const int resultLimit = 200;
private IEnumerable<StayExportFileDTO> stayExportList = [];
private MudDataGrid<StayExportFileDTO> _dataGrid = null!;
private ConfirmDeleteMsgBox _confirmDeleteMsgBox = null!;
protected override async Task OnInitializedAsync()
{
await LoadDataAsync(showMsg: false);
}
private async Task LoadDataAsync(bool showMsg = true)
{
_dataLoading = true;
stayExportList = await stayExportEntityService.GetDtoListAsync(limit: resultLimit);
if (showMsg)
{
userMessage.ShowMessage("Data loaded from DB", MessageKind.Info);
}
_dataLoading = false;
}
#region Event Handlers
private async Task BtnSearchDataAsync()
{
await LoadDataAsync();
}
private void BtnNewExport()
{
Navigation.NavigateTo(GuestConsts.Page_StayExport_New);
}
private async Task ShowExportParams(StayExportFileDTO dto)
{
string jsonParams = JsonSerializer.Serialize(dto.ExportParams, options: new() { WriteIndented = true });
await DialogService.ShowMessageBoxAsync("Details", (MarkupString)$"<pre>{jsonParams}</pre>");
}
private async Task ShowExportDetails(StayExportFileDTO dto)
{
if (string.IsNullOrEmpty(dto.ExportDetails))
{
userMessage.ShowMessage("This ExportFile doesn't have any export details", MessageKind.Info);
return;
}
using var jDoc = JsonDocument.Parse(dto.ExportDetails);
string jsonDetails = JsonSerializer.Serialize(jDoc, new JsonSerializerOptions { WriteIndented = true });
await DialogService.ShowMessageBoxAsync("Details", (MarkupString)$"<pre>{jsonDetails}</pre>");
}
private async Task BtnDownloadFile(StayExportFileDTO item)
{
if (item.ExportResult == Enums.Models.StayExportFileResult.NoData)
{
userMessage.ShowMessage("A Export without data can't be downloaded!", MessageKind.Warning);
return;
}
if (string.IsNullOrEmpty(item.ExportedPath))
{
userMessage.ShowMessage("File can't be downloaded: no path found!", MessageKind.Warning);
return;
}
using Stream objectStream = new MemoryStream();
Result loadResult = await ExportService.GetExportedFileAsync(item, objectStream);
if (!loadResult.IsSuccess)
{
userMessage.ShowResultWithMessage(loadResult, "Exported File can't be downloaded");
return;
}
objectStream.Seek(0, SeekOrigin.Begin);
using var streamRef = new DotNetStreamReference(stream: objectStream);
var fileName = Path.GetFileName(item.ExportedPath);
await JS.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef);
userMessage.ShowMessage("Exported File downloaded!", MessageKind.Success);
}
private async Task BtnDeleteItem(StayExportFileDTO item)
{
bool? confirmDelete = await _confirmDeleteMsgBox.MsgBox.ShowAsync();
if (confirmDelete is null || confirmDelete.Value == false)
{
return;
}
Result deleteResult = await ExportService.DeleteExportedFileAsync(item);
if (!deleteResult.IsSuccess)
{
string errorMsg = $"Error deleting Export file {item.Id}";
userMessage.ShowResultWithMessage(deleteResult, errorMsg);
return;
}
userMessage.ShowMessage($"Export file {item.Id} deleted!", MessageKind.Success);
await LoadDataAsync();
}
#endregion
}
@@ -0,0 +1,162 @@
@attribute [Route($"{GuestConsts.Page_StayExport_New}")]
@using FluentValidation;
@using Somnia.GuestLog.Models.DTOs;
@using Somnia.GuestLog.Enums.Models;
@using Somnia.GuestLog.Services;
@using Somnia.GuestLog.Validators;
@using Somnia.GuestLog.Services.StayExport;
@inject IUserMessageService userMessage
@inject IDialogService DialogService
@inject NavigationManager Navigation
@inject IValidator<StayExportParamsDTO> ExportParamsValidator;
@inject ILocalizerService Localizer;
@inject IStayExportService ExportService;
@inject IJSRuntime JS
<PageTitle>Somnia - Stay Export - New</PageTitle>
<MudStack Row="true" Class="my-4">
<MudText Typo="Typo.h4" GutterBottom="true">
@LanguageLocale.page_StayExportNew_pageTitle
</MudText>
<MudSpacer />
<MudButton Variant="Variant.Filled" Color="Color.Success" Size="Size.Small" StartIcon="@Icons.Material.Filled.Add"
OnClick="() => BtnCreateExport()" Disabled="@_isExported">
@LanguageLocale.general_btnCreate
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Default" Size="Size.Small"
StartIcon="@Icons.Material.Filled.ArrowBack" OnClick="() => BtnReturn()">
@LanguageLocale.general_btnReturn
</MudButton>
</MudStack>
<MudForm @ref="@_form" Model="@exportParams"
Validation="@((ExportParamsValidator as StayExportParamsDTOValidator)!.ValidateValue)" ReadOnly="@_isExported">
<MudPaper Class="pa-4 my-4" Elevation="3" Height="100%">
<MudStack Row style="margin-block: 0.2em;">
<MudSelect @bind-Value="exportFormat" Label="@LanguageLocale.dto_StayExportFile_ExportFormat" FullWidth
Clearable>
@foreach (StayExportFormat option in Enum.GetValues<StayExportFormat>())
{
<MudSelectItem Value="new Nullable<StayExportFormat>(option)">
@Localizer.GetEnumValue(option)
</MudSelectItem>
}
</MudSelect>
<MudSelect @bind-Value="exportParams.ExportType" For="@(() => exportParams.ExportType)"
Label="@LanguageLocale.dto_StayExportParams_ExportType" FullWidth Clearable>
@foreach (StayExportParamType option in Enum.GetValues<StayExportParamType>())
{
<MudSelectItem Value="new Nullable<StayExportParamType>(option)">
@Localizer.GetEnumValue(option)
</MudSelectItem>
}
</MudSelect>
</MudStack>
</MudPaper>
@if (_showPeriodFields)
{
<MudPaper Class="pa-4" Elevation="3" Height="100%">
<MudStack Row>
<MudDatePicker @bind-Date="exportParams.ExportPeriodStart" For="@(() => exportParams.ExportPeriodStart)"
Label="@LanguageLocale.dto_StayExportParams_ExportPeriodStart" Clearable OpenTo="OpenTo.Date" Editable
AutoClose="true" />
<MudDatePicker @bind-Date="exportParams.ExportPeriodEnd" For="@(() => exportParams.ExportPeriodEnd)"
Label="@LanguageLocale.dto_StayExportParams_ExportPeriodEnd" Clearable OpenTo="OpenTo.Date" Editable
AutoClose="true" />
</MudStack>
</MudPaper>
}
</MudForm>
@if (_isExported && exportedFile != null)
{
<MudPaper Class="pa-4 my-6" Elevation="3" Height="100%">
<MudStack>
<MudText Typo="Typo.h4">File Exported!</MudText>
<MudTextField Value="@Localizer.GetEnumValue(exportedFile.ExportResult)"
Label="@LanguageLocale.dto_StayExportFile_ExportResult" FullWidth ReadOnly="true" />
<MudTextField Value="@Localizer.GetEnumValue(exportedFile.ExportFormat)"
Label="@LanguageLocale.dto_StayExportFile_ExportFormat" FullWidth ReadOnly="true" />
<MudButton OnClick="BtnDownloadExportedFile">Download Exported File</MudButton>
</MudStack>
</MudPaper>
}
@code {
MudForm _form = null!;
StayExportFormat? exportFormat = null;
StayExportParamsDTO exportParams = new();
StayExportFileDTO? exportedFile = null;
bool _showPeriodFields => exportParams.ExportType == StayExportParamType.PeriodDateIn;
bool _isExported => exportedFile != null;
private void BtnReturn()
{
Navigation.NavigateTo(GuestConsts.Page_StayExport_List);
}
private async Task BtnCreateExport()
{
await _form.ValidateAsync();
if (!_form.IsValid || exportFormat == null)
{
userMessage.ShowMessage("Invalid Form!", MessageKind.Warning);
return;
}
var loadingDialog = await DialogService.ShowAsync<LoadingDialog>();
var exportResult = await ExportService.ExportStaysByParamsAsync(exportFormat.Value, exportParams);
exportedFile = exportResult.Value;
loadingDialog.Close();
if (!exportResult.IsSuccess)
{
userMessage.ShowResultWithMessage(exportResult, $"Error exporting stays");
return;
}
userMessage.ShowMessage(
$"Stay exported: {exportResult.ResultCode} with export result: {exportedFile?.ExportResult}",
MessageKind.Success);
}
private async Task BtnDownloadExportedFile()
{
Debug.Assert(exportedFile != null);
if (string.IsNullOrEmpty(exportedFile.ExportedPath))
{
userMessage.ShowMessage("Exported File can't be downloaded as no file was saved", MessageKind.Warning);
return;
}
using Stream dataStream = new MemoryStream();
Result loadResult = await ExportService.GetExportedFileAsync(exportedFile, dataStream);
if (!loadResult.IsSuccess)
{
userMessage.ShowResultWithMessage(loadResult, "Exported File can't be downloaded");
return;
}
dataStream.Seek(0, SeekOrigin.Begin);
using var streamRef = new DotNetStreamReference(stream: dataStream);
var fileName = Path.GetFileName(exportedFile.ExportedPath);
await JS.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef);
userMessage.ShowMessage("Exported File downloaded!", MessageKind.Success);
}
}
@@ -1 +1,3 @@
@using Somnia.GuestLog.Components.Controls @using Somnia.GuestLog.Components.Controls
@using Somnia.GuestLog.Services.Entity;
@using System.Diagnostics;
@@ -1,5 +1,6 @@
@using System.Net.Http @using System.Net.Http
@using System.Net.Http.Json @using System.Net.Http.Json
@using System.Linq;
@using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web
@@ -15,4 +16,4 @@
@using Somnia.GuestLog.Components.Forms @using Somnia.GuestLog.Components.Forms
@using Somnia.GuestLog.Components.Controls @using Somnia.GuestLog.Components.Controls
@using Somnia.GuestLog.Resources @using Somnia.GuestLog.Resources
@using System.Linq; @using Somnia.GuestLog.Enums.UI
@@ -0,0 +1,50 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Serilog": {
"Using": [
"Serilog.Sinks.Console",
"Serilog.Sinks.File"
],
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
}
},
"Enrich": [
"FromLogContext",
"WithThreadId",
"WithProcessId"
],
"Properties": {
"ApplicationName": "Somnia-GuestLog"
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"restrictedToMinimumLevel": "Information"
}
},
{
"Name": "File",
"Args": {
// Logs data path
"path": "/SomniaGuestLog/logs/log_.log",
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{ProcessId}-{ThreadId}] [{Level}] {JobKey}{Message}{NewLine}{Exception}",
"rollingInterval": "Day",
"rollOnFileSizeLimit": true,
"fileSizeLimitBytes": 1073741824,
"retainedFileCountLimit": 30,
"restrictedToMinimumLevel": "Information"
}
}
]
}
}
@@ -0,0 +1,9 @@
namespace Somnia.GuestLog.Enums.Models;
public enum StayExportFileResult
{
OK = 0,
NoData = 1,
ExportError = 2,
UploadError = 3,
}
@@ -0,0 +1,7 @@
namespace Somnia.GuestLog.Enums.Models;
public enum StayExportFormat
{
GuestLogJson = 0,
SpainParteViajeros = 1,
}
@@ -0,0 +1,6 @@
namespace Somnia.GuestLog.Enums.Models;
public enum StayExportParamType
{
PeriodDateIn = 0,
}
@@ -0,0 +1,7 @@
namespace Somnia.GuestLog.Enums.Services;
public enum AddressQueryProvider
{
Nominatim,
GoogleApiV4
}
@@ -0,0 +1,15 @@
namespace Somnia.GuestLog.Enums.Services;
public enum ResultCode
{
//Common Codes
Ok,
UnknownError,
FailedValidation,
NotFound,
FileStorageError,
//Specific Codes
GuestInStay,
GuestCurrentlyInStay,
}
+6
View File
@@ -0,0 +1,6 @@
namespace Somnia.GuestLog.Enums.UI;
public enum FormMode
{
Create, ReadOnly, Edit
}
@@ -0,0 +1,6 @@
namespace Somnia.GuestLog.Enums.UI;
public enum MessageKind
{
Info, Warning, Error, Success, Normal
}
+13 -1
View File
@@ -6,13 +6,16 @@ public static class GuestConsts
public const string Page_GuestDocs_List = "/guest-documents"; public const string Page_GuestDocs_List = "/guest-documents";
public const string Page_GuestDocs_New = "/guest-documents/new"; public const string Page_GuestDocs_New = "/guest-documents/new";
public const string Page_GuestDocs_Detail = $"/guest-documents/d/{Page_GuestDocs_Detail_Param_DocId}"; public const string Page_GuestDocs_Detail = $"/guest-documents/d/{Page_GuestDocs_Detail_Param_DocId}";
public const string Page_GuestDocs_Detail_Param_DocId = "{docId}"; public const string Page_GuestDocs_Detail_Param_DocId = "{docId:long}";
public const string Page_StayEntry_List = "/stayEntries"; public const string Page_StayEntry_List = "/stayEntries";
public const string Page_StayEntry_New = "/stayEntries/new"; public const string Page_StayEntry_New = "/stayEntries/new";
public const string Page_StayEntry_Detail = $"/stayEntries/d/{Page_StayEntry_Detail_Param_Id}"; public const string Page_StayEntry_Detail = $"/stayEntries/d/{Page_StayEntry_Detail_Param_Id}";
public const string Page_StayEntry_Detail_Param_Id = "{StayId:long}"; public const string Page_StayEntry_Detail_Param_Id = "{StayId:long}";
public const string Page_StayExport_List = "/stayExport";
public const string Page_StayExport_New = "/stayExport/new";
public const string QueryParam_ReturnUrl = "returnUrl"; public const string QueryParam_ReturnUrl = "returnUrl";
@@ -28,4 +31,13 @@ public static class GuestConsts
public const string DocumentType_DNI = "DNI"; public const string DocumentType_DNI = "DNI";
public const string DocumentType_NIE = "NIE"; public const string DocumentType_NIE = "NIE";
public const string DocumentType_PASSPORT = "PASSPORT"; public const string DocumentType_PASSPORT = "PASSPORT";
public const string GenderOptions_MALE = "M";
public const string GenderOptions_FEMALE = "F";
public const string GenderOptions_OTHER = "O";
public const int GuestDocument_FirstVersion = 1;
public const string EmptyField = "-";
} }
@@ -5,4 +5,8 @@ public static class DbConsts
public const string DbType_Timestamptz = "timestamp with time zone"; public const string DbType_Timestamptz = "timestamp with time zone";
public const string DbType_Timestamptz_0 = "timestamp(0) with time zone"; public const string DbType_Timestamptz_0 = "timestamp(0) with time zone";
public const string DbType_Timestamptz_3 = "timestamp(3) with time zone"; public const string DbType_Timestamptz_3 = "timestamp(3) with time zone";
public const string DbType_jsonb = "jsonb";
public const string PK_NotApply = "N/A";
} }
@@ -11,7 +11,13 @@ public class GuestDocumentEntityConfig : IEntityTypeConfiguration<GuestDocument>
builder.ToTable("GuestDocument"); builder.ToTable("GuestDocument");
//PK //PK
builder.HasKey(e => new { e.DocumentType, e.DocumentNum }); builder.HasKey(e => new { e.Id, e.Version });
builder.Property(e => e.Id)
.UseIdentityByDefaultColumn();
builder.Property(e => e.Version)
.IsRequired();
builder.Property(e => e.DocumentType) builder.Property(e => e.DocumentType)
.IsRequired() .IsRequired()
@@ -48,6 +54,15 @@ public class GuestDocumentEntityConfig : IEntityTypeConfiguration<GuestDocument>
builder.Property(e => e.EmailAddress) builder.Property(e => e.EmailAddress)
.HasMaxLength(256); .HasMaxLength(256);
builder.Property(e => e.CreateTime)
.IsRequired()
.HasColumnType(DbConsts.DbType_Timestamptz_3);
builder.Property(e => e.UpdateTime)
.IsRequired()
.HasColumnType(DbConsts.DbType_Timestamptz_3);
builder.Property(e => e.IsLastVersion)
.IsRequired();
var addressBuilder = builder.ComplexProperty(e => e.AddressDetails); var addressBuilder = builder.ComplexProperty(e => e.AddressDetails);
@@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Somnia.GuestLog.Models.Entities;
namespace Somnia.GuestLog.Infrastructure.EntityConfigs;
public class StayExportFileEntityConfig : IEntityTypeConfiguration<StayExportFile>
{
public void Configure(EntityTypeBuilder<StayExportFile> builder)
{
builder.ToTable("StayExportFile");
//PK
builder.HasKey(e => e.Id);
builder.Property(e => e.Id)
.IsRequired()
.UseIdentityByDefaultColumn();
builder.ComplexProperty(e => e.ExportParams, e =>
{
e.ToJson();
});
builder.Property(e => e.ExportedPath)
.HasMaxLength(256);
builder.Property(e => e.ExportDetails)
.HasColumnType(DbConsts.DbType_jsonb);
builder.Property(e => e.ExportTime)
.IsRequired()
.HasColumnType(DbConsts.DbType_Timestamptz);
}
}
@@ -13,11 +13,14 @@ public partial class GuestLogContext : DbContext
public DbSet<GuestDocument> GuestDocuments { get; set; } public DbSet<GuestDocument> GuestDocuments { get; set; }
public DbSet<StayEntry> StayEntries { get; set; } public DbSet<StayEntry> StayEntries { get; set; }
public DbSet<StayExportFile> StayExportFiles { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
modelBuilder.ApplyConfiguration<GuestDocument>(new GuestDocumentEntityConfig()); modelBuilder.ApplyConfiguration<GuestDocument>(new GuestDocumentEntityConfig());
modelBuilder.ApplyConfiguration<StayEntry>(new StayEntryEntityConfig()); modelBuilder.ApplyConfiguration<StayEntry>(new StayEntryEntityConfig());
modelBuilder.ApplyConfiguration<StayExportFile>(new StayExportFileEntityConfig());
OnModelCreatingPartial(modelBuilder); OnModelCreatingPartial(modelBuilder);
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
@@ -0,0 +1,205 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Somnia.GuestLog.Infrastructure;
#nullable disable
namespace Somnia.GuestLog.Infrastructure.Migrations
{
[DbContext(typeof(GuestLogContext))]
[Migration("20260830201743_GuestDocument_ChangePKToIdAndVersion")]
partial class GuestDocument_ChangePKToIdAndVersion
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.7")
.HasAnnotation("Proxies:ChangeTracking", false)
.HasAnnotation("Proxies:CheckEquality", false)
.HasAnnotation("Proxies:LazyLoading", true)
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("GuestDocumentStayEntry", b =>
{
b.Property<long>("StayEntryId")
.HasColumnType("bigint");
b.Property<long>("GuestDocumentsId")
.HasColumnType("bigint");
b.Property<int>("GuestDocumentsVersion")
.HasColumnType("integer");
b.HasKey("StayEntryId", "GuestDocumentsId", "GuestDocumentsVersion");
b.HasIndex("GuestDocumentsId", "GuestDocumentsVersion");
b.ToTable("GuestDocumentStayEntry");
});
modelBuilder.Entity("Somnia.GuestLog.Models.Entities.GuestDocument", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<int>("Version")
.HasColumnType("integer");
b.Property<DateOnly>("BirthDate")
.HasColumnType("date");
b.Property<DateTime>("CreateTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("DocumentNum")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("DocumentType")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("EmailAddress")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Gender")
.HasMaxLength(1)
.HasColumnType("character varying(1)");
b.Property<DateOnly>("IssueDate")
.HasColumnType("date");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Nationality")
.IsRequired()
.HasMaxLength(3)
.HasColumnType("character varying(3)");
b.Property<string>("PhoneNumber")
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("SupportNum")
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<string>("Surname")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Surname2")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<DateTime>("UpdateTime")
.HasColumnType("timestamp with time zone");
b.ComplexProperty(typeof(Dictionary<string, object>), "AddressDetails", "Somnia.GuestLog.Models.Entities.GuestDocument.AddressDetails#GuestAddress", b1 =>
{
b1.Property<string>("Address")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b1.Property<string>("AddressCity")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b1.Property<string>("AddressCountry")
.IsRequired()
.HasMaxLength(3)
.HasColumnType("character varying(3)");
b1.Property<string>("AddressProvince")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b1.Property<string>("AddressState")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b1.Property<string>("ZipCode")
.HasMaxLength(12)
.HasColumnType("character varying(12)");
});
b.HasKey("Id", "Version");
b.ToTable("GuestDocument", (string)null);
});
modelBuilder.Entity("Somnia.GuestLog.Models.Entities.StayEntry", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreateTime")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("DateIn")
.HasColumnType("timestamp(0) with time zone");
b.Property<DateTime>("DateOut")
.HasColumnType("timestamp(0) with time zone");
b.Property<string>("PaymentMethod")
.IsRequired()
.HasMaxLength(5)
.HasColumnType("character varying(5)");
b.Property<int>("RoomCount")
.HasColumnType("integer");
b.Property<string>("RoomNumber")
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<DateTime>("UpdateTime")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("StayEntry", (string)null);
});
modelBuilder.Entity("GuestDocumentStayEntry", b =>
{
b.HasOne("Somnia.GuestLog.Models.Entities.StayEntry", null)
.WithMany()
.HasForeignKey("StayEntryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Somnia.GuestLog.Models.Entities.GuestDocument", null)
.WithMany()
.HasForeignKey("GuestDocumentsId", "GuestDocumentsVersion")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,101 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Somnia.GuestLog.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class GuestDocument_ChangePKToIdAndVersion : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
//Data don't really matter right now. Just get it working good.
//This migration will destry all data, but it's fine.
migrationBuilder.DropTable(
name: "GuestDocumentStayEntry"
);
migrationBuilder.Sql("""DELETE FROM "StayEntry";""");
migrationBuilder.DropTable(
name: "GuestDocument"
);
migrationBuilder.CreateTable(
name: "GuestDocument",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Version = table.Column<int>(type: "integer", nullable: false),
DocumentType = table.Column<string>(type: "character varying(8)", maxLength: 8, nullable: false),
DocumentNum = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
IssueDate = table.Column<DateOnly>(type: "date", nullable: false),
SupportNum = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: true),
Nationality = table.Column<string>(type: "character varying(3)", maxLength: 3, nullable: false),
Name = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
Surname = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
Surname2 = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
BirthDate = table.Column<DateOnly>(type: "date", nullable: false),
Gender = table.Column<string>(type: "character varying(1)", maxLength: 1, nullable: true),
PhoneNumber = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
EmailAddress = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
CreateTime = table.Column<DateTime>(type: "timestamp(3) with time zone", nullable: false),
UpdateTime = table.Column<DateTime>(type: "timestamp(3) with time zone", nullable: false),
IsLastVersion = table.Column<bool>(type: "boolean", nullable: false),
AddressDetails_Address = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
AddressDetails_AddressCity = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
AddressDetails_AddressCountry = table.Column<string>(type: "character varying(3)", maxLength: 3, nullable: true),
AddressDetails_AddressProvince = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
AddressDetails_AddressState = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
AddressDetails_ZipCode = table.Column<string>(type: "character varying(12)", maxLength: 12, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_GuestDocument", x => new { x.Id, x.Version });
}
);
//Existing index for "DocType" and "DocNum" can be useful, so let's keep it.
migrationBuilder.CreateTable(
name: "GuestDocumentStayEntry",
columns: table => new
{
StayEntryId = table.Column<long>(type: "bigint", nullable: false),
GuestDocumentsId = table.Column<long>(type: "bigint", nullable: false),
GuestDocumentsVersion = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_GuestDocumentStayEntry", x => new { x.StayEntryId, x.GuestDocumentsId, x.GuestDocumentsVersion });
table.ForeignKey(
name: "FK_GuestDocumentStayEntry_GuestDocument_GuestDocumentsId_Guest~",
columns: x => new { x.GuestDocumentsId, x.GuestDocumentsVersion },
principalTable: "GuestDocument",
principalColumns: new[] { "Id", "Version" },
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_GuestDocumentStayEntry_StayEntry_StayEntryId",
column: x => x.StayEntryId,
principalTable: "StayEntry",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_GuestDocumentStayEntry_GuestDocumentsId_GuestDocumentsVersi~",
table: "GuestDocumentStayEntry",
columns: new[] { "GuestDocumentsId", "GuestDocumentsVersion" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
throw new NotSupportedException("Rollback is not supported for this migration!");
}
}
}
@@ -0,0 +1,249 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Somnia.GuestLog.Infrastructure;
#nullable disable
namespace Somnia.GuestLog.Infrastructure.Migrations
{
[DbContext(typeof(GuestLogContext))]
[Migration("20260901161146_AddTable_StayExportFile")]
partial class AddTable_StayExportFile
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.7")
.HasAnnotation("Proxies:ChangeTracking", false)
.HasAnnotation("Proxies:CheckEquality", false)
.HasAnnotation("Proxies:LazyLoading", true)
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("GuestDocumentStayEntry", b =>
{
b.Property<long>("StayEntryId")
.HasColumnType("bigint");
b.Property<long>("GuestDocumentsId")
.HasColumnType("bigint");
b.Property<int>("GuestDocumentsVersion")
.HasColumnType("integer");
b.HasKey("StayEntryId", "GuestDocumentsId", "GuestDocumentsVersion");
b.HasIndex("GuestDocumentsId", "GuestDocumentsVersion");
b.ToTable("GuestDocumentStayEntry");
});
modelBuilder.Entity("Somnia.GuestLog.Models.Entities.GuestDocument", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<int>("Version")
.HasColumnType("integer");
b.Property<DateOnly>("BirthDate")
.HasColumnType("date");
b.Property<DateTime>("CreateTime")
.HasColumnType("timestamp(3) with time zone");
b.Property<string>("DocumentNum")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("DocumentType")
.IsRequired()
.HasMaxLength(8)
.HasColumnType("character varying(8)");
b.Property<string>("EmailAddress")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Gender")
.HasMaxLength(1)
.HasColumnType("character varying(1)");
b.Property<bool>("IsLastVersion")
.HasColumnType("boolean");
b.Property<DateOnly>("IssueDate")
.HasColumnType("date");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Nationality")
.IsRequired()
.HasMaxLength(3)
.HasColumnType("character varying(3)");
b.Property<string>("PhoneNumber")
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("SupportNum")
.HasMaxLength(10)
.HasColumnType("character varying(10)");
b.Property<string>("Surname")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Surname2")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<DateTime>("UpdateTime")
.HasColumnType("timestamp(3) with time zone");
b.ComplexProperty(typeof(Dictionary<string, object>), "AddressDetails", "Somnia.GuestLog.Models.Entities.GuestDocument.AddressDetails#GuestAddress", b1 =>
{
b1.Property<string>("Address")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b1.Property<string>("AddressCity")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b1.Property<string>("AddressCountry")
.IsRequired()
.HasMaxLength(3)
.HasColumnType("character varying(3)");
b1.Property<string>("AddressProvince")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b1.Property<string>("AddressState")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b1.Property<string>("ZipCode")
.HasMaxLength(12)
.HasColumnType("character varying(12)");
});
b.HasKey("Id", "Version");
b.ToTable("GuestDocument", (string)null);
});
modelBuilder.Entity("Somnia.GuestLog.Models.Entities.StayEntry", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreateTime")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("DateIn")
.HasColumnType("timestamp(0) with time zone");
b.Property<DateTime>("DateOut")
.HasColumnType("timestamp(0) with time zone");
b.Property<string>("PaymentMethod")
.IsRequired()
.HasMaxLength(5)
.HasColumnType("character varying(5)");
b.Property<int>("RoomCount")
.HasColumnType("integer");
b.Property<string>("RoomNumber")
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<DateTime>("UpdateTime")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("StayEntry", (string)null);
});
modelBuilder.Entity("Somnia.GuestLog.Models.Entities.StayExportFile", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<int>("ExportFormat")
.HasColumnType("integer");
b.Property<int>("ExportResult")
.HasColumnType("integer");
b.Property<DateTime>("ExportTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("ExportedPath")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.ComplexProperty(typeof(Dictionary<string, object>), "ExportParams", "Somnia.GuestLog.Models.Entities.StayExportFile.ExportParams#StayExportParams", b1 =>
{
b1.IsRequired();
b1.Property<DateTime?>("ExportPeriodEnd");
b1.Property<DateTime?>("ExportPeriodStart");
b1.Property<int>("ExportType");
b1
.ToJson("ExportParams")
.HasColumnType("jsonb");
});
b.HasKey("Id");
b.ToTable("StayExportFile", (string)null);
});
modelBuilder.Entity("GuestDocumentStayEntry", b =>
{
b.HasOne("Somnia.GuestLog.Models.Entities.StayEntry", null)
.WithMany()
.HasForeignKey("StayEntryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Somnia.GuestLog.Models.Entities.GuestDocument", null)
.WithMany()
.HasForeignKey("GuestDocumentsId", "GuestDocumentsVersion")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,40 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Somnia.GuestLog.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddTable_StayExportFile : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "StayExportFile",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ExportFormat = table.Column<int>(type: "integer", nullable: false),
ExportResult = table.Column<int>(type: "integer", nullable: false),
ExportedPath = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
ExportTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
ExportParams = table.Column<string>(type: "jsonb", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_StayExportFile", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "StayExportFile");
}