diff --git a/src/Somnia.GuestLog/Components/Controls/GuestDocForm.razor b/src/Somnia.GuestLog/Components/Controls/GuestDocForm.razor
index b429098..69191eb 100644
--- a/src/Somnia.GuestLog/Components/Controls/GuestDocForm.razor
+++ b/src/Somnia.GuestLog/Components/Controls/GuestDocForm.razor
@@ -15,29 +15,31 @@
@LanguageLocale.control_GuestDocForm_sectionDocument
-
+
+ Required ReadOnly="true">
@foreach (string docType in dataListService.GetDataListByName(GuestConsts.DataList_DocumentTypes))
{
@dataListService.GetLocalizedValue(GuestConsts.DataList_DocumentTypes, docType)
}
+ Immediate="true" Required ReadOnly="true" />
-
+
+ Immediate="true" Required="@isSuportNumberRequired" Mask="@(new PatternMask("aaa000000"))"
+ OnBlur="@(() => Model.SupportNum = Model.SupportNum?.ToUpper())" />
+ SelectValueOnTab="true" AdornmentIcon="@Icons.Material.Filled.Search"
+ style="@FieldMarginY" />
@@ -45,13 +47,17 @@
@LanguageLocale.control_GuestDocForm_sectionGuest
-
-
-
+
+
+
-
+
@@ -71,10 +77,12 @@
-
-
+
+
@@ -85,16 +93,17 @@
-
-
-
-
+
+
+
+
-
-
+
@@ -117,9 +126,10 @@
private MudForm _form = null!;
+ private const string FieldMarginY = "margin-block: 0.5em";
private Dictionary? countryDict = null;
- private bool isSuportNumberRequired { get => Model?.Nationality == "ESP"; }
+ private bool isSuportNumberRequired { get => Model?.Nationality == GuestConsts.CountryCode_ESP; }
private string CreateTimeString
{
get
@@ -146,11 +156,13 @@
private async Task> SearchCountryCodes(string value, CancellationToken token)
{
// if text is null or empty, show complete list
- if (string.IsNullOrEmpty(value)) {
+ if (string.IsNullOrEmpty(value))
+ {
return new string[0];
}
- if(countryDict is null || countryDict.Count == 0) {
+ if (countryDict is null || countryDict.Count == 0)
+ {
var countryCodeSet = dataListService.GetDataListByName(GuestConsts.DataList_CountryCodes);
countryDict = new(countryCodeSet.Count);
diff --git a/src/Somnia.GuestLog/Components/Pages/GuestDocuments/GuestDocDetail.razor b/src/Somnia.GuestLog/Components/Pages/GuestDocuments/GuestDocDetail.razor
new file mode 100644
index 0000000..1887437
--- /dev/null
+++ b/src/Somnia.GuestLog/Components/Pages/GuestDocuments/GuestDocDetail.razor
@@ -0,0 +1,81 @@
+@attribute [Route($"{GuestConsts.Page_GuestDocs_Detail}")]
+
+@using Somnia.GuestLog.Models.DTOs;
+@using Somnia.GuestLog.Services;
+
+@inject ISnackbar Snackbar
+@inject IDialogService DialogService
+@inject NavigationManager Navigation
+@inject IGuestDocumentService guestDocService
+
+
+@inject FluentValidation.IValidator docIdValidator;
+
+
+Somnia - Guest Docs - Details
+
+
+ Guest Document - "@docId"
+
+
+ @LanguageLocale.general_btnEdit
+
+
+ @LanguageLocale.general_btnReturn
+
+
+
+@if (documentModel is not null)
+{
+
+}
+
+@code {
+
+ [Parameter]
+ public string? docId { get; set; }
+
+ GuestDocForm _form = null!;
+
+ GuestDocumentDTO? documentModel = null;
+
+
+ protected override async Task OnInitializedAsync()
+ {
+ if (docId is null)
+ {
+ Snackbar.Add("No GuestDocument provided!!", Severity.Error);
+ Navigation.NotFound();
+ return;
+ }
+
+ GuestDocIdDTO? docIdModel = GuestDocIdDTO.DecodeFromString(docId);
+ if (docIdModel is null)
+ {
+ Snackbar.Add($"Provided GuestDocId can't be decoded: '{docId}'", Severity.Error);
+ Navigation.NotFound();
+ return;
+ }
+
+ //Get model from db.
+ documentModel = await guestDocService.GetDtoByIdAsync(docIdModel.DocumentType, docIdModel.DocumentNum);
+ if (documentModel is null)
+ {
+ Snackbar.Add($"GuestDocument not found with ID: '{docId}'", Severity.Error);
+ Navigation.NotFound();
+ return;
+ }
+ }
+
+ private void BtnEdit()
+ {
+ Snackbar.Add("Not implemented yet!", Severity.Info);
+ }
+
+ private void BtnReturn()
+ {
+ Navigation.NavigateTo(GuestConsts.Page_GuestDocs_List);
+ }
+}
\ No newline at end of file
diff --git a/src/Somnia.GuestLog/Components/Pages/GuestDocuments/GuestDocNew.razor b/src/Somnia.GuestLog/Components/Pages/GuestDocuments/GuestDocNew.razor
index 71cb331..9ba6115 100644
--- a/src/Somnia.GuestLog/Components/Pages/GuestDocuments/GuestDocNew.razor
+++ b/src/Somnia.GuestLog/Components/Pages/GuestDocuments/GuestDocNew.razor
@@ -1,15 +1,16 @@
@attribute [Route($"{GuestConsts.Page_GuestDocs_New}")]
-@using Microsoft.EntityFrameworkCore;
@using Somnia.GuestLog.Models.DTOs;
-@using Somnia.GuestLog.Infrastructure;
@using Somnia.GuestLog.Services;
+@using Somnia.GuestLog.Validators;
-@inject IDbContextFactory contextFactory
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject NavigationManager Navigation
@inject IGuestDocumentService guestDocService
+@inject IDataListService dataListService;
+
+@inject FluentValidation.IValidator docIdValidator;
Somnia - Guest Docs - New
@@ -26,16 +27,98 @@
@LanguageLocale.general_btnReturn
-
+
+@if (currentPageMode == ePageMode.selectDocId || currentPageMode == ePageMode.alreadyExistDoc)
+{
+
+
+
+
+
+ @foreach (string docType in dataListService.GetDataListByName(GuestConsts.DataList_DocumentTypes))
+ {
+
+ @dataListService.GetLocalizedValue(GuestConsts.DataList_DocumentTypes, docType)
+
+ }
+
+
+
+
+ @LanguageLocale.general_btnSearch
+
+
+
+
+ @if (currentPageMode == ePageMode.alreadyExistDoc)
+ {
+
+
+ @LanguageLocale.page_GuestDocNew_msgAlreadyExists
+
+
+ @LanguageLocale.page_GuestDocNew_btnGoExistingDoc
+
+
+ }
+
+}
+
+@if (currentPageMode == ePageMode.createNew)
+{
+
+}
@code {
-
GuestDocForm _form = null!;
+ MudForm _docIdForm = null!;
+ ePageMode currentPageMode = ePageMode.selectDocId;
+ GuestDocIdDTO currentDocId = new() { DocumentNum = "", DocumentType = "" };
+ GuestDocumentDTO newDocumentModel = new();
+
+ bool loadingDocId = false;
#region Event Handlers
+ ///
+ /// Check if given Document already exists or can be created.
+ ///
+ private async Task BtnCheckDocId()
+ {
+ await _docIdForm.ValidateAsync();
+ if (!_docIdForm.IsValid)
+ {
+ return;
+ }
+
+ loadingDocId = true;
+ GuestDocumentDTO? dto = await guestDocService.GetDtoByIdAsync(currentDocId.DocumentType, currentDocId.DocumentNum);
+ loadingDocId = false;
+
+ if (dto is null)
+ {
+ //Document don't exist -> Create it.
+ currentPageMode = ePageMode.createNew;
+
+ //Update model with current DocId
+ newDocumentModel.DocumentType = currentDocId.DocumentType;
+ newDocumentModel.DocumentNum = currentDocId.DocumentNum;
+
+ return;
+ }
+
+ //GuestDoc already exists!
+ currentPageMode = ePageMode.alreadyExistDoc;
+ }
+
private void BtnReturn()
{
Navigation.NavigateTo(GuestConsts.Page_GuestDocs_List);
@@ -69,6 +152,27 @@
Navigation.NavigateTo(GuestConsts.Page_GuestDocs_List);
}
+ private void BtnGoToDocument()
+ {
+ if (currentPageMode != ePageMode.alreadyExistDoc)
+ {
+ Snackbar.Add("Invalid operation!", Severity.Error);
+ return;
+ }
+
+ string docId = GuestDocIdDTO.EncodeAsString(currentDocId);
+ string uri = GuestConsts.Page_GuestDocs_Detail.Replace("{docId}", docId);
+ Navigation.NavigateTo(uri);
+ }
+
#endregion
+ #region Models
+
+ enum ePageMode
+ {
+ selectDocId, alreadyExistDoc, createNew
+ }
+
+ #endregion
}
diff --git a/src/Somnia.GuestLog/GuestConsts.cs b/src/Somnia.GuestLog/GuestConsts.cs
index c618f70..d22f412 100644
--- a/src/Somnia.GuestLog/GuestConsts.cs
+++ b/src/Somnia.GuestLog/GuestConsts.cs
@@ -5,6 +5,7 @@ public static class GuestConsts
public const string Page_GuestDocs_List = "/guest-documents";
public const string Page_GuestDocs_New = "/guest-documents/new";
+ public const string Page_GuestDocs_Detail = "/guest-documents/d/{docId}";
public const string regexPattern_phoneNum = @"\+?\d{1,4}?[-.\s]?\(?\d{1,3}?\)?[-.\s]?\d{1,4}[-.\s]?\d{1,4}[-.\s]?\d{1,9}";
@@ -13,4 +14,11 @@ public static class GuestConsts
public const string DataList_DocumentTypes = "DocumentType";
public const string DataList_GenderOptions = "GenderOption";
public const string DataList_CountryCodes = "CountryCode";
+
+
+ public const string CountryCode_ESP = "ESP";
+
+ public const string DocumentType_DNI = "DNI";
+ public const string DocumentType_NIE = "NIE";
+ public const string DocumentType_PASSPORT = "PASSPORT";
}
\ No newline at end of file
diff --git a/src/Somnia.GuestLog/Models/DTOs/GuestDocIdDTO.cs b/src/Somnia.GuestLog/Models/DTOs/GuestDocIdDTO.cs
new file mode 100644
index 0000000..3a17309
--- /dev/null
+++ b/src/Somnia.GuestLog/Models/DTOs/GuestDocIdDTO.cs
@@ -0,0 +1,28 @@
+namespace Somnia.GuestLog.Models.DTOs;
+
+public class GuestDocIdDTO
+{
+ public required string DocumentType { get; set; }
+ public required string DocumentNum { get; set; }
+
+
+ public static string EncodeAsString(GuestDocIdDTO docId)
+ {
+ return $"{docId.DocumentType}-{docId.DocumentNum}";
+ }
+
+ public static GuestDocIdDTO? DecodeFromString(string str)
+ {
+ var parts = str.Split("-");
+ if (parts.Length < 2)
+ {
+ return null;
+ }
+
+ return new()
+ {
+ DocumentType = parts[0],
+ DocumentNum = parts[1]
+ };
+ }
+}
diff --git a/src/Somnia.GuestLog/Program.cs b/src/Somnia.GuestLog/Program.cs
index 3146a04..2e54ad2 100644
--- a/src/Somnia.GuestLog/Program.cs
+++ b/src/Somnia.GuestLog/Program.cs
@@ -28,6 +28,7 @@ builder.Services.AddRazorComponents()
// Add Validation Services
builder.Services.AddSingleton, GuestDocumentDTOValidator>();
+builder.Services.AddSingleton, GuestDocIdDTOValidator>();
// Add Services
builder.Services.AddSingleton();
diff --git a/src/Somnia.GuestLog/Resources/LanguageLocale.resx b/src/Somnia.GuestLog/Resources/LanguageLocale.resx
index 7a036de..bff279b 100644
--- a/src/Somnia.GuestLog/Resources/LanguageLocale.resx
+++ b/src/Somnia.GuestLog/Resources/LanguageLocale.resx
@@ -149,11 +149,18 @@
Deleting can not be undone!
Delete
Create
+ Edit
Return
Close
+ Search
+ Save
+ Cancel
New Guest
+ This Guest Document Already exists!!
+ Go to existing document
+
Document Details
Guest Details
Contact Details
diff --git a/src/Somnia.GuestLog/Services/StaticDataListService.cs b/src/Somnia.GuestLog/Services/StaticDataListService.cs
index 7297b51..9b8f4e3 100644
--- a/src/Somnia.GuestLog/Services/StaticDataListService.cs
+++ b/src/Somnia.GuestLog/Services/StaticDataListService.cs
@@ -51,9 +51,9 @@ public class StaticDataListService(
#region DataList Dicts
private static readonly HashSet _documentTypeSet = [
- "DNI",
- "NIE",
- "PASSPORT",
+ GuestConsts.DocumentType_DNI,
+ GuestConsts.DocumentType_NIE,
+ GuestConsts.DocumentType_PASSPORT,
"ID",
"OTHER",
];
diff --git a/src/Somnia.GuestLog/Utils/DNIUtils.cs b/src/Somnia.GuestLog/Utils/DNIUtils.cs
new file mode 100644
index 0000000..de89c64
--- /dev/null
+++ b/src/Somnia.GuestLog/Utils/DNIUtils.cs
@@ -0,0 +1,106 @@
+namespace Somnia.GuestLog.Utils;
+
+public static class DNIUtils
+{
+ #region Validators
+
+ public static bool IsValidDNI(string dni)
+ {
+ //Bypass validation if letter if 'Ñ'
+ if (dni.Length > 1 && dni[^1] == 'Ñ')
+ {
+ return true;
+ }
+
+ if (dni.Length != 9 || !RegexUtils.RegexDniFormat().IsMatch(dni))
+ {
+ return false;
+ // throw new InvalidOperationException($"DNI with invalid format!! Value: '{dni}'");
+ }
+
+ char calculatedLetter = CalculateLetterDniFromDigits(dni[..8]);
+
+ return calculatedLetter == dni[8];
+ }
+
+ public static bool IsValidNIE(string nie)
+ {
+ //Bypass validation if letter if 'Ñ'
+ if (nie.Length > 1 && nie[^1] == 'Ñ')
+ {
+ return true;
+ }
+
+ if (nie.Length != 9 || !RegexUtils.RegexNieFormat().IsMatch(nie))
+ {
+ return false;
+ // throw new InvalidOperationException($"NIE with invalid format!! Value: '{nie}'");
+ }
+
+ char calculatedLetter = CalculateLetterNieFromDigits(nie[..8]);
+
+ return calculatedLetter == nie[8];
+ }
+
+
+ #endregion
+
+ #region Letter Calculators
+
+ public static char CalculateLetterDniFromDigits(string dniDigits)
+ {
+ if (dniDigits.Length != 8)
+ {
+ throw new InvalidOperationException($"Invalid digit length! Received value '{dniDigits}' is not 8 digits long");
+ }
+
+ return GetDniLetter(dniDigits.AsSpan());
+ }
+
+ public static char CalculateLetterNieFromDigits(string dniDigits)
+ {
+ //Convert first letter to number.
+ int firstLetterAsDigit = NiePrefixToDigit(dniDigits[0]);
+
+ string newDigits = firstLetterAsDigit + dniDigits[1..];
+
+ return CalculateLetterDniFromDigits(newDigits);
+ }
+
+ #endregion
+
+ #region Internal Calculations
+
+ //Letters for remainder 0..22
+ private const string Letters = "TRWAGMYFPDXBNJZSQVHLCKE";
+
+
+ // NIE prefixes map to digits:
+ // X -> 0, Y -> 1, Z -> 2 (so they become 0/1/2 + 7 digits for mod computation)
+ private static int NiePrefixToDigit(char prefix)
+ => prefix switch
+ {
+ 'X' or 'x' => 0,
+ 'Y' or 'y' => 1,
+ 'Z' or 'z' => 2,
+ _ => throw new ArgumentException("Invalid NIE prefix. Expected X, Y, or Z.")
+ };
+
+ private static char GetDniLetter(ReadOnlySpan digits)
+ {
+ // Fast path: avoid allocating by parsing digits incrementally as a number mod 23.
+ int mod = 0;
+ for (int i = 0; i < digits.Length; i++)
+ {
+ char c = digits[i];
+ // Optional validation: digits only
+ if ((uint)(c - '0') > 9) throw new ArgumentException("Non-digit character found.");
+ mod = (mod * 10 + (c - '0')) % 23;
+ }
+
+ return Letters[mod];
+ }
+
+ #endregion
+
+}
diff --git a/src/Somnia.GuestLog/Utils/RegexUtils.cs b/src/Somnia.GuestLog/Utils/RegexUtils.cs
new file mode 100644
index 0000000..946c797
--- /dev/null
+++ b/src/Somnia.GuestLog/Utils/RegexUtils.cs
@@ -0,0 +1,15 @@
+using System.Text.RegularExpressions;
+
+namespace Somnia.GuestLog.Utils;
+
+public static partial class RegexUtils
+{
+ [GeneratedRegex("^[0-9]{8}[A-Z]$")]
+ public static partial Regex RegexDniFormat();
+
+ [GeneratedRegex("^[XYZ][0-9]{7}[A-Z]$")]
+ public static partial Regex RegexNieFormat();
+
+ [GeneratedRegex("^[A-Z]{3}[0-9]{6}$")]
+ public static partial Regex RegexGuestDocSuportNumber();
+}
diff --git a/src/Somnia.GuestLog/Validators/CustomValidators.cs b/src/Somnia.GuestLog/Validators/CustomValidators.cs
new file mode 100644
index 0000000..d6be1a7
--- /dev/null
+++ b/src/Somnia.GuestLog/Validators/CustomValidators.cs
@@ -0,0 +1,14 @@
+using FluentValidation;
+using Somnia.GuestLog.Models.DTOs;
+using Somnia.GuestLog.Services;
+
+namespace Somnia.GuestLog.Validators;
+
+public static class CustomValidators
+{
+ public static IRuleBuilderOptions> ListMustContainFewerThan(this IRuleBuilder> ruleBuilder, int num)
+ {
+ return ruleBuilder.Must(list => list.Count < num).WithMessage("The list contains too many items");
+ }
+
+}
\ No newline at end of file
diff --git a/src/Somnia.GuestLog/Validators/GuestDocIdDTOValidator.cs b/src/Somnia.GuestLog/Validators/GuestDocIdDTOValidator.cs
new file mode 100644
index 0000000..179acb6
--- /dev/null
+++ b/src/Somnia.GuestLog/Validators/GuestDocIdDTOValidator.cs
@@ -0,0 +1,44 @@
+using FluentValidation;
+using Somnia.GuestLog.Models.DTOs;
+using Somnia.GuestLog.Services;
+using Somnia.GuestLog.Utils;
+
+namespace Somnia.GuestLog.Validators;
+
+///
+/// Validator for GuestDocumentDTO which contains multiple rules and can be shared with the back end API
+///
+public class GuestDocIdDTOValidator : AbstractValidator
+{
+ public GuestDocIdDTOValidator(IDataListService dataListService)
+ {
+ RuleFor(x => x.DocumentType)
+ .NotEmpty()
+ .Must(val => val != null && dataListService.IsValueInList(GuestConsts.DataList_DocumentTypes, val));
+
+ RuleFor(x => x.DocumentNum)
+ .NotEmpty()
+ .Length(6, 32);
+
+ RuleFor(x => x.DocumentNum)
+ .Cascade(CascadeMode.Stop)
+ .NotEmpty()
+ .Must(num => DNIUtils.IsValidDNI(num))
+ .WithMessage("Invalid DNI")
+ .When(x => x.DocumentType == GuestConsts.DocumentType_DNI);
+
+ RuleFor(x => x.DocumentNum)
+ .Cascade(CascadeMode.Stop)
+ .NotEmpty()
+ .Must(num => DNIUtils.IsValidNIE(num))
+ .WithMessage("Invalid NIE")
+ .When(x => x.DocumentType == GuestConsts.DocumentType_NIE);
+ }
+
+ public Func