diff --git a/src/Somnia.GuestLog/Components/Controls/AddressQueryDialog.razor b/src/Somnia.GuestLog/Components/Controls/AddressQueryDialog.razor index 8b70e37..c40679c 100644 --- a/src/Somnia.GuestLog/Components/Controls/AddressQueryDialog.razor +++ b/src/Somnia.GuestLog/Components/Controls/AddressQueryDialog.razor @@ -5,23 +5,24 @@ @inject IAddressQueryFactory addressQueryFactory; @inject ISnackbar Snackbar - + - Search Address + @LanguageLocale.control_AddressQueryDialog_dialogTitle - + @if (addressResults.Count == 0) { - No results! + @LanguageLocale.general_noResults } - + - - - + + + - - + + @@ -49,24 +55,46 @@ - Cancel - Select Address + + @LanguageLocale.general_btnCancel + + + @LanguageLocale.control_AddressQueryDialog_btnSelectAddress + @code { + // Dialog Instance injected when open by the DialogService. + // This field is null when dialog is not open by the DialogService. [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!; + // Reference to the Dialog when open by the ShowAsync Method. (When dialog is used inline). + // This field is null when dialog is not open using the internal methods. + private IDialogReference? _reference; - public string SearchText { get; set; } = ""; + //Reference to the MudDialog component. Used to manage the internal dialog when the component is inline. + private MudDialog DialogRef { get; set; } = null!; + + private string SearchText { get; set; } = ""; private List addressResults = []; private AddressResultDTO? SelectedAddressResult { get; set; } - private MudTextField txtSearch = null!; - - - private void Cancel() => MudDialog.Cancel(); + private void Cancel() + { + if (MudDialog is not null) + { + MudDialog.Cancel(); + return; + } + else if (_reference is not null) + { + DialogRef.CloseAsync(DialogResult.Cancel()); + return; + } + throw new InvalidOperationException("Unable to cancel (close) the dialog!"); + } private async Task btnSearchAddress() { @@ -88,7 +116,7 @@ } } - private void btnSelectAddress() + private async Task btnSelectAddress() { if (SelectedAddressResult is null) { @@ -96,8 +124,37 @@ return; } - //In a real world scenario this bool would probably be a service to delete the item from api/database - Snackbar.Add("Address Selected", Severity.Success); - MudDialog.Close(DialogResult.Ok(SelectedAddressResult)); + DialogResult dialogResult = DialogResult.Ok(SelectedAddressResult); + + //Close the dialog when opened by DialogService. + if (MudDialog != null) + { + logger.LogDebug("AddressQueryDialog - Closed MudDialogInstance!"); + MudDialog.Close(dialogResult); + return; + } + //Close the dialog when is a Inline dialog. + else if (_reference is not null) + { + logger.LogDebug("AddressQueryDialog - Closed inline dialog instance!"); + await DialogRef.CloseAsync(dialogResult); + return; + } + //Throw if the dialog can't be closed (as none of the above is true). + throw new InvalidOperationException("Dialog cannot be closed! No reference to close!"); } + + + public async Task ShowAsync(DialogOptions? options = null) + { + _reference = await DialogRef.ShowAsync(options: options); + return _reference; + } + + public async Task CloseAsync() + { + await DialogRef.CloseAsync(); + _reference = null; + } + } diff --git a/src/Somnia.GuestLog/Components/Controls/GuestDocForm.razor b/src/Somnia.GuestLog/Components/Controls/GuestDocForm.razor index 69191eb..e96410f 100644 --- a/src/Somnia.GuestLog/Components/Controls/GuestDocForm.razor +++ b/src/Somnia.GuestLog/Components/Controls/GuestDocForm.razor @@ -17,14 +17,16 @@ + Required ReadOnly="@ReadOnlyDocId"> @foreach (string docType in dataListService.GetDataListByName(GuestConsts.DataList_DocumentTypes)) { - @dataListService.GetLocalizedValue(GuestConsts.DataList_DocumentTypes, docType) + + @dataListService.GetLocalizedValue(GuestConsts.DataList_DocumentTypes, docType) + } + Immediate="true" Required ReadOnly="@ReadOnlyDocId" /> @@ -64,7 +66,9 @@ @foreach (string gender in dataListService.GetDataListByName(GuestConsts.DataList_GenderOptions)) { - @dataListService.GetLocalizedValue(GuestConsts.DataList_GenderOptions, gender) + + @dataListService.GetLocalizedValue(GuestConsts.DataList_GenderOptions, gender) + } @@ -92,23 +96,31 @@ @LanguageLocale.control_GuestDocForm_sectionAddress - + @if (!ReadOnly) + { + + @LanguageLocale.control_GuestDocForm_btnSearchAddress + + } + + + Label="@LanguageLocale.general_address_city" ReadOnly="@ReadOnlyAddress" /> + Label="@LanguageLocale.general_address_province" ReadOnly="@ReadOnlyAddress" /> + Label="@LanguageLocale.general_address_state" ReadOnly="@ReadOnlyAddress" /> - + @@ -116,6 +128,8 @@ @CreateTimeString + + @code { [Parameter] @@ -124,9 +138,21 @@ [Parameter] public bool ReadOnly { get; set; } = false; + [Parameter] + public bool ReadOnlyDocId { get; set; } = true; + private MudForm _form = null!; + AddressQueryDialog AddressQueryDialog { get; set; } = null!; + DialogOptions AddressQueryDialogOptions = new() + { + MaxWidth = MaxWidth.Large, + FullWidth = true, + }; + private bool ReadOnlyAddress { get; set; } = true; + private const string FieldMarginY = "margin-block: 0.5em"; + private Dictionary? countryDict = null; private bool isSuportNumberRequired { get => Model?.Nationality == GuestConsts.CountryCode_ESP; } @@ -178,4 +204,39 @@ .Select(x => x.Key); } + #region Event Handlers + + private async Task BtnOpenAddressSearchDialog() + { + var reference = await AddressQueryDialog.ShowAsync(options: AddressQueryDialogOptions); + + DialogResult? result = await reference.Result; + + if (result is null || result.Canceled) + { + return; + } + + AddressResultDTO? resultData = await reference.GetReturnValueAsync(); + if (resultData is null) + { + return; + } + + //Update Address info. + Model.Address = $"{resultData.AddressRoad}, {resultData.HouseNumber}"; + Model.AddressCity = resultData.AddressCity; + Model.AddressProvince = resultData.AddressProvince; + Model.AddressState = resultData.AddressState; + Model.ZipCode = resultData.ZipCode; + + //Set country by address code. + Model.AddressCountry = resultData.AddressCountryCode; + + //Address is editable after searching (if the form is editable). + ReadOnlyAddress = ReadOnly; + } + + #endregion + } \ No newline at end of file diff --git a/src/Somnia.GuestLog/Models/Configuration/AddressQueryConfig.cs b/src/Somnia.GuestLog/Models/Configuration/AddressQueryConfig.cs index b2b9bb6..ee346ed 100644 --- a/src/Somnia.GuestLog/Models/Configuration/AddressQueryConfig.cs +++ b/src/Somnia.GuestLog/Models/Configuration/AddressQueryConfig.cs @@ -9,6 +9,7 @@ public class AddressQueryConfig public string? ServiceURL { get; set; } public string? ApiKey { get; set; } public string? UserAgent { get; set; } + public string? ResultLang { get; set; } public enum AddressQueryProviderEnum diff --git a/src/Somnia.GuestLog/Models/DTOs/AddressResultDTO.cs b/src/Somnia.GuestLog/Models/DTOs/AddressResultDTO.cs index b3e9b2b..dee8463 100644 --- a/src/Somnia.GuestLog/Models/DTOs/AddressResultDTO.cs +++ b/src/Somnia.GuestLog/Models/DTOs/AddressResultDTO.cs @@ -9,4 +9,9 @@ public class AddressResultDTO public string? AddressState { get; set; } public string? AddressCountry { get; set; } public string? ZipCode { get; set; } + + /// + /// CountryCode in ISO3166-1:alpha3 + /// + public string? AddressCountryCode { get; set; } } diff --git a/src/Somnia.GuestLog/Resources/LanguageLocale.resx b/src/Somnia.GuestLog/Resources/LanguageLocale.resx index 87a237e..737f323 100644 --- a/src/Somnia.GuestLog/Resources/LanguageLocale.resx +++ b/src/Somnia.GuestLog/Resources/LanguageLocale.resx @@ -134,12 +134,6 @@ Gender Phone Number Email Address - Address - City - County - State - Country - Zip Code Creation Time Update Time @@ -156,8 +150,16 @@ Search Save Cancel + No Results! Use '%' as wildcard + Address + City + County + State + Country + Zip Code + Guest Documents Document List New Guest @@ -170,5 +172,11 @@ Guest Details Contact Details Guest Address + Search Address + + Search Address + Search Address + Search Address + Use Address \ No newline at end of file diff --git a/src/Somnia.GuestLog/Services/AddressQuery/NominatimAddressQueryService.cs b/src/Somnia.GuestLog/Services/AddressQuery/NominatimAddressQueryService.cs index 54f6279..d9e7d9b 100644 --- a/src/Somnia.GuestLog/Services/AddressQuery/NominatimAddressQueryService.cs +++ b/src/Somnia.GuestLog/Services/AddressQuery/NominatimAddressQueryService.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Options; using Somnia.GuestLog.Models.Configuration; using Somnia.GuestLog.Models.DTOs; +using Somnia.GuestLog.Utils; namespace Somnia.GuestLog.Services.AddressQuery; @@ -36,6 +37,8 @@ public class NominatimAddressQueryService( continue; } + string? alpha3CountryCode = CountryCodesUtils.Alpha2ToAlpha3(result.Address.Country_code); + rsList.Add(new() { HouseNumber = result.Address.House_number, @@ -44,7 +47,8 @@ public class NominatimAddressQueryService( AddressProvince = result.Address.Province, AddressState = result.Address.State, AddressCountry = result.Address.Country, - ZipCode = result.Address.Postcode + ZipCode = result.Address.Postcode, + AddressCountryCode = alpha3CountryCode }); } @@ -66,7 +70,10 @@ public class NominatimAddressQueryService( client.BaseAddress = baseAddr; client.DefaultRequestHeaders.UserAgent.ParseAdd(config.UserAgent ?? defaultUserAgent); - client.DefaultRequestHeaders.AcceptLanguage.ParseAdd("es"); + if (config.ResultLang is not null) + { + client.DefaultRequestHeaders.AcceptLanguage.ParseAdd(config.ResultLang); + } return client; } @@ -92,7 +99,8 @@ public class NominatimAddressQueryService( string? Province = null, string? State = null, string? Country = null, - string? Postcode = null + string? Postcode = null, + string? Country_code = null ); private record class ApiResult( diff --git a/src/Somnia.GuestLog/Utils/CountryCodesUtils.cs b/src/Somnia.GuestLog/Utils/CountryCodesUtils.cs new file mode 100644 index 0000000..7a3bca0 --- /dev/null +++ b/src/Somnia.GuestLog/Utils/CountryCodesUtils.cs @@ -0,0 +1,282 @@ +namespace Somnia.GuestLog.Utils; + +public static class CountryCodesUtils +{ + // Returns null when input is invalid/unknown. + public static string? Alpha2ToAlpha3(string? alpha2) + { + if (alpha2 is null) return null; + + // Common normalization: trim + uppercase. + // Avoids issues with "us", " US ", etc. + var key = alpha2.Trim().ToUpperInvariant(); + + if (key.Length != 2) return null; + + return Alpha2ToAlpha3Dict.TryGetValue(key, out var alpha3) ? alpha3 : null; + } + + // Throwing for invalid/unknown codes + public static string Alpha2ToAlpha3OrThrow(string alpha2) + { + var result = Alpha2ToAlpha3(alpha2) + ?? throw new ArgumentException("Unknown or invalid ISO 3166-1 alpha2 code.", nameof(alpha2)); + return result; + } + + // Build-time constant lookup. + // Keys are ISO 3166-1 alpha-2 codes (uppercase). + private static readonly Dictionary Alpha2ToAlpha3Dict = + new(StringComparer.Ordinal) + { + ["AF"] = "AFG", + ["AX"] = "ALA", + ["AL"] = "ALB", + ["DZ"] = "DZA", + ["AS"] = "ASM", + ["AD"] = "AND", + ["AO"] = "AGO", + ["AI"] = "AIA", + ["AQ"] = "ATA", + ["AG"] = "ATG", + ["AR"] = "ARG", + ["AM"] = "ARM", + ["AW"] = "ABW", + ["AU"] = "AUS", + ["AT"] = "AUT", + ["AZ"] = "AZE", + ["BS"] = "BHS", + ["BH"] = "BHR", + ["BD"] = "BGD", + ["BB"] = "BRB", + ["BY"] = "BLR", + ["BE"] = "BEL", + ["BZ"] = "BLZ", + ["BJ"] = "BEN", + ["BM"] = "BMU", + ["BT"] = "BTN", + ["BO"] = "BOL", + ["BQ"] = "BES", + ["BA"] = "BIH", + ["BW"] = "BWA", + ["BV"] = "BVT", + ["BR"] = "BRA", + ["IO"] = "IOT", + ["BN"] = "BRN", + ["BG"] = "BGR", + ["BF"] = "BFA", + ["BI"] = "BDI", + ["CV"] = "CPV", + ["KH"] = "KHM", + ["CM"] = "CMR", + ["CA"] = "CAN", + ["KY"] = "CYM", + ["CF"] = "CAF", + ["TD"] = "TCD", + ["CL"] = "CHL", + ["CN"] = "CHN", + ["CX"] = "CXR", + ["CC"] = "CCK", + ["CO"] = "COL", + ["KM"] = "COM", + ["CG"] = "COG", + ["CD"] = "COD", + ["CK"] = "COK", + ["CR"] = "CRI", + ["CI"] = "CIV", + ["HR"] = "HRV", + ["CU"] = "CUB", + ["CW"] = "CUW", + ["CY"] = "CYP", + ["CZ"] = "CZE", + ["DK"] = "DNK", + ["DJ"] = "DJI", + ["DM"] = "DMA", + ["DO"] = "DOM", + ["EC"] = "ECU", + ["EG"] = "EGY", + ["SV"] = "SLV", + ["GQ"] = "GNQ", + ["ER"] = "ERI", + ["EE"] = "EST", + ["SZ"] = "SWZ", + ["ET"] = "ETH", + ["FK"] = "FLK", + ["FO"] = "FRO", + ["FJ"] = "FJI", + ["FI"] = "FIN", + ["FR"] = "FRA", + ["GF"] = "GUF", + ["PF"] = "PYF", + ["TF"] = "ATF", + ["GA"] = "GAB", + ["GM"] = "GMB", + ["GE"] = "GEO", + ["DE"] = "DEU", + ["GH"] = "GHA", + ["GI"] = "GIB", + ["GR"] = "GRC", + ["GL"] = "GRL", + ["GD"] = "GRD", + ["GP"] = "GLP", + ["GU"] = "GUM", + ["GT"] = "GTM", + ["GG"] = "GGY", + ["GN"] = "GIN", + ["GW"] = "GNB", + ["GY"] = "GUY", + ["HT"] = "HTI", + ["HM"] = "HMD", + ["VA"] = "VAT", + ["HN"] = "HND", + ["HK"] = "HKG", + ["HU"] = "HUN", + ["IS"] = "ISL", + ["IN"] = "IND", + ["ID"] = "IDN", + ["IR"] = "IRN", + ["IQ"] = "IRQ", + ["IE"] = "IRL", + ["IM"] = "IMN", + ["IL"] = "ISR", + ["IT"] = "ITA", + ["JM"] = "JAM", + ["JP"] = "JPN", + ["JE"] = "JEY", + ["JO"] = "JOR", + ["KZ"] = "KAZ", + ["KE"] = "KEN", + ["KI"] = "KIR", + ["KP"] = "PRK", + ["KR"] = "KOR", + ["KW"] = "KWT", + ["KG"] = "KGZ", + ["LA"] = "LAO", + ["LV"] = "LVA", + ["LB"] = "LBN", + ["LS"] = "LSO", + ["LR"] = "LBR", + ["LY"] = "LBY", + ["LI"] = "LIE", + ["LT"] = "LTU", + ["LU"] = "LUX", + ["MO"] = "MAC", + ["MG"] = "MDG", + ["MW"] = "MWI", + ["MY"] = "MYS", + ["MV"] = "MDV", + ["ML"] = "MLI", + ["MT"] = "MLT", + ["MH"] = "MHL", + ["MQ"] = "MTQ", + ["MR"] = "MRT", + ["MU"] = "MUS", + ["YT"] = "MYT", + ["MX"] = "MEX", + ["FM"] = "FSM", + ["MD"] = "MDA", + ["MC"] = "MCO", + ["MN"] = "MNG", + ["ME"] = "MNE", + ["MS"] = "MSR", + ["MA"] = "MAR", + ["MZ"] = "MOZ", + ["MM"] = "MMR", + ["NA"] = "NAM", + ["NR"] = "NRU", + ["NP"] = "NPL", + ["NL"] = "NLD", + ["NC"] = "NCL", + ["NZ"] = "NZL", + ["NI"] = "NIC", + ["NE"] = "NER", + ["NG"] = "NGA", + ["NU"] = "NIU", + ["NF"] = "NFK", + ["MK"] = "MKD", + ["MP"] = "MNP", + ["NO"] = "NOR", + ["OM"] = "OMN", + ["PK"] = "PAK", + ["PW"] = "PLW", + ["PS"] = "PSE", + ["PA"] = "PAN", + ["PG"] = "PNG", + ["PY"] = "PRY", + ["PE"] = "PER", + ["PH"] = "PHL", + ["PN"] = "PCN", + ["PL"] = "POL", + ["PT"] = "PRT", + ["PR"] = "PRI", + ["QA"] = "QAT", + ["RE"] = "REU", + ["RO"] = "ROU", + ["RU"] = "RUS", + ["RW"] = "RWA", + ["BL"] = "BLM", + ["SH"] = "SHN", + ["KN"] = "KNA", + ["LC"] = "LCA", + ["MF"] = "MAF", + ["PM"] = "SPM", + ["VC"] = "VCT", + ["WS"] = "WSM", + ["SM"] = "SMR", + ["ST"] = "STP", + ["SA"] = "SAU", + ["SN"] = "SEN", + ["RS"] = "SRB", + ["SC"] = "SYC", + ["SL"] = "SLE", + ["SG"] = "SGP", + ["SX"] = "SXM", + ["SK"] = "SVK", + ["SI"] = "SVN", + ["SB"] = "SLB", + ["SO"] = "SOM", + ["ZA"] = "ZAF", + ["GS"] = "SGS", + ["SS"] = "SSD", + ["ES"] = "ESP", + ["LK"] = "LKA", + ["SD"] = "SDN", + ["SR"] = "SUR", + ["SJ"] = "SJM", + ["SE"] = "SWE", + ["CH"] = "CHE", + ["SY"] = "SYR", + ["TW"] = "TWN", + ["TJ"] = "TJK", + ["TZ"] = "TZA", + ["TH"] = "THA", + ["TL"] = "TLS", + ["TG"] = "TGO", + ["TK"] = "TKL", + ["TO"] = "TON", + ["TT"] = "TTO", + ["TN"] = "TUN", + ["TR"] = "TUR", + ["TM"] = "TKM", + ["TC"] = "TCA", + ["TV"] = "TUV", + ["UG"] = "UGA", + ["UA"] = "UKR", + ["AE"] = "ARE", + ["GB"] = "GBR", + ["US"] = "USA", + ["UM"] = "UMI", + ["UY"] = "URY", + ["UZ"] = "UZB", + ["VU"] = "VUT", + ["VE"] = "VEN", + ["VN"] = "VNM", + ["VG"] = "VGB", + ["VI"] = "VIR", + ["WF"] = "WLF", + ["EH"] = "ESH", + ["YE"] = "YEM", + ["ZM"] = "ZMB", + ["ZW"] = "ZWE", + }; +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/appsettings.json b/src/Somnia.GuestLog/appsettings.json index e48845a..d9e7306 100644 --- a/src/Somnia.GuestLog/appsettings.json +++ b/src/Somnia.GuestLog/appsettings.json @@ -14,6 +14,7 @@ "Provider": "Nominatim", "ServiceURL": "https://nominatim.openstreetmap.org/", "ApiKey": "", - "UserAgent": "Somnia-GuestLog-Dev" + "UserAgent": "Somnia-GuestLog-Dev", + "ResultLang": "es" } } \ No newline at end of file