Вытащил солюшен на уровень выше, чтобы прощё было дотнетить
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
15
BlazorWebAssemblyVisaApiClient/App.razor
Normal file
15
BlazorWebAssemblyVisaApiClient/App.razor
Normal file
@@ -0,0 +1,15 @@
|
||||
@using BlazorWebAssemblyVisaApiClient.ErrorHandling
|
||||
<GlobalErrorHandler >
|
||||
<Router AppAssembly="@typeof(App).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
|
||||
</Found>
|
||||
<NotFound>
|
||||
<PageTitle>Not found</PageTitle>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p role="alert">Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
</GlobalErrorHandler>
|
||||
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
||||
<PackageReference Include="Blazor.Bootstrap" Version="3.0.0" />
|
||||
<PackageReference Include="FluentValidation" Version="11.9.2" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.DataAnnotations.Validation" Version="3.2.0-rc1.20223.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.1"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.1" PrivateAssets="all"/>
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\VisaApiClient\VisaApiClient.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<_ContentIncludedByDefault Remove="wwwroot\sample-data\weather.json" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\css\bootstrap\bootstrap.min.css" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\css\bootstrap\bootstrap.min.css.map" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace BlazorWebAssemblyVisaApiClient.Common.Exceptions;
|
||||
|
||||
public class BlazorClientException(string message) : Exception(message);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace BlazorWebAssemblyVisaApiClient.Common.Exceptions;
|
||||
|
||||
public class NotLoggedInException() : BlazorClientException("User is not logged in.");
|
||||
@@ -0,0 +1,80 @@
|
||||
@using System.Net
|
||||
@using BlazorWebAssemblyVisaApiClient.ErrorHandling
|
||||
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.UserDataProvider
|
||||
@using VisaApiClient
|
||||
@code {
|
||||
public static AuthData? AuthData;
|
||||
|
||||
[CascadingParameter] private GlobalErrorHandler ErrorHandler { get; set; } = null!;
|
||||
|
||||
[CascadingParameter] private Status? Status { get; set; }
|
||||
|
||||
[Inject] private IClient Client { get; set; } = null!;
|
||||
|
||||
[Inject] private NavigationManager Nav { get; set; } = null!;
|
||||
|
||||
[Inject] private IUserDataProvider UserDataProvider { get; set; } = null!;
|
||||
|
||||
///Authorize with email and password
|
||||
/// <returns>Message to user</returns>
|
||||
public async Task TryAuthorize(AuthData authData)
|
||||
{
|
||||
Status?.SetMessage("Wait...");
|
||||
try
|
||||
{
|
||||
var token = await Client.LoginAsync(authData.Email, authData.Password);
|
||||
Client.AuthToken = token;
|
||||
AuthData = authData;
|
||||
UserDataProvider.UpdateCurrentRole();
|
||||
|
||||
Status?.SetSuccess("Logged in successfully.");
|
||||
}
|
||||
catch (ApiException<ProblemDetails> e)
|
||||
{
|
||||
if (e.Result.Status == (int)HttpStatusCode.Forbidden)
|
||||
{
|
||||
Status?.SetError(e.Result.Detail!);
|
||||
}
|
||||
else
|
||||
{
|
||||
Status?.SetError("Error occured");
|
||||
ErrorHandler.Handle(e);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Status?.SetError("Error occured");
|
||||
ErrorHandler.Handle(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void Logout()
|
||||
{
|
||||
Client.AuthToken = null;
|
||||
AuthData = null;
|
||||
try
|
||||
{
|
||||
UserDataProvider.UpdateCurrentRole();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ErrorHandler.Handle(e);
|
||||
}
|
||||
}
|
||||
|
||||
///Re-auth if token expired or something
|
||||
public async Task ReAuthenticate()
|
||||
{
|
||||
if (AuthData is not null)
|
||||
{
|
||||
await TryAuthorize(AuthData);
|
||||
}
|
||||
else
|
||||
{
|
||||
Client.AuthToken = null;
|
||||
AuthData = null;
|
||||
Nav.NavigateTo("/");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
@using BlazorWebAssemblyVisaApiClient.ErrorHandling
|
||||
@using VisaApiClient
|
||||
@code
|
||||
{
|
||||
[CascadingParameter] protected GlobalErrorHandler ErrorHandler { get; set; } = null!;
|
||||
|
||||
[Inject] protected IClient Client { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
@using VisaApiClient
|
||||
<div>
|
||||
<div >
|
||||
<label >
|
||||
Country:<br/>
|
||||
<InputText class="rounded" @bind-Value="Address.Country"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => Address.Country"></ValidationMessage><br/>
|
||||
</div>
|
||||
|
||||
<div >
|
||||
<label >
|
||||
City:<br/>
|
||||
<InputText class="rounded" @bind-Value="Address.City"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => Address.City"></ValidationMessage><br/>
|
||||
</div>
|
||||
|
||||
<div >
|
||||
<label >
|
||||
Street:<br/>
|
||||
<InputText class="rounded" @bind-Value="Address.Street"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => Address.Street"></ValidationMessage><br/>
|
||||
</div>
|
||||
|
||||
<div >
|
||||
<label >
|
||||
Building:<br/>
|
||||
<InputText class="rounded" @bind-Value="Address.Building"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => Address.Building"></ValidationMessage>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public AddressModel Address { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
@using VisaApiClient
|
||||
<div>
|
||||
<div >
|
||||
<label >
|
||||
Email:<br/>
|
||||
<InputText class="rounded" @bind-Value="AuthData.Email"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => AuthData.Email"></ValidationMessage><br/>
|
||||
</div>
|
||||
|
||||
<div >
|
||||
<label >
|
||||
Password:<br/>
|
||||
<InputText type="password" class="rounded" @bind-Value="AuthData.Password"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => AuthData.Password"></ValidationMessage>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public AuthData AuthData { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
@using VisaApiClient
|
||||
<div>
|
||||
<div >
|
||||
<label>
|
||||
First name@(Constants.RequiredFieldMarkup):<br/>
|
||||
<InputText DisplayName="First name" class="rounded" @bind-Value="Name.FirstName"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => Name.FirstName"></ValidationMessage>
|
||||
</div><br/>
|
||||
|
||||
<div >
|
||||
<label>
|
||||
Surname@(Constants.RequiredFieldMarkup):<br/>
|
||||
<InputText class="rounded" @bind-Value="Name.Surname"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => Name.Surname"></ValidationMessage>
|
||||
</div><br/>
|
||||
|
||||
<div >
|
||||
<label>
|
||||
Patronymic:<br/>
|
||||
<InputText class="rounded" @bind-Value="Name.Patronymic"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => Name.Patronymic"></ValidationMessage>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public NameModel Name { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.DateTimeProvider
|
||||
@using VisaApiClient
|
||||
<div>
|
||||
<div >
|
||||
<label>
|
||||
Passport number:<br/>
|
||||
<InputText DisplayName="Passport number" class="rounded" @bind-Value="Passport.Number"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => Passport.Number"></ValidationMessage>
|
||||
</div><br/>
|
||||
|
||||
<div >
|
||||
<label>
|
||||
Issuer:<br/>
|
||||
<InputText class="rounded" @bind-Value="Passport.Issuer"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => Passport.Issuer"></ValidationMessage>
|
||||
</div><br/>
|
||||
|
||||
<div >
|
||||
<label>
|
||||
Issue date:<br/>
|
||||
<InputDate DisplayName="Issue date" class="rounded" @bind-Value="Passport.IssueDate" max="@formattedDate"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => Passport.IssueDate"></ValidationMessage>
|
||||
</div><br/>
|
||||
|
||||
<div >
|
||||
<label>
|
||||
Expiration date:<br/>
|
||||
<InputDate DisplayName="Expiration date" class="rounded" @bind-Value="Passport.ExpirationDate" min="@formattedDate"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => Passport.ExpirationDate"></ValidationMessage>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string formattedDate = null!;
|
||||
|
||||
[Parameter, EditorRequired] public PassportModel Passport { get; set; } = null!;
|
||||
|
||||
[Inject] IDateTimeProvider DateTimeProvider { get; set; } = null!;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Passport.IssueDate = DateTime.Now;
|
||||
Passport.ExpirationDate = DateTime.Now;
|
||||
formattedDate = DateTimeProvider.FormattedNow();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
@using BlazorWebAssemblyVisaApiClient.Validation.Applicants.Models
|
||||
<div>
|
||||
<div >
|
||||
<label >
|
||||
Name:<br/>
|
||||
<InputText class="rounded" @bind-Value="PlaceOfWork.Name"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => PlaceOfWork.Name"></ValidationMessage><br/>
|
||||
</div>
|
||||
|
||||
<div >
|
||||
<label >
|
||||
Phone number:<br/>
|
||||
<InputText DisplayName="Phone number" class="rounded" @bind-Value="PlaceOfWork.PhoneNum"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => PlaceOfWork.PhoneNum"></ValidationMessage>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public PlaceOfWorkModel PlaceOfWork { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
@typeparam TItem where TItem : class
|
||||
@using System.Linq.Expressions
|
||||
@using System.Reflection
|
||||
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Helpers
|
||||
@typeparam TMember where TMember : struct, Enum
|
||||
|
||||
<InputSelect TValue="TMember" @bind-Value="selected">
|
||||
@foreach (var value in enumValues)
|
||||
{
|
||||
<option value="@value.Key">@value.Value</option>
|
||||
}
|
||||
</InputSelect><br/>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public TItem Model { get; set; } = default!;
|
||||
|
||||
[Parameter, EditorRequired] public Expression<Func<TItem, TMember>> EnumProperty { get; set; } = null!;
|
||||
|
||||
[Parameter] public Action? OnChanged { get; set; }
|
||||
|
||||
private Dictionary<TMember, string> enumValues = new();
|
||||
private PropertyInfo modelMemberInfo = null!;
|
||||
private TMember selected;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
var modelMemberName = ((MemberExpression)EnumProperty.Body).Member.Name;
|
||||
modelMemberInfo = typeof(TItem).GetProperty(modelMemberName)!;
|
||||
|
||||
foreach (var value in Enum.GetValues<TMember>())
|
||||
{
|
||||
enumValues.Add(value, value.GetDisplayName());
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnAfterRender(bool firstRender)
|
||||
{
|
||||
var current = (TMember)modelMemberInfo.GetValue(Model)!;
|
||||
if (!current.Equals(selected))
|
||||
{
|
||||
OnValueChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnValueChanged()
|
||||
{
|
||||
modelMemberInfo.SetValue(Model, selected);
|
||||
OnChanged?.Invoke();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.DateTimeProvider
|
||||
@using VisaApiClient
|
||||
<div>
|
||||
<label>
|
||||
Issuer:<br/>
|
||||
<InputText DisplayName="Issuer of permission to destination Country" class="rounded"
|
||||
@bind-Value="PermissionToDestCountry.Issuer"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => PermissionToDestCountry.Issuer"></ValidationMessage><br/>
|
||||
|
||||
<label>
|
||||
Expiration date:<br/>
|
||||
<InputDate DisplayName="Expiration date of permission to destination Country" class="rounded"
|
||||
@bind-Value="PermissionToDestCountry.ExpirationDate"
|
||||
min="@formattedDate"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => PermissionToDestCountry.ExpirationDate"></ValidationMessage>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string formattedDate = null!;
|
||||
|
||||
[Parameter, EditorRequired] public PermissionToDestCountryModel PermissionToDestCountry { get; set; } = null!;
|
||||
|
||||
[Inject] IDateTimeProvider DateTimeProvider { get; set; } = null!;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
formattedDate = DateTimeProvider.FormattedNow();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.DateTimeProvider
|
||||
@using VisaApiClient
|
||||
<div>
|
||||
<label>
|
||||
Number:<br/>
|
||||
<InputText DisplayName="Number of re-entry permit" class="rounded"
|
||||
@bind-Value="ReentryPermit.Number"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => ReentryPermit.Number"></ValidationMessage><br/>
|
||||
|
||||
<label>
|
||||
Expiration date:<br/>
|
||||
<InputDate DisplayName="Expiration date of re-entry permit" class="rounded"
|
||||
@bind-Value="ReentryPermit.ExpirationDate"
|
||||
min="@formattedDate"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => ReentryPermit.ExpirationDate"></ValidationMessage>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string formattedDate = null!;
|
||||
|
||||
[Parameter, EditorRequired] public ReentryPermitModel ReentryPermit { get; set; } = null!;
|
||||
|
||||
[Inject] IDateTimeProvider DateTimeProvider { get; set; } = null!;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
formattedDate = DateTimeProvider.FormattedNow();
|
||||
ReentryPermit.ExpirationDate = DateTimeProvider.Now();
|
||||
}
|
||||
|
||||
}
|
||||
34
BlazorWebAssemblyVisaApiClient/Components/Status.razor
Normal file
34
BlazorWebAssemblyVisaApiClient/Components/Status.razor
Normal file
@@ -0,0 +1,34 @@
|
||||
<p class="@statusClass">@((MarkupString)StatusText)</p>
|
||||
<CascadingValue Value="this">
|
||||
@ChildContent
|
||||
</CascadingValue>
|
||||
|
||||
@code {
|
||||
private string statusClass = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
public string StatusText { get; private set; } = string.Empty;
|
||||
|
||||
public void SetMessage(string message)
|
||||
{
|
||||
statusClass = string.Empty;
|
||||
StatusText = message;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public void SetError(string message)
|
||||
{
|
||||
statusClass = "validation-message";
|
||||
StatusText = message;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public void SetSuccess(string message)
|
||||
{
|
||||
statusClass = "text-success";
|
||||
StatusText = message;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
19
BlazorWebAssemblyVisaApiClient/Constants.cs
Normal file
19
BlazorWebAssemblyVisaApiClient/Constants.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient;
|
||||
|
||||
public static class Constants
|
||||
{
|
||||
public readonly static Regex EnglishWordRegex = new("^[a-zA-Z]*$");
|
||||
|
||||
public readonly static Regex EnglishPhraseRegex = new(@"^[a-z A-Z№0-9?><;,{}[\]\-_+=!@#$%\^&*|']*$");
|
||||
|
||||
public readonly static Regex PhoneNumRegex = new(@"^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$");
|
||||
|
||||
public readonly static MarkupString RequiredFieldMarkup = (MarkupString)"<span style=\"color: red;\">*</span>";
|
||||
|
||||
public const string ApplicantRole = "Applicant";
|
||||
public const string ApprovingAuthorityRole = "ApprovingAuthority";
|
||||
public const string AdminRole = "Admin";
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
@using System.Net
|
||||
@using BlazorWebAssemblyVisaApiClient.Common.Exceptions
|
||||
@using VisaApiClient
|
||||
<CascadingValue Value="this">
|
||||
<Modal @ref="modal">
|
||||
<BodyTemplate>
|
||||
@errorDetails
|
||||
</BodyTemplate>
|
||||
<FooterTemplate>
|
||||
<Button Color="ButtonColor.Secondary" @onclick="modal.HideAsync">Okaaaay</Button>
|
||||
</FooterTemplate>
|
||||
</Modal>
|
||||
@ChildContent
|
||||
</CascadingValue>
|
||||
|
||||
@code
|
||||
{
|
||||
private Modal modal = null!;
|
||||
private string errorDetails = null!;
|
||||
|
||||
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
[Inject] private NavigationManager Nav { get; set; } = null!;
|
||||
|
||||
public void Handle(Exception ex)
|
||||
{
|
||||
switch (ex)
|
||||
{
|
||||
case ApiException<ProblemDetails>
|
||||
{
|
||||
StatusCode: (int)HttpStatusCode.Unauthorized or (int)HttpStatusCode.Forbidden
|
||||
} or NotLoggedInException:
|
||||
Nav.NavigateTo("/");
|
||||
modal.Title = "Authorization failed";
|
||||
errorDetails = "You are not authorized or your authorization is expired";
|
||||
modal.ShowAsync();
|
||||
break;
|
||||
|
||||
case ApiException<ProblemDetails> problemDetails:
|
||||
modal.Title = problemDetails.Result.Title!;
|
||||
errorDetails = problemDetails.Result.Detail!;
|
||||
modal.ShowAsync();
|
||||
break;
|
||||
|
||||
default:
|
||||
modal.Title = "Something went wrong";
|
||||
errorDetails = "Please, text an email with your problem definition on nasrudin@mail.ru";
|
||||
modal.ShowAsync();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using AutoMapper;
|
||||
using BlazorWebAssemblyVisaApiClient.Validation.Applicants.Models;
|
||||
using VisaApiClient;
|
||||
using PlaceOfWorkModel = BlazorWebAssemblyVisaApiClient.Validation.Applicants.Models.PlaceOfWorkModel;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient.Infrastructure.AutoMapper.Profiles;
|
||||
|
||||
public class RegisterApplicantRequestProfile : Profile
|
||||
{
|
||||
public RegisterApplicantRequestProfile()
|
||||
{
|
||||
CreateMap<RegisterApplicantRequestModel, RegisterApplicantRequest>(MemberList.Destination);
|
||||
|
||||
CreateMap<RegisterRequestModel, RegisterRequest>(MemberList.Destination);
|
||||
|
||||
CreateMap<PlaceOfWorkModel, VisaApiClient.PlaceOfWorkModel>(MemberList.Destination);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using AutoMapper;
|
||||
using BlazorWebAssemblyVisaApiClient.Validation.VisaApplications.Models;
|
||||
using VisaApiClient;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient.Infrastructure.AutoMapper.Profiles;
|
||||
|
||||
public class VisaApplicationCreateRequestProfile : Profile
|
||||
{
|
||||
public VisaApplicationCreateRequestProfile()
|
||||
{
|
||||
CreateMap<VisaApplicationCreateRequestModel, VisaApplicationCreateRequest>(MemberList.Destination);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient.Infrastructure.Helpers;
|
||||
|
||||
public static class EnumExtensions
|
||||
{
|
||||
public static string GetDisplayName(this Enum value)
|
||||
{
|
||||
var enumMembers = value.GetType().GetMembers();
|
||||
var member = enumMembers.First(info => info.Name == value.ToString());
|
||||
var displayAttribute = (DisplayAttribute?)member
|
||||
.GetCustomAttributes(typeof(DisplayAttribute), false)
|
||||
.FirstOrDefault();
|
||||
var displayName = displayAttribute?.Name ?? value.ToString();
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Text;
|
||||
using FluentValidation.Results;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient.Infrastructure.Helpers;
|
||||
|
||||
public static class ValidationResultExtensions
|
||||
{
|
||||
public static string ToErrorsString(this ValidationResult validationResult)
|
||||
=> ErrorsToString(validationResult.Errors.Select(e => e.ErrorMessage));
|
||||
|
||||
private static string ErrorsToString(IEnumerable<string> errors)
|
||||
{
|
||||
var stringBuilder = new StringBuilder();
|
||||
foreach (var error in errors)
|
||||
{
|
||||
stringBuilder.Append($"{error}<br/>");
|
||||
}
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace BlazorWebAssemblyVisaApiClient.Infrastructure.Services.DateTimeProvider;
|
||||
|
||||
public class DateTimeProvider : IDateTimeProvider
|
||||
{
|
||||
public DateTime Now() => DateTime.Now;
|
||||
|
||||
public string FormattedNow() => Now().ToString("yyyy-MM-dd");
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace BlazorWebAssemblyVisaApiClient.Infrastructure.Services.DateTimeProvider;
|
||||
|
||||
public interface IDateTimeProvider
|
||||
{
|
||||
DateTime Now();
|
||||
|
||||
string FormattedNow();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using BlazorWebAssemblyVisaApiClient.Common.Exceptions;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient.Infrastructure.Services.UserDataProvider.Exceptions;
|
||||
|
||||
public class UnknownRoleException() : BlazorClientException("Unknown user role");
|
||||
@@ -0,0 +1,14 @@
|
||||
using VisaApiClient;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient.Infrastructure.Services.UserDataProvider;
|
||||
|
||||
public interface IUserDataProvider
|
||||
{
|
||||
public string? CurrentRole { get; }
|
||||
|
||||
public Action? OnRoleChanged { get; set; }
|
||||
|
||||
public Task<ApplicantModel> GetApplicant();
|
||||
|
||||
public void UpdateCurrentRole();
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.UserDataProvider.Exceptions;
|
||||
using VisaApiClient;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient.Infrastructure.Services.UserDataProvider;
|
||||
|
||||
public class UserDataProvider(IClient client) : IUserDataProvider
|
||||
{
|
||||
private readonly static JwtSecurityTokenHandler tokenHandler = new();
|
||||
|
||||
public string? CurrentRole { get; private set; }
|
||||
|
||||
public Action? OnRoleChanged { get; set; }
|
||||
|
||||
public async Task<ApplicantModel> GetApplicant()
|
||||
{
|
||||
return await client.GetApplicantAsync();
|
||||
}
|
||||
|
||||
public void UpdateCurrentRole()
|
||||
{
|
||||
var role = CurrentRole;
|
||||
|
||||
if (client.AuthToken is null)
|
||||
{
|
||||
if (CurrentRole is not null)
|
||||
{
|
||||
role = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var token = tokenHandler.ReadJwtToken(client.AuthToken.Token);
|
||||
role = token.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.Role)?.Value;
|
||||
|
||||
switch (role)
|
||||
{
|
||||
case Constants.ApplicantRole: break;
|
||||
case Constants.ApprovingAuthorityRole: break;
|
||||
case Constants.AdminRole: break;
|
||||
default: throw new UnknownRoleException();
|
||||
}
|
||||
}
|
||||
|
||||
if (CurrentRole != role)
|
||||
{
|
||||
CurrentRole = role;
|
||||
OnRoleChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
41
BlazorWebAssemblyVisaApiClient/Layout/MainLayout.razor
Normal file
41
BlazorWebAssemblyVisaApiClient/Layout/MainLayout.razor
Normal file
@@ -0,0 +1,41 @@
|
||||
@using BlazorWebAssemblyVisaApiClient.Components.Auth
|
||||
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.UserDataProvider
|
||||
@inherits LayoutComponentBase
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
<NavMenu/>
|
||||
</div>
|
||||
|
||||
<main class="fullscreen">
|
||||
<div class="top-row px-4">
|
||||
<AuthComponent @ref="authComponent"/>
|
||||
@if (UserDataProvider.CurrentRole is not null)
|
||||
{
|
||||
<p>
|
||||
Logged as @UserDataProvider.CurrentRole (@AuthComponent.AuthData?.Email)
|
||||
<button class="btn-secondary" @onclick="authComponent.Logout">Log out</button>
|
||||
</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<NavLink href="/">Log in</NavLink>
|
||||
}
|
||||
</div>
|
||||
|
||||
<article class="content px-4">
|
||||
@Body
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
private AuthComponent authComponent = null!;
|
||||
|
||||
[Inject] private IUserDataProvider UserDataProvider { get; set; } = null!;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
UserDataProvider.OnRoleChanged += StateHasChanged;
|
||||
}
|
||||
}
|
||||
77
BlazorWebAssemblyVisaApiClient/Layout/MainLayout.razor.css
Normal file
77
BlazorWebAssemblyVisaApiClient/Layout/MainLayout.razor.css
Normal file
@@ -0,0 +1,77 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #d6d5d5;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
78
BlazorWebAssemblyVisaApiClient/Layout/NavMenu.razor
Normal file
78
BlazorWebAssemblyVisaApiClient/Layout/NavMenu.razor
Normal file
@@ -0,0 +1,78 @@
|
||||
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.UserDataProvider
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="">Schengen Visa</a>
|
||||
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
|
||||
<nav class="flex-column">
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Login
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
@if (UserDataProvider.CurrentRole is Constants.ApplicantRole or Constants.ApprovingAuthorityRole)
|
||||
{
|
||||
<nav class="flex-column">
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="applications" Match="NavLinkMatch.All">
|
||||
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Applications
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
@if (UserDataProvider.CurrentRole is Constants.ApplicantRole)
|
||||
{
|
||||
<nav class="flex-column">
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="applications/new" Match="NavLinkMatch.All">
|
||||
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> New application
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
@if (UserDataProvider.CurrentRole is Constants.AdminRole)
|
||||
{
|
||||
<nav class="flex-column">
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="authorities" Match="NavLinkMatch.All">
|
||||
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Authorities
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
@if (UserDataProvider.CurrentRole is Constants.AdminRole)
|
||||
{
|
||||
<nav class="flex-column">
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="authorities/add" Match="NavLinkMatch.All">
|
||||
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Add authority
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool collapseNavMenu = true;
|
||||
|
||||
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
|
||||
|
||||
[Inject] private IUserDataProvider UserDataProvider { get; set; } = null!;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
UserDataProvider.OnRoleChanged += StateHasChanged;
|
||||
}
|
||||
|
||||
private void ToggleNavMenu()
|
||||
{
|
||||
collapseNavMenu = !collapseNavMenu;
|
||||
}
|
||||
|
||||
}
|
||||
83
BlazorWebAssemblyVisaApiClient/Layout/NavMenu.razor.css
Normal file
83
BlazorWebAssemblyVisaApiClient/Layout/NavMenu.razor.css
Normal file
@@ -0,0 +1,83 @@
|
||||
.navbar-toggler {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.bi {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.75rem;
|
||||
top: -1px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bi-house-door-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-plus-square-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-list-nested-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a {
|
||||
color: #d7d7d7;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep a:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
/* Allow sidebar to scroll for tall menus */
|
||||
height: calc(100vh - 3.5rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
60
BlazorWebAssemblyVisaApiClient/Pages/AddAuthority.razor
Normal file
60
BlazorWebAssemblyVisaApiClient/Pages/AddAuthority.razor
Normal file
@@ -0,0 +1,60 @@
|
||||
@page "/authorities/add"
|
||||
@using AutoMapper
|
||||
@using BlazorWebAssemblyVisaApiClient.Components
|
||||
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Helpers
|
||||
@using BlazorWebAssemblyVisaApiClient.Validation.Applicants.Models
|
||||
@using FluentValidation
|
||||
@using VisaApiClient
|
||||
@inherits BlazorWebAssemblyVisaApiClient.Components.Base.VisaClientComponentBase
|
||||
|
||||
<EditForm Model="requestModel" class="with-centered-content">
|
||||
<ObjectGraphDataAnnotationsValidator/>
|
||||
<div >
|
||||
<label>
|
||||
Email:<br/>
|
||||
<InputText class="rounded" @bind-Value="requestModel.AuthData.Email"/>
|
||||
<ValidationMessage For="() => requestModel.AuthData.Email"/>
|
||||
</label><br/>
|
||||
<p/>
|
||||
|
||||
<label>
|
||||
Password:<br/>
|
||||
<InputText type="password" class="rounded" @bind-Value="requestModel.AuthData.Password"/>
|
||||
<ValidationMessage For="() => requestModel.AuthData.Password"/>
|
||||
</label><br/>
|
||||
<p/>
|
||||
|
||||
<button class="btn-primary rounded" @onclick="Add">Add</button><br/>
|
||||
<Status @ref="status"/>
|
||||
</div>
|
||||
</EditForm>
|
||||
|
||||
@code
|
||||
{
|
||||
private RegisterRequestModel requestModel = new();
|
||||
private Status status = new();
|
||||
|
||||
[Inject] private IValidator<RegisterRequestModel> RegisterRequestModelValidator { get; set; } = null!;
|
||||
[Inject] private IMapper Mapper { get; set; } = null!;
|
||||
|
||||
private async Task Add()
|
||||
{
|
||||
var validationResult = await RegisterRequestModelValidator.ValidateAsync(requestModel);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
status.SetError(validationResult.ToErrorsString());
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
status.SetMessage("Wait...");
|
||||
await Client.RegisterAuthorityAsync(Mapper.Map<RegisterRequest>(requestModel));
|
||||
status.SetSuccess("Success");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ErrorHandler.Handle(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
261
BlazorWebAssemblyVisaApiClient/Pages/Application.razor
Normal file
261
BlazorWebAssemblyVisaApiClient/Pages/Application.razor
Normal file
@@ -0,0 +1,261 @@
|
||||
@page "/applications/{ApplicationId}"
|
||||
@using System.Net
|
||||
@using BlazorWebAssemblyVisaApiClient.Common.Exceptions
|
||||
@using BlazorWebAssemblyVisaApiClient.Components
|
||||
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Helpers
|
||||
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.UserDataProvider
|
||||
@using BlazorWebAssemblyVisaApiClient.Validation.Applicants.Models
|
||||
@using BlazorWebAssemblyVisaApiClient.Validation.VisaApplications.Models
|
||||
@using VisaApiClient
|
||||
@inherits BlazorWebAssemblyVisaApiClient.Components.Base.VisaClientComponentBase
|
||||
|
||||
<PageTitle>Application</PageTitle>
|
||||
|
||||
<table class="table table-bordered table-hover table-sm">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td >
|
||||
Applicant's fullname:<br/>
|
||||
<em>@NameToString(application.Applicant.Name)</em>
|
||||
</td>
|
||||
<td colspan="2">
|
||||
Date of birth:<br/>
|
||||
<em>@application.Applicant.BirthDate.ToString("d")</em>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
Country and city of birth:<br/>
|
||||
<em>@application.Applicant.CountryOfBirth, @application.Applicant.CityOfBirth</em>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
Citizenship:<br/>
|
||||
<em>@application.Applicant.Citizenship</em>
|
||||
</td>
|
||||
<td>
|
||||
Citizenship by birth:<br/>
|
||||
<em>@application.Applicant.CitizenshipByBirth</em>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td >
|
||||
Gender:<br/>
|
||||
<em>@application.Applicant.Gender.GetDisplayName()</em>
|
||||
</td>
|
||||
<td colspan="2">
|
||||
Marital status:<br/>
|
||||
<em>@(((MaritalStatusModel)application.Applicant.MaritalStatus).GetDisplayName())</em>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td >
|
||||
Father's fullname:<br/>
|
||||
<em>@NameToString(application.Applicant.FatherName)</em>
|
||||
</td>
|
||||
<td colspan="2">
|
||||
Mother's fullname:<br/>
|
||||
<em>@NameToString(application.Applicant.MotherName)</em>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td >
|
||||
Passport number:<br/>
|
||||
<em>@application.Applicant.Passport.Number</em>
|
||||
</td>
|
||||
<td >
|
||||
Issue date:<br/>
|
||||
<em>@application.Applicant.Passport.IssueDate.ToString("d")</em>
|
||||
</td>
|
||||
<td >
|
||||
Expiration date:<br/>
|
||||
<em>@application.Applicant.Passport.ExpirationDate.ToString("d")</em>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
Passport issuer:<br/>
|
||||
<em>@application.Applicant.Passport.Issuer</em>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
Re-entry permission (for non-residents):<br/>
|
||||
@if (application.Applicant.IsNonResident)
|
||||
{
|
||||
<em>@(application.ReentryPermit is null ? "None" : $"{application.ReentryPermit.Number}, expires at {application.ReentryPermit.ExpirationDate:d}")</em>
|
||||
}
|
||||
else
|
||||
{
|
||||
<em>Not non-resident</em>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
Job title:<br/>
|
||||
<em>@application.Applicant.JobTitle</em>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
Place of work, address, hirer's phone number:<br/>
|
||||
<em>
|
||||
@((MarkupString)$"{application.Applicant.PlaceOfWork.Name}<br>Address: {AddressToString(application.Applicant.PlaceOfWork.Address)}<br>Phone num: {application.Applicant.PlaceOfWork.PhoneNum}")
|
||||
</em>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td >
|
||||
Destination Country:<br/>
|
||||
<em>@application.DestinationCountry</em>
|
||||
</td>
|
||||
<td >
|
||||
Visa category:<br/>
|
||||
<em>@(((VisaCategoryModel)application.VisaCategory).GetDisplayName())</em>
|
||||
</td>
|
||||
<td >
|
||||
Visa:<br/>
|
||||
<em>@(application.ForGroup ? "For group" : "Individual")</em>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td >
|
||||
Requested number of entries:<br/>
|
||||
<em>@application.RequestedNumberOfEntries.GetDisplayName()</em>
|
||||
</td>
|
||||
<td colspan="2">
|
||||
Valid for:<br/>
|
||||
<em>@($"{application.ValidDaysRequested} days")</em>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
Past visas:<br/>
|
||||
@if (application.PastVisas.Any())
|
||||
{
|
||||
foreach (var visa in application.PastVisas)
|
||||
{
|
||||
<em>@($"{visa.Name} issued at {visa.IssueDate:d} and was valid until {visa.ExpirationDate:d}")</em>
|
||||
<br/>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<em>None</em>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
Permission to destination Country, if transit:<br/>
|
||||
@if (application.VisaCategory is VisaCategory.Transit)
|
||||
{
|
||||
<em>@(application.PermissionToDestCountry is null ? "None" : $"Expires at {application.PermissionToDestCountry.ExpirationDate}, issued by: {application.PermissionToDestCountry.Issuer}")</em>
|
||||
}
|
||||
else
|
||||
{
|
||||
<em>Non-transit</em>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
Past visits:<br/>
|
||||
@if (application.PastVisas.Any())
|
||||
{
|
||||
foreach (var visit in application.PastVisits)
|
||||
{
|
||||
<em>@($"Visit to {visit.DestinationCountry}, entered at {visit.StartDate:d} and lasts until {visit.EndDate:d}")</em>
|
||||
<br/>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<em>None</em>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@if (currentRole == Constants.ApprovingAuthorityRole)
|
||||
{
|
||||
<button class="btn-outline-primary" @onclick="Approve">Approve</button>
|
||||
<button class="btn-outline-danger" @onclick="Reject">Reject</button>
|
||||
<Status @ref="status"/>
|
||||
}
|
||||
|
||||
@code {
|
||||
private VisaApplicationModel application = new();
|
||||
private string currentRole = null!;
|
||||
private Status status = null!;
|
||||
|
||||
[Parameter] public string ApplicationId { get; set; } = null!;
|
||||
|
||||
[Inject] private IUserDataProvider UserDataProvider { get; set; } = null!;
|
||||
|
||||
[Inject] private NavigationManager Nav { get; set; } = null!;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var applicationId = Guid.Parse(ApplicationId);
|
||||
currentRole = UserDataProvider.CurrentRole ?? throw new NotLoggedInException();
|
||||
|
||||
application = currentRole switch
|
||||
{
|
||||
Constants.ApplicantRole => await Client.GetApplicationForApplicantAsync(applicationId),
|
||||
Constants.ApprovingAuthorityRole => await Client.GetApplicationForAuthorityAsync(applicationId),
|
||||
_ => throw new NotLoggedInException()
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
if (e is ApiException<ProblemDetails> { Result.Status: (int)HttpStatusCode.Conflict })
|
||||
{
|
||||
Nav.NavigateTo("/applications");
|
||||
}
|
||||
ErrorHandler.Handle(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static string NameToString(NameModel name)
|
||||
=> $"{name.FirstName} {name.Surname} {name.Patronymic}".TrimEnd();
|
||||
|
||||
private static string AddressToString(AddressModel address)
|
||||
=> $"{address.Country}, {address.City}, {address.Street} {address.Building}";
|
||||
|
||||
private async void Approve()
|
||||
{
|
||||
try
|
||||
{
|
||||
status.SetMessage("Wait...");
|
||||
await Client.SetStatusFromAuthorityAsync(application.Id, AuthorityRequestStatuses.Approved);
|
||||
Nav.NavigateTo("/applications");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
status.SetError("Error occured.");
|
||||
ErrorHandler.Handle(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async void Reject()
|
||||
{
|
||||
try
|
||||
{
|
||||
status.SetMessage("Wait...");
|
||||
await Client.SetStatusFromAuthorityAsync(application.Id, AuthorityRequestStatuses.Rejected);
|
||||
Nav.NavigateTo("/applications");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
status.SetError("Error occured.");
|
||||
ErrorHandler.Handle(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
143
BlazorWebAssemblyVisaApiClient/Pages/Applications.razor
Normal file
143
BlazorWebAssemblyVisaApiClient/Pages/Applications.razor
Normal file
@@ -0,0 +1,143 @@
|
||||
@page "/applications"
|
||||
@using BlazorWebAssemblyVisaApiClient.Common.Exceptions
|
||||
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Helpers
|
||||
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.UserDataProvider
|
||||
@using BlazorWebAssemblyVisaApiClient.Validation.VisaApplications.Models
|
||||
@using VisaApiClient
|
||||
@inherits BlazorWebAssemblyVisaApiClient.Components.Base.VisaClientComponentBase
|
||||
|
||||
<PageTitle>Applications</PageTitle>
|
||||
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Destination Country</th>
|
||||
<th>Visa Category</th>
|
||||
<th>Request date</th>
|
||||
<th>Days requested</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
@foreach (var application in applications)
|
||||
{
|
||||
var rowClass = application.Status switch
|
||||
{
|
||||
ApplicationStatus.Pending => "",
|
||||
ApplicationStatus.Approved => "table-success",
|
||||
ApplicationStatus.Rejected => "table-danger",
|
||||
ApplicationStatus.Closed => "table-danger",
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
<tr class="@rowClass">
|
||||
<td>@application.DestinationCountry</td>
|
||||
<td>@(((VisaCategoryModel)application.VisaCategory).GetDisplayName())</td>
|
||||
<td>@application.RequestDate.ToString("d")</td>
|
||||
<td>@application.ValidDaysRequested</td>
|
||||
<td>@application.Status.GetDisplayName()</td>
|
||||
<td>
|
||||
<NavLink href="@($"/applications/{application.Id}")">
|
||||
<button class="btn-primary">See</button>
|
||||
</NavLink>
|
||||
@if (currentRole == Constants.ApplicantRole)
|
||||
{
|
||||
<span> | </span>
|
||||
<input type="button" class="btn-outline-primary" @onclick="() => DownloadApplication(application)" value="Download"/>
|
||||
if (application.Status is ApplicationStatus.Pending)
|
||||
{
|
||||
<span> | </span>
|
||||
<input type="button" class="border-danger" @onclick="() => CloseApplication(application)" value="Close"/>
|
||||
}
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table >
|
||||
|
||||
<script>
|
||||
window.downloadFileFromStream = async (contentStreamReference) => {
|
||||
const arrayBuffer = await contentStreamReference.arrayBuffer();
|
||||
const blob = new Blob([arrayBuffer]);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchorElement = document.createElement('a');
|
||||
anchorElement.href = url;
|
||||
anchorElement.download = 'Application.xlsx';
|
||||
anchorElement.click();
|
||||
anchorElement.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
</script>
|
||||
|
||||
@code {
|
||||
private string currentRole = null!;
|
||||
private List<VisaApplicationPreview> applications = [];
|
||||
|
||||
[Inject] private IUserDataProvider UserDataProvider { get; set; } = null!;
|
||||
|
||||
[Inject] private IJSRuntime JavaScriptInterop { get; set; } = null!;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
currentRole = UserDataProvider.CurrentRole ?? throw new NotLoggedInException();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ErrorHandler.Handle(e);
|
||||
}
|
||||
|
||||
await Fetch();
|
||||
}
|
||||
|
||||
private async Task Fetch()
|
||||
{
|
||||
try
|
||||
{
|
||||
applications = currentRole switch
|
||||
{
|
||||
Constants.ApplicantRole => (await Client.GetApplicationsForApplicantAsync()).OrderByDescending(a => a.RequestDate).ToList(),
|
||||
Constants.ApprovingAuthorityRole => (await Client.GetPendingAsync()).OrderByDescending(a => a.RequestDate).ToList(),
|
||||
_ => throw new NotLoggedInException()
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ErrorHandler.Handle(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CloseApplication(VisaApplicationPreview application)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Client.CloseApplicationAsync(application.Id);
|
||||
application.Status = ApplicationStatus.Closed;
|
||||
StateHasChanged();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ErrorHandler.Handle(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DownloadApplication(VisaApplicationPreview application)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await Client.DownloadApplicationForApplicantAsync(application.Id);
|
||||
using var streamRef = new DotNetStreamReference(stream: response.Stream);
|
||||
|
||||
await JavaScriptInterop.InvokeVoidAsync("downloadFileFromStream", streamRef);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ErrorHandler.Handle(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
32
BlazorWebAssemblyVisaApiClient/Pages/Auth.razor
Normal file
32
BlazorWebAssemblyVisaApiClient/Pages/Auth.razor
Normal file
@@ -0,0 +1,32 @@
|
||||
@page "/"
|
||||
@using BlazorWebAssemblyVisaApiClient.Components
|
||||
@using BlazorWebAssemblyVisaApiClient.Components.Auth
|
||||
@using BlazorWebAssemblyVisaApiClient.Components.FormComponents.Applicants
|
||||
@using VisaApiClient
|
||||
@inherits BlazorWebAssemblyVisaApiClient.Components.Base.VisaClientComponentBase
|
||||
|
||||
<PageTitle>Authentication</PageTitle>
|
||||
|
||||
<div class="with-centered-content">
|
||||
<EditForm class="form" Model="loginData" OnValidSubmit="TryLogin">
|
||||
<DataAnnotationsValidator/>
|
||||
|
||||
<AuthDataInput AuthData="loginData"/><br/>
|
||||
|
||||
<input class="btn-outline-primary rounded" type="submit" value="Login"/>
|
||||
or
|
||||
<NavLink href="register">Register</NavLink >
|
||||
<Status><AuthComponent @ref="auth"/></Status>
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
private AuthData loginData = new();
|
||||
private AuthComponent auth = null!;
|
||||
|
||||
private async Task TryLogin()
|
||||
{
|
||||
await auth.TryAuthorize(loginData);
|
||||
}
|
||||
}
|
||||
58
BlazorWebAssemblyVisaApiClient/Pages/Authorities.razor
Normal file
58
BlazorWebAssemblyVisaApiClient/Pages/Authorities.razor
Normal file
@@ -0,0 +1,58 @@
|
||||
@page "/authorities"
|
||||
@using VisaApiClient
|
||||
@inherits BlazorWebAssemblyVisaApiClient.Components.Base.VisaClientComponentBase
|
||||
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th><th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var authority in authorities)
|
||||
{
|
||||
var path = $"authorities/{authority.Id}/{authority.Email}";
|
||||
<tr>
|
||||
<td>@authority.Email</td>
|
||||
<td>
|
||||
<NavLink href="@path">
|
||||
<button class="btn-outline-primary">Change</button>
|
||||
</NavLink>
|
||||
|
|
||||
<button class="btn-outline-danger" @onclick="() => Delete(authority)">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@code {
|
||||
private List<UserModel> authorities = [];
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
authorities = (await Client.GetAuthorityAccountsAsync()).ToList();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ErrorHandler.Handle(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Delete(UserModel authority)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Client.RemoveAuthorityAccountAsync(authority.Id);
|
||||
authorities.Remove(authority);
|
||||
StateHasChanged();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ErrorHandler.Handle(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
83
BlazorWebAssemblyVisaApiClient/Pages/Authority.razor
Normal file
83
BlazorWebAssemblyVisaApiClient/Pages/Authority.razor
Normal file
@@ -0,0 +1,83 @@
|
||||
@page "/authorities/{authorityId}/{oldEmail}"
|
||||
@using BlazorWebAssemblyVisaApiClient.Common.Exceptions
|
||||
@using BlazorWebAssemblyVisaApiClient.Components
|
||||
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Helpers
|
||||
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.UserDataProvider
|
||||
@using FluentValidation
|
||||
@using VisaApiClient
|
||||
@inherits BlazorWebAssemblyVisaApiClient.Components.Base.VisaClientComponentBase
|
||||
|
||||
<EditForm Model="model" class="with-centered-content">
|
||||
<div >
|
||||
<label>
|
||||
New email:<br/>
|
||||
<InputText class="rounded" @bind-Value="model.Email"/>
|
||||
</label><br/><p/>
|
||||
|
||||
<label>
|
||||
New password (leave blank if shouldn't be changed):<br/>
|
||||
<InputText type="password" class="rounded" @bind-Value="model.Password"/>
|
||||
</label><br/><p/>
|
||||
|
||||
<button class="btn-primary rounded" @onclick="Save">Save</button><br/>
|
||||
<Status @ref="status"/>
|
||||
</div>
|
||||
</EditForm>
|
||||
|
||||
@code
|
||||
{
|
||||
private Status status = null!;
|
||||
private ChangeAuthData model = new();
|
||||
|
||||
[Parameter] public string AuthorityId { get; set; } = null!;
|
||||
|
||||
[Parameter] public string OldEmail { get; set; } = null!;
|
||||
|
||||
[Inject] private IUserDataProvider UserDataProvider { get; set; } = null!;
|
||||
|
||||
[Inject] private IValidator<ChangeUserAuthDataRequest> ChangeUserAuthDataRequestValidator { get; set; } = null!;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (UserDataProvider.CurrentRole is not Constants.AdminRole)
|
||||
{
|
||||
throw new NotLoggedInException();
|
||||
}
|
||||
|
||||
model.Email = OldEmail;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ErrorHandler.Handle(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Save()
|
||||
{
|
||||
var request = new ChangeUserAuthDataRequest
|
||||
{
|
||||
UserId = Guid.Parse(AuthorityId),
|
||||
NewAuthData = model
|
||||
};
|
||||
|
||||
var validationResult = await ChangeUserAuthDataRequestValidator.ValidateAsync(request);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
status.SetError(validationResult.ToErrorsString());
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
status.SetMessage("Wait...");
|
||||
await Client.ChangeAuthorityAuthDataAsync(request);
|
||||
status.SetSuccess("Success");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ErrorHandler.Handle(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
360
BlazorWebAssemblyVisaApiClient/Pages/CreateApplication.razor
Normal file
360
BlazorWebAssemblyVisaApiClient/Pages/CreateApplication.razor
Normal file
@@ -0,0 +1,360 @@
|
||||
@page "/applications/new"
|
||||
@using System.Net
|
||||
@using AutoMapper
|
||||
@using BlazorWebAssemblyVisaApiClient.Components
|
||||
@using BlazorWebAssemblyVisaApiClient.Components.FormComponents
|
||||
@using BlazorWebAssemblyVisaApiClient.Components.FormComponents.VisaApplications
|
||||
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Helpers
|
||||
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.DateTimeProvider
|
||||
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.UserDataProvider
|
||||
@using BlazorWebAssemblyVisaApiClient.Validation
|
||||
@using BlazorWebAssemblyVisaApiClient.Validation.VisaApplications.Models
|
||||
@using FluentValidation
|
||||
@using Newtonsoft.Json.Linq
|
||||
@using VisaApiClient
|
||||
@inherits BlazorWebAssemblyVisaApiClient.Components.Base.VisaClientComponentBase
|
||||
|
||||
<PageTitle>New Application</PageTitle>
|
||||
|
||||
<div class="horizontal-centered-content">
|
||||
<h3>New application</h3>
|
||||
<EditForm class="form" Model="requestModel" OnValidSubmit="TryCreate">
|
||||
<ObjectGraphDataAnnotationsValidator/>
|
||||
|
||||
<div class="form-block">
|
||||
<h5>Visa@(Constants.RequiredFieldMarkup)</h5>
|
||||
<label>
|
||||
Destination Country:<br/>
|
||||
<InputText DisplayName="Destination Country" class="rounded" @bind-Value="requestModel.DestinationCountry"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => requestModel.DestinationCountry"></ValidationMessage><br/>
|
||||
|
||||
<label>
|
||||
Category:
|
||||
<EnumInputList Model="requestModel"
|
||||
EnumProperty="r => r.VisaCategory"
|
||||
OnChanged="StateHasChanged"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => requestModel.VisaCategory"></ValidationMessage><br/>
|
||||
|
||||
<label>
|
||||
Number of entries: <EnumInputList Model="requestModel" EnumProperty="r => r.RequestedNumberOfEntries"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => requestModel.RequestedNumberOfEntries"></ValidationMessage><br/>
|
||||
|
||||
<label>
|
||||
For group: <InputCheckbox @bind-Value="requestModel.IsForGroup"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => requestModel.IsForGroup"></ValidationMessage><br/>
|
||||
|
||||
<label>
|
||||
Valid for days:<br/>
|
||||
<InputNumber DisplayName="Valid days" class="rounded" @bind-Value="requestModel.ValidDaysRequested"/>
|
||||
</label>
|
||||
<ValidationMessage For="() => requestModel.ValidDaysRequested"></ValidationMessage><br/>
|
||||
</div>
|
||||
|
||||
<div class="form-block">
|
||||
<h5>Past visas</h5>
|
||||
@if (requestModel.PastVisas.Count > 0)
|
||||
{
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th><th>Issue date</th><th>Expiration date</th><th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var visa in requestModel.PastVisas)
|
||||
{
|
||||
<tr>
|
||||
<td>@visa.Name</td>
|
||||
<td>@visa.IssueDate.ToString("d.MM.yyyy")</td>
|
||||
<td>@visa.ExpirationDate.ToString("d.MM.yyyy")</td>
|
||||
<td>
|
||||
<input type="button" class="border-danger" @onclick="() => RemovePastVisa(visa)" value="X"/>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
<label>
|
||||
Name:<br/>
|
||||
<InputText DisplayName="Past visa name" class="rounded" @bind-Value="editableVisa.Name"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => editableVisa.Name"></ValidationMessage><br/>
|
||||
|
||||
<label>
|
||||
Issue date:<br/>
|
||||
<InputDate DisplayName="Past visa issue date"
|
||||
class="rounded"
|
||||
@bind-Value="editableVisa.IssueDate"
|
||||
max="@formattedNow"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => editableVisa.IssueDate"></ValidationMessage><br/>
|
||||
|
||||
<label>
|
||||
Expiration date:<br/>
|
||||
<InputDate DisplayName="Past visa expiration date"
|
||||
class="rounded"
|
||||
@bind-Value="editableVisa.ExpirationDate"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => editableVisa.ExpirationDate"></ValidationMessage><br/>
|
||||
|
||||
<input type="button" class="btn-outline-primary rounded"
|
||||
disabled="@(requestModel.PastVisas.Count == ConfigurationConstraints.MaxPastVisas)"
|
||||
@onclick="AddPastVisa" value="Add"/>
|
||||
<Status @ref="pastVisaStatus"/>
|
||||
</div>
|
||||
|
||||
<div class="form-block">
|
||||
<h5>Past visits</h5>
|
||||
@if (requestModel.PastVisits.Count > 0)
|
||||
{
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Destination Country</th><th>Start date</th><th>End date</th><th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var visit in requestModel.PastVisits)
|
||||
{
|
||||
<tr>
|
||||
<td>@visit.DestinationCountry</td>
|
||||
<td>@visit.StartDate.ToString("d.MM.yyyy")</td>
|
||||
<td>@visit.EndDate.ToString("d.MM.yyyy")</td>
|
||||
<td>
|
||||
<input type="button" class="border-danger" @onclick="() => RemovePastVisit(visit)" value="X"/>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
<label>
|
||||
Destination Country:<br/>
|
||||
<InputText DisplayName="Past visit destination Country" class="rounded" @bind-Value="editableVisit.DestinationCountry"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => editableVisit.DestinationCountry"></ValidationMessage><br/>
|
||||
|
||||
<label>
|
||||
Start date:<br/>
|
||||
<InputDate DisplayName="Past visit start date"
|
||||
class="rounded"
|
||||
@bind-Value="editableVisit.StartDate"
|
||||
max="@formattedNow"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => editableVisit.StartDate"></ValidationMessage><br/>
|
||||
|
||||
<label>
|
||||
End date:<br/>
|
||||
<InputDate DisplayName="Past visit end date"
|
||||
class="rounded"
|
||||
@bind-Value="editableVisit.EndDate"
|
||||
max="@formattedNow"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => editableVisit.EndDate"></ValidationMessage><br/>
|
||||
|
||||
<input type="button" class="btn-outline-primary rounded"
|
||||
disabled="@(requestModel.PastVisits.Count == ConfigurationConstraints.MaxPastVisits)"
|
||||
@onclick="AddPastVisit" value="Add"/>
|
||||
<Status @ref="pastVisitStatus"/>
|
||||
</div>
|
||||
|
||||
@if (requestModel.VisaCategory is VisaCategoryModel.Transit)
|
||||
{
|
||||
requestModel.PermissionToDestCountry ??= NewPermissionToDestCountry();
|
||||
<div class="form-block">
|
||||
<h5>Permission to destination Country@(Constants.RequiredFieldMarkup)</h5>
|
||||
<PermissionToDestCountryInput PermissionToDestCountry="requestModel.PermissionToDestCountry"/>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
requestModel.PermissionToDestCountry = null;
|
||||
}
|
||||
|
||||
@if (isNonResident)
|
||||
{
|
||||
requestModel.ReentryPermit ??= NewReentryPermit();
|
||||
<div class="form-block">
|
||||
<h5>Re-entry permission@(Constants.RequiredFieldMarkup)</h5>
|
||||
<ReentryPermitInput ReentryPermit="requestModel.ReentryPermit"/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<br/><input type="submit" class="btn-outline-primary rounded" value="Register"/>
|
||||
<ValidationSummary/>
|
||||
<Status @ref="status"/>
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private VisaApplicationCreateRequestModel requestModel = new();
|
||||
private Status status = null!;
|
||||
private Status pastVisaStatus = null!;
|
||||
private Status pastVisitStatus = null!;
|
||||
private bool isNonResident;
|
||||
private string formattedNow = null!;
|
||||
private PastVisaModel editableVisa = null!;
|
||||
private PastVisitModel editableVisit = null!;
|
||||
|
||||
[Inject] IDateTimeProvider DateTimeProvider { get; set; } = null!;
|
||||
|
||||
[Inject] IUserDataProvider UserDataProvider { get; set; } = null!;
|
||||
|
||||
[Inject] IValidator<VisaApplicationCreateRequestModel> VisaApplicationCreateRequestValidator { get; set; } = null!;
|
||||
|
||||
[Inject] IValidator<PastVisaModel> PastVisaModelValidator { get; set; } = null!;
|
||||
|
||||
[Inject] IValidator<PastVisitModel> PastVisitModelValidator { get; set; } = null!;
|
||||
|
||||
[Inject] IMapper Mapper { get; set; } = null!;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
editableVisa = NewPastVisa();
|
||||
editableVisit = NewPastVisit();
|
||||
requestModel.PermissionToDestCountry = NewPermissionToDestCountry();
|
||||
formattedNow = DateTimeProvider.FormattedNow();
|
||||
|
||||
try
|
||||
{
|
||||
isNonResident = (await UserDataProvider.GetApplicant()).IsNonResident;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ErrorHandler.Handle(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TryCreate()
|
||||
{
|
||||
var validationResult = await VisaApplicationCreateRequestValidator.ValidateAsync(requestModel);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
var errorsString = validationResult.ToErrorsString();
|
||||
status.SetError(errorsString);
|
||||
}
|
||||
|
||||
status.SetMessage("Wait...");
|
||||
|
||||
var request = Mapper.Map<VisaApplicationCreateRequest>(requestModel);
|
||||
try
|
||||
{
|
||||
await Client.CreateApplicationAsync(request);
|
||||
status.SetSuccess("Application created successfully.");
|
||||
}
|
||||
catch (ApiException<ProblemDetails> e)
|
||||
{
|
||||
if (e.StatusCode == (int)HttpStatusCode.BadRequest
|
||||
&& e.Result.AdditionalProperties.TryGetValue("errors", out var errors))
|
||||
{
|
||||
try
|
||||
{
|
||||
var errorsList = ((JArray)errors).ToObject<List<string>>();
|
||||
status.SetError(string.Join("<br/>", errorsList!));
|
||||
}
|
||||
catch (Exception inner)
|
||||
{
|
||||
ErrorHandler.Handle(inner);
|
||||
status.SetError("Error occured");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ErrorHandler.Handle(e);
|
||||
}
|
||||
}
|
||||
|
||||
private PastVisaModel NewPastVisa()
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ExpirationDate = DateTimeProvider.Now(),
|
||||
IssueDate = DateTimeProvider.Now()
|
||||
};
|
||||
}
|
||||
|
||||
private ReentryPermitModel NewReentryPermit()
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ExpirationDate = DateTimeProvider.Now()
|
||||
};
|
||||
}
|
||||
|
||||
private PermissionToDestCountryModel NewPermissionToDestCountry()
|
||||
{
|
||||
return new()
|
||||
{
|
||||
ExpirationDate = DateTimeProvider.Now()
|
||||
};
|
||||
}
|
||||
|
||||
private PastVisitModel NewPastVisit()
|
||||
{
|
||||
return new()
|
||||
{
|
||||
StartDate = DateTimeProvider.Now(),
|
||||
EndDate = DateTimeProvider.Now()
|
||||
};
|
||||
}
|
||||
|
||||
private void AddPastVisa()
|
||||
{
|
||||
if (requestModel.PastVisas.Count >= ConfigurationConstraints.MaxPastVisas)
|
||||
{
|
||||
pastVisaStatus.SetError($"{ConfigurationConstraints.MaxPastVisas} past visas is maximum");
|
||||
return;
|
||||
}
|
||||
|
||||
var validationResult = PastVisaModelValidator.Validate(editableVisa);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
pastVisaStatus.SetError(validationResult.ToErrorsString());
|
||||
return;
|
||||
}
|
||||
|
||||
requestModel.PastVisas.Add(editableVisa);
|
||||
editableVisa = NewPastVisa();
|
||||
pastVisaStatus.SetSuccess("Added successfully");
|
||||
}
|
||||
|
||||
private void RemovePastVisa(PastVisaModel visa)
|
||||
{
|
||||
requestModel.PastVisas.Remove(visa);
|
||||
}
|
||||
|
||||
private void AddPastVisit()
|
||||
{
|
||||
if (requestModel.PastVisits.Count >= ConfigurationConstraints.MaxPastVisits)
|
||||
{
|
||||
pastVisitStatus.SetError($"{ConfigurationConstraints.MaxPastVisits} past visits is maximum");
|
||||
return;
|
||||
}
|
||||
|
||||
var validationResult = PastVisitModelValidator.Validate(editableVisit);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
pastVisitStatus.SetError(validationResult.ToErrorsString());
|
||||
return;
|
||||
}
|
||||
|
||||
requestModel.PastVisits.Add(editableVisit);
|
||||
editableVisit = NewPastVisit();
|
||||
pastVisitStatus.SetSuccess("Added successfully");
|
||||
}
|
||||
|
||||
private void RemovePastVisit(PastVisitModel visit)
|
||||
{
|
||||
requestModel.PastVisits.Remove(visit);
|
||||
}
|
||||
}
|
||||
192
BlazorWebAssemblyVisaApiClient/Pages/Register.razor
Normal file
192
BlazorWebAssemblyVisaApiClient/Pages/Register.razor
Normal file
@@ -0,0 +1,192 @@
|
||||
@page "/register"
|
||||
@using System.Net
|
||||
@using AutoMapper
|
||||
@using BlazorWebAssemblyVisaApiClient.Components
|
||||
@using BlazorWebAssemblyVisaApiClient.Components.FormComponents
|
||||
@using BlazorWebAssemblyVisaApiClient.Components.FormComponents.Applicants
|
||||
@using BlazorWebAssemblyVisaApiClient.Infrastructure.Helpers
|
||||
@using BlazorWebAssemblyVisaApiClient.Validation
|
||||
@using BlazorWebAssemblyVisaApiClient.Validation.Applicants.Models
|
||||
@using FluentValidation
|
||||
@using Newtonsoft.Json
|
||||
@using Newtonsoft.Json.Linq
|
||||
@using VisaApiClient
|
||||
@inherits BlazorWebAssemblyVisaApiClient.Components.Base.VisaClientComponentBase
|
||||
|
||||
<PageTitle>Registration</PageTitle>
|
||||
|
||||
<div class="horizontal-centered-content">
|
||||
<h3>Registration data</h3>
|
||||
<EditForm class="form" Model="requestModel" OnValidSubmit="TryRegisterApplicant">
|
||||
<ObjectGraphDataAnnotationsValidator/>
|
||||
|
||||
<div class="form-block">
|
||||
<h5>Authentication data@(Constants.RequiredFieldMarkup)</h5>
|
||||
<AuthDataInput AuthData="requestModel.RegisterRequest.AuthData"/>
|
||||
</div>
|
||||
|
||||
<div class="form-block">
|
||||
<h5>Your Fullname</h5>
|
||||
<NameInput Name="requestModel.ApplicantName"/>
|
||||
</div>
|
||||
|
||||
<div class="form-block">
|
||||
<h5>Fullname of your mother</h5>
|
||||
<NameInput Name="requestModel.MotherName"/>
|
||||
</div>
|
||||
|
||||
<div class="form-block">
|
||||
<h5>Fullname of your father</h5>
|
||||
<NameInput Name="requestModel.FatherName"/>
|
||||
</div>
|
||||
|
||||
<div class="form-block">
|
||||
<h5>Your passport@(Constants.RequiredFieldMarkup)</h5>
|
||||
<PassportInput Passport="requestModel.Passport"/>
|
||||
</div>
|
||||
|
||||
<div class="form-block">
|
||||
<h5>Birth data@(Constants.RequiredFieldMarkup)</h5>
|
||||
<div >
|
||||
<label>
|
||||
Country of birth:<br/>
|
||||
<InputText DisplayName="Country of birth" class="rounded" @bind-Value="requestModel.CountryOfBirth"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => requestModel.CountryOfBirth"></ValidationMessage><br/>
|
||||
<label>
|
||||
City of birth:<br/>
|
||||
<InputText DisplayName="City of birth" class="rounded" @bind-Value="requestModel.CityOfBirth"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => requestModel.CityOfBirth"></ValidationMessage><br/>
|
||||
<label>
|
||||
Birth date:<br/>
|
||||
<InputDate DisplayName="Birth date" class="rounded" @bind-Value="requestModel.BirthDate" max="@formattedMaxBirthdayDate"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => requestModel.BirthDate"></ValidationMessage>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-block">
|
||||
<h5>Citizenship@(Constants.RequiredFieldMarkup)</h5>
|
||||
<div >
|
||||
<label>
|
||||
Citizenship:<br/>
|
||||
<InputText class="rounded" @bind-Value="requestModel.Citizenship"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => requestModel.Citizenship"></ValidationMessage><br/>
|
||||
<label>
|
||||
Citizenship by birth:<br/>
|
||||
<InputText DisplayName="Citizenship by birth" class="rounded" @bind-Value="requestModel.CitizenshipByBirth"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => requestModel.CitizenshipByBirth"></ValidationMessage>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-block">
|
||||
<h5>Address of your place of work@(Constants.RequiredFieldMarkup)</h5>
|
||||
<div >
|
||||
<AddressInput Address="requestModel.PlaceOfWork.Address"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-block">
|
||||
<h5>Place of work data@(Constants.RequiredFieldMarkup)</h5>
|
||||
<div >
|
||||
<PlaceOfWorkInput PlaceOfWork="requestModel.PlaceOfWork"/><br/>
|
||||
|
||||
<label>
|
||||
Job title:<br/>
|
||||
<InputText DisplayName="Job title" class="rounded" @bind-Value="requestModel.JobTitle"/>
|
||||
</label><br/>
|
||||
<ValidationMessage For="() => requestModel.JobTitle"></ValidationMessage>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-block">
|
||||
<h5>Other</h5>
|
||||
|
||||
<div >
|
||||
<label>
|
||||
Gender: <EnumInputList Model="requestModel" EnumProperty="r => r.Gender"/>
|
||||
</label>
|
||||
</div><br/>
|
||||
|
||||
<div >
|
||||
<label>
|
||||
Marital status: <EnumInputList Model="requestModel" EnumProperty="r => r.MaritalStatus"/>
|
||||
</label>
|
||||
</div><br/>
|
||||
|
||||
<div >
|
||||
<label>
|
||||
Non-resident: <InputCheckbox @bind-Value="requestModel.IsNonResident"/>
|
||||
</label>
|
||||
</div>
|
||||
</div><br/>
|
||||
|
||||
<input type="submit" class="btn-outline-primary" value="Register"/>
|
||||
<Status @ref="status"/>
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
private RegisterApplicantRequestModel requestModel = new();
|
||||
private Status status = null!;
|
||||
private string formattedMaxBirthdayDate = null!;
|
||||
|
||||
[Inject] IValidator<RegisterApplicantRequestModel> RegisterApplicantRequestValidator { get; set; } = null!;
|
||||
|
||||
[Inject] IMapper Mapper { get; set; } = null!;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
requestModel.BirthDate = DateTime.Now.AddYears(-ConfigurationConstraints.ApplicantMinAge);
|
||||
formattedMaxBirthdayDate = requestModel.BirthDate.ToString("yyyy-MM-dd");
|
||||
}
|
||||
|
||||
private async void TryRegisterApplicant()
|
||||
{
|
||||
var validationResult = await RegisterApplicantRequestValidator.ValidateAsync(requestModel);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
var errorsString = validationResult.ToErrorsString();
|
||||
status.SetError(errorsString);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
status.SetMessage("Wait...");
|
||||
|
||||
var request = Mapper.Map<RegisterApplicantRequest>(requestModel);
|
||||
try
|
||||
{
|
||||
await Client.RegisterAsync(request);
|
||||
status.SetSuccess("Register successful. Now log in.");
|
||||
}
|
||||
catch (ApiException<ProblemDetails> e)
|
||||
{
|
||||
if (e.StatusCode == (int)HttpStatusCode.BadRequest
|
||||
&& e.Result.AdditionalProperties.TryGetValue("errors", out var errors))
|
||||
{
|
||||
var errorsList = ((JArray)errors).ToObject<List<string>>();
|
||||
if (errorsList is null)
|
||||
{
|
||||
ErrorHandler.Handle(new JsonException("Can't convert validation errors to list"));
|
||||
return;
|
||||
}
|
||||
|
||||
status.SetError(string.Join("<br/>", errorsList));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
status.SetError("Error occured");
|
||||
ErrorHandler.Handle(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
BlazorWebAssemblyVisaApiClient/Program.cs
Normal file
37
BlazorWebAssemblyVisaApiClient/Program.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System.Reflection;
|
||||
using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.DateTimeProvider;
|
||||
using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.UserDataProvider;
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using VisaApiClient;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient;
|
||||
|
||||
public static class Program
|
||||
{
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
|
||||
builder.AddInfrastructure();
|
||||
|
||||
builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
}
|
||||
|
||||
private static void AddInfrastructure(this WebAssemblyHostBuilder builder)
|
||||
{
|
||||
const string baseAddress = "https://localhost:7168";
|
||||
builder.Services.AddScoped(_ => new HttpClient { BaseAddress = new(baseAddress) });
|
||||
builder.Services.AddBlazorBootstrap();
|
||||
builder.Services.AddScoped<IClient, Client>(sp => new(baseAddress, sp.GetRequiredService<HttpClient>()));
|
||||
|
||||
builder.Services.AddSingleton<IDateTimeProvider, DateTimeProvider>();
|
||||
builder.Services.AddScoped<IUserDataProvider, UserDataProvider>();
|
||||
builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:25927",
|
||||
"sslPort": 44345
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "http://localhost:5038",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "https://localhost:7200;http://localhost:5038",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient.Validation.Applicants.Models
|
||||
{
|
||||
public enum MaritalStatusModel
|
||||
{
|
||||
Other = 0,
|
||||
|
||||
Married = 1,
|
||||
|
||||
Unmarried = 2,
|
||||
|
||||
Separated = 3,
|
||||
|
||||
[Display(Name = "Widow or widower")]
|
||||
WidowOrWidower = 4
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using VisaApiClient;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient.Validation.Applicants.Models;
|
||||
|
||||
/// Model of place of work with attributes required for validation to work
|
||||
public class PlaceOfWorkModel
|
||||
{
|
||||
[Required]
|
||||
[StringLength(ConfigurationConstraints.PlaceOfWorkNameLength, MinimumLength = 1)]
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
[Required]
|
||||
[ValidateComplexType]
|
||||
public AddressModel Address { get; set; } = new AddressModel();
|
||||
|
||||
[Required]
|
||||
[StringLength(ConfigurationConstraints.PhoneNumberLength, MinimumLength = ConfigurationConstraints.PhoneNumberMinLength)]
|
||||
public string PhoneNum { get; set; } = default!;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using VisaApiClient;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient.Validation.Applicants.Models;
|
||||
|
||||
/// Model of request with attributes required for validation to work
|
||||
public class RegisterApplicantRequestModel
|
||||
{
|
||||
[Required]
|
||||
[ValidateComplexType]
|
||||
public RegisterRequestModel RegisterRequest { get; set; } = new();
|
||||
|
||||
[Required]
|
||||
[ValidateComplexType]
|
||||
public NameModel ApplicantName { get; set; } = new();
|
||||
|
||||
[Required]
|
||||
[ValidateComplexType]
|
||||
public PassportModel Passport { get; set; } = new();
|
||||
|
||||
[Required(AllowEmptyStrings = true)]
|
||||
public DateTimeOffset BirthDate { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(70, MinimumLength = 1)]
|
||||
public string CityOfBirth { get; set; } = default!;
|
||||
|
||||
[Required]
|
||||
[StringLength(70, MinimumLength = 1)]
|
||||
public string CountryOfBirth { get; set; } = default!;
|
||||
|
||||
[Required]
|
||||
[StringLength(30, MinimumLength = 1)]
|
||||
public string Citizenship { get; set; } = default!;
|
||||
|
||||
[Required]
|
||||
[StringLength(30, MinimumLength = 1)]
|
||||
public string CitizenshipByBirth { get; set; } = default!;
|
||||
|
||||
[Required(AllowEmptyStrings = true)]
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public Gender Gender { get; set; }
|
||||
|
||||
[Required(AllowEmptyStrings = true)]
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public MaritalStatusModel MaritalStatus { get; set; }
|
||||
|
||||
[Required]
|
||||
[ValidateComplexType]
|
||||
public NameModel FatherName { get; set; } = new();
|
||||
|
||||
[Required]
|
||||
[ValidateComplexType]
|
||||
public NameModel MotherName { get; set; } = new();
|
||||
|
||||
[Required]
|
||||
[StringLength(50, MinimumLength = 1)]
|
||||
public string JobTitle { get; set; } = default!;
|
||||
|
||||
[Required]
|
||||
[ValidateComplexType]
|
||||
public PlaceOfWorkModel PlaceOfWork { get; set; } = new();
|
||||
|
||||
public bool IsNonResident { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using VisaApiClient;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient.Validation.Applicants.Models;
|
||||
|
||||
/// Model of request with attributes required for validation to work
|
||||
public class RegisterRequestModel
|
||||
{
|
||||
[Required]
|
||||
[ValidateComplexType]
|
||||
public AuthData AuthData { get; set; } = new AuthData();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using FluentValidation;
|
||||
using VisaApiClient;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient.Validation.Applicants.Validators;
|
||||
|
||||
public class NameModelValidator : AbstractValidator<NameModel>
|
||||
{
|
||||
public NameModelValidator()
|
||||
{
|
||||
RuleFor(m => m.FirstName)
|
||||
.NotEmpty()
|
||||
.WithMessage("First Name can not be empty")
|
||||
.Matches(Constants.EnglishWordRegex)
|
||||
.WithMessage("First name must be in english characters")
|
||||
.MaximumLength(ConfigurationConstraints.NameLength)
|
||||
.WithMessage($"First Name length must be less than {ConfigurationConstraints.NameLength}");
|
||||
|
||||
RuleFor(m => m.Surname)
|
||||
.NotEmpty()
|
||||
.WithMessage("Surname can not be empty")
|
||||
.Matches(Constants.EnglishWordRegex)
|
||||
.WithMessage("Surname must be in english characters")
|
||||
.MaximumLength(ConfigurationConstraints.NameLength)
|
||||
.WithMessage($"Surname length must be less than {ConfigurationConstraints.NameLength}");
|
||||
|
||||
RuleFor(m => m.Patronymic)
|
||||
.Matches(Constants.EnglishWordRegex)
|
||||
.WithMessage("Patronymic must be in english characters")
|
||||
.MaximumLength(ConfigurationConstraints.NameLength)
|
||||
.WithMessage($"Patronymic length must be less than {ConfigurationConstraints.NameLength}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.DateTimeProvider;
|
||||
using FluentValidation;
|
||||
using VisaApiClient;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient.Validation.Applicants.Validators;
|
||||
|
||||
public class PassportModelValidator : AbstractValidator<PassportModel>
|
||||
{
|
||||
public PassportModelValidator(IDateTimeProvider dateTimeProvider)
|
||||
{
|
||||
RuleFor(r => r.Issuer)
|
||||
.NotEmpty()
|
||||
.WithMessage("Passport issuer can not be empty")
|
||||
.Matches(Constants.EnglishPhraseRegex)
|
||||
.WithMessage("Passport issuer field can contain only english letters, digits and special symbols")
|
||||
.MaximumLength(ConfigurationConstraints.IssuerNameLength)
|
||||
.WithMessage($"Passport issuer length must be less than {ConfigurationConstraints.IssuerNameLength}");
|
||||
|
||||
RuleFor(r => r.Number)
|
||||
.NotEmpty()
|
||||
.WithMessage("Passport number can not be empty")
|
||||
.Matches(Constants.EnglishPhraseRegex)
|
||||
.WithMessage("Passport number field can contain only english letters, digits and special symbols")
|
||||
.MaximumLength(ConfigurationConstraints.PassportNumberLength)
|
||||
.WithMessage($"Passport number length must be less than {ConfigurationConstraints.PassportNumberLength}");
|
||||
|
||||
RuleFor(r => r.ExpirationDate)
|
||||
.NotEmpty()
|
||||
.WithMessage("Passport expiration date can not be empty")
|
||||
.GreaterThan(dateTimeProvider.Now())
|
||||
.WithMessage("Can not approve visa for applicants with expired passport");
|
||||
|
||||
RuleFor(r => r.IssueDate)
|
||||
.NotEmpty()
|
||||
.WithMessage("Passport issue date can not be empty")
|
||||
.LessThanOrEqualTo(dateTimeProvider.Now())
|
||||
.WithMessage("Passport issue date must be in past");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using BlazorWebAssemblyVisaApiClient.Validation.Applicants.Models;
|
||||
using FluentValidation;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient.Validation.Applicants.Validators;
|
||||
|
||||
public class PlaceOfWorkModelValidator : AbstractValidator<PlaceOfWorkModel>
|
||||
{
|
||||
public PlaceOfWorkModelValidator()
|
||||
{
|
||||
RuleFor(p => p.Name)
|
||||
.NotEmpty()
|
||||
.WithMessage("Place of work name can not be empty")
|
||||
.Matches(Constants.EnglishPhraseRegex)
|
||||
.WithMessage("Place of work name field can contain only english letters, digits and special symbols")
|
||||
.MaximumLength(ConfigurationConstraints.PlaceOfWorkNameLength)
|
||||
.WithMessage($"Place of work name length must be less than {ConfigurationConstraints.PlaceOfWorkNameLength}");
|
||||
|
||||
RuleFor(p => p.PhoneNum)
|
||||
.NotEmpty()
|
||||
.WithMessage("Place of work phone number can not be empty")
|
||||
.Matches(Constants.PhoneNumRegex)
|
||||
.WithMessage("Place of work phone number field must be valid")
|
||||
.MaximumLength(ConfigurationConstraints.PhoneNumberLength)
|
||||
.WithMessage(
|
||||
$"Phone number length must be in range from {ConfigurationConstraints.PhoneNumberMinLength} to {ConfigurationConstraints.PhoneNumberLength}")
|
||||
.MinimumLength(ConfigurationConstraints.PhoneNumberMinLength)
|
||||
.WithMessage(
|
||||
$"Phone number length must be in range from {ConfigurationConstraints.PhoneNumberMinLength} to {ConfigurationConstraints.PhoneNumberLength}");
|
||||
|
||||
RuleFor(p => p.Address)
|
||||
.NotEmpty()
|
||||
.WithMessage("Place of work address can not be empty");
|
||||
|
||||
RuleFor(p => p.Address.Country)
|
||||
.NotEmpty()
|
||||
.WithMessage("Country name of place of work can not be empty")
|
||||
.Matches(Constants.EnglishPhraseRegex)
|
||||
.WithMessage("Place of work Country field can contain only english letters, digits and special symbols")
|
||||
.MaximumLength(ConfigurationConstraints.CountryNameLength)
|
||||
.WithMessage($"Country name of place of work length must be less than {ConfigurationConstraints.CountryNameLength}");
|
||||
|
||||
RuleFor(p => p.Address.City)
|
||||
.NotEmpty()
|
||||
.WithMessage("City name of place of work can not be empty")
|
||||
.Matches(Constants.EnglishPhraseRegex)
|
||||
.WithMessage("Place of work City field can contain only english letters, digits and special symbols")
|
||||
.MaximumLength(ConfigurationConstraints.CityNameLength)
|
||||
.WithMessage($"City name of place of work length must be less than {ConfigurationConstraints.CityNameLength}");
|
||||
|
||||
RuleFor(p => p.Address.Street)
|
||||
.NotEmpty()
|
||||
.WithMessage("Street name of place of work can not be empty")
|
||||
.Matches(Constants.EnglishPhraseRegex)
|
||||
.WithMessage("Place of work Street field can contain only english letters, digits and special symbols")
|
||||
.MaximumLength(ConfigurationConstraints.StreetNameLength)
|
||||
.WithMessage($"Street name of place of work length must be less than {ConfigurationConstraints.StreetNameLength}");
|
||||
|
||||
RuleFor(p => p.Address.Building)
|
||||
.NotEmpty()
|
||||
.WithMessage("Building of place of work can not be empty")
|
||||
.Matches(Constants.EnglishPhraseRegex)
|
||||
.WithMessage("Place of work building field can contain only english letters, digits and special symbols")
|
||||
.MaximumLength(ConfigurationConstraints.CountryNameLength)
|
||||
.WithMessage($"Building of place of work length must be less than {ConfigurationConstraints.BuildingNumberLength}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.DateTimeProvider;
|
||||
using BlazorWebAssemblyVisaApiClient.Validation.Applicants.Models;
|
||||
using FluentValidation;
|
||||
using VisaApiClient;
|
||||
using PlaceOfWorkModel = BlazorWebAssemblyVisaApiClient.Validation.Applicants.Models.PlaceOfWorkModel;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient.Validation.Applicants.Validators;
|
||||
|
||||
public class RegisterApplicantRequestValidator : AbstractValidator<RegisterApplicantRequestModel>
|
||||
{
|
||||
public RegisterApplicantRequestValidator(
|
||||
IDateTimeProvider dateTimeProvider,
|
||||
IValidator<NameModel> nameValidator,
|
||||
IValidator<RegisterRequestModel> registerRequestValidator,
|
||||
IValidator<PassportModel> passportValidator,
|
||||
IValidator<PlaceOfWorkModel> placeOfWorkModelValidator)
|
||||
{
|
||||
RuleFor(r => r.RegisterRequest)
|
||||
.NotEmpty()
|
||||
.SetValidator(registerRequestValidator);
|
||||
|
||||
RuleFor(r => r.ApplicantName)
|
||||
.NotEmpty()
|
||||
.SetValidator(nameValidator);
|
||||
|
||||
RuleFor(r => r.FatherName)
|
||||
.NotEmpty()
|
||||
.SetValidator(nameValidator);
|
||||
|
||||
RuleFor(r => r.MotherName)
|
||||
.NotEmpty()
|
||||
.SetValidator(nameValidator);
|
||||
|
||||
RuleFor(r => r.Passport)
|
||||
.NotEmpty()
|
||||
.SetValidator(passportValidator);
|
||||
|
||||
RuleFor(r => r.BirthDate)
|
||||
.NotEmpty()
|
||||
.WithMessage("Birth date can not be empty")
|
||||
.LessThanOrEqualTo(dateTimeProvider.Now().AddYears(-ConfigurationConstraints.ApplicantMinAge))
|
||||
.WithMessage($"Applicant must be older than {ConfigurationConstraints.ApplicantMinAge}");
|
||||
|
||||
RuleFor(r => r.CountryOfBirth)
|
||||
.NotEmpty()
|
||||
.WithMessage("Country of birth can not be empty")
|
||||
.Matches(Constants.EnglishPhraseRegex)
|
||||
.WithMessage("Country of birth field can contain only english letters, digits and special symbols")
|
||||
.MaximumLength(ConfigurationConstraints.CountryNameLength)
|
||||
.WithMessage($"Country of birth name length must be less than {ConfigurationConstraints.CountryNameLength}");
|
||||
|
||||
RuleFor(r => r.CityOfBirth)
|
||||
.NotEmpty()
|
||||
.WithMessage("City of birth can not be empty")
|
||||
.Matches(Constants.EnglishPhraseRegex)
|
||||
.WithMessage("City of birth field can contain only english letters, digits and special symbols")
|
||||
.MaximumLength(ConfigurationConstraints.CityNameLength)
|
||||
.WithMessage($"City of birth name length must be less than {ConfigurationConstraints.CityNameLength}");
|
||||
|
||||
RuleFor(r => r.Citizenship)
|
||||
.NotEmpty()
|
||||
.WithMessage("Citizenship can not be empty")
|
||||
.Matches(Constants.EnglishPhraseRegex)
|
||||
.WithMessage("Citizenship field can contain only english letters, digits and special symbols")
|
||||
.MaximumLength(ConfigurationConstraints.CitizenshipLength)
|
||||
.WithMessage($"Citizenship length must be less than {ConfigurationConstraints.CitizenshipLength}");
|
||||
|
||||
RuleFor(r => r.CitizenshipByBirth)
|
||||
.NotEmpty()
|
||||
.WithMessage("Citizenship by birth can not be empty")
|
||||
.Matches(Constants.EnglishPhraseRegex)
|
||||
.WithMessage("Citizenship by birth field can contain only english letters, digits and special symbols")
|
||||
.MaximumLength(ConfigurationConstraints.CitizenshipLength)
|
||||
.WithMessage($"Citizenship by birth length must be less than {ConfigurationConstraints.CitizenshipLength}");
|
||||
|
||||
RuleFor(r => r.Gender)
|
||||
.IsInEnum();
|
||||
|
||||
RuleFor(r => r.MaritalStatus)
|
||||
.IsInEnum();
|
||||
|
||||
RuleFor(r => r.JobTitle)
|
||||
.NotEmpty()
|
||||
.WithMessage("Title of job can not be empty")
|
||||
.Matches(Constants.EnglishPhraseRegex)
|
||||
.WithMessage("Title of job field can contain only english letters, digits and special symbols")
|
||||
.MaximumLength(ConfigurationConstraints.JobTitleLength)
|
||||
.WithMessage($"Title of job length must be less than {ConfigurationConstraints.JobTitleLength}");
|
||||
|
||||
RuleFor(r => r.PlaceOfWork)
|
||||
.NotEmpty()
|
||||
.SetValidator(placeOfWorkModelValidator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using FluentValidation;
|
||||
using VisaApiClient;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient.Validation.Auth;
|
||||
|
||||
public class AuthDataValidator : AbstractValidator<AuthData>
|
||||
{
|
||||
public AuthDataValidator()
|
||||
{
|
||||
RuleFor(d => d.Email)
|
||||
.NotEmpty()
|
||||
.WithMessage("Email can not be empty")
|
||||
.EmailAddress()
|
||||
.WithMessage("Email must be valid")
|
||||
.MaximumLength(ConfigurationConstraints.EmailLength)
|
||||
.WithMessage($"Email length must be less than {ConfigurationConstraints.EmailLength}");
|
||||
|
||||
RuleFor(d => d.Password)
|
||||
.NotEmpty()
|
||||
.WithMessage("Password can not be empty")
|
||||
.Matches(Constants.EnglishPhraseRegex)
|
||||
.WithMessage("Password can contain only english letters, digits and special symbols")
|
||||
.MaximumLength(ConfigurationConstraints.PasswordLength)
|
||||
.WithMessage($"Password length must be less than {ConfigurationConstraints.PasswordLength}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using FluentValidation;
|
||||
using VisaApiClient;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient.Validation.Auth;
|
||||
|
||||
public class ChangeUserAuthDataRequestValidator : AbstractValidator<ChangeUserAuthDataRequest>
|
||||
{
|
||||
public ChangeUserAuthDataRequestValidator()
|
||||
{
|
||||
RuleFor(r => r.NewAuthData)
|
||||
.NotEmpty();
|
||||
|
||||
RuleFor(r => r.NewAuthData.Email)
|
||||
.NotEmpty()
|
||||
.EmailAddress()
|
||||
.WithMessage("Email should be valid")
|
||||
.MaximumLength(ConfigurationConstraints.EmailLength)
|
||||
.WithMessage($"Email address length must be less than {ConfigurationConstraints.EmailLength}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using BlazorWebAssemblyVisaApiClient.Validation.Applicants.Models;
|
||||
using FluentValidation;
|
||||
using VisaApiClient;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient.Validation.Auth;
|
||||
|
||||
public class RegisterRequestModelValidator : AbstractValidator<RegisterRequestModel>
|
||||
{
|
||||
public RegisterRequestModelValidator(IValidator<AuthData> authDataValidator)
|
||||
{
|
||||
RuleFor(r => r.AuthData)
|
||||
.NotEmpty()
|
||||
.SetValidator(authDataValidator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace BlazorWebAssemblyVisaApiClient.Validation;
|
||||
|
||||
public static class ConfigurationConstraints
|
||||
{
|
||||
public const int CityNameLength = 70;
|
||||
public const int CountryNameLength = 70;
|
||||
public const int CitizenshipLength = 30;
|
||||
public const int ReentryPermitNumberLength = 25;
|
||||
public const int IssuerNameLength = 200;
|
||||
public const int VisaNameLength = 70;
|
||||
public const int StreetNameLength = 100;
|
||||
public const int PlaceOfWorkNameLength = 200;
|
||||
public const int NameLength = 50;
|
||||
public const int BuildingNumberLength = 10;
|
||||
public const int PassportNumberLength = 20;
|
||||
public const int PhoneNumberLength = 13;
|
||||
public const int PhoneNumberMinLength = 11;
|
||||
public const int EmailLength = 254;
|
||||
public const int PasswordLength = 50;
|
||||
public const int ApplicantMinAge = 14;
|
||||
public const int JobTitleLength = 50;
|
||||
public const int MaxValidDays = 90;
|
||||
public const int MaxPastVisas = 10;
|
||||
public const int MaxPastVisits = 10;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using VisaApiClient;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient.Validation.VisaApplications.Models;
|
||||
|
||||
/// Model for request for data annotations validation to work
|
||||
public class VisaApplicationCreateRequestModel
|
||||
{
|
||||
[ValidateComplexType]
|
||||
public ReentryPermitModel? ReentryPermit { get; set; } = default!;
|
||||
|
||||
[Required]
|
||||
[MaxLength(ConfigurationConstraints.CountryNameLength)]
|
||||
public string DestinationCountry { get; set; } = default!;
|
||||
|
||||
[Required]
|
||||
public VisaCategoryModel VisaCategory { get; set; }
|
||||
|
||||
[Required]
|
||||
public bool IsForGroup { get; set; }
|
||||
|
||||
[Required]
|
||||
public RequestedNumberOfEntries RequestedNumberOfEntries { get; set; }
|
||||
|
||||
[Required]
|
||||
[Range(0, ConfigurationConstraints.MaxValidDays)]
|
||||
public int ValidDaysRequested { get; set; }
|
||||
|
||||
[ValidateComplexType]
|
||||
public List<PastVisaModel> PastVisas { get; set; } = [];
|
||||
|
||||
[ValidateComplexType]
|
||||
public PermissionToDestCountryModel? PermissionToDestCountry { get; set; } = default!;
|
||||
|
||||
[ValidateComplexType]
|
||||
public List<PastVisitModel> PastVisits { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient.Validation.VisaApplications.Models
|
||||
{
|
||||
public enum VisaCategoryModel
|
||||
{
|
||||
Transit = 0,
|
||||
[Display(Name = "Short dated")]
|
||||
ShortDated = 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.DateTimeProvider;
|
||||
using FluentValidation;
|
||||
using VisaApiClient;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient.Validation.VisaApplications.Validators;
|
||||
|
||||
public class PastVisaModelValidator : AbstractValidator<PastVisaModel>
|
||||
{
|
||||
public PastVisaModelValidator(IDateTimeProvider dateTimeProvider)
|
||||
{
|
||||
RuleFor(v => v.ExpirationDate)
|
||||
.NotEmpty()
|
||||
.WithMessage("Expiration date of past visa can not be empty")
|
||||
.GreaterThan(v => v.IssueDate)
|
||||
.WithMessage("Past visa expiration date can not be earlier than issue date");
|
||||
|
||||
RuleFor(v => v.IssueDate)
|
||||
.NotEmpty()
|
||||
.WithMessage("Issue date of past visa can not be empty")
|
||||
.LessThan(dateTimeProvider.Now())
|
||||
.WithMessage("Issue date of past visa must be in past");
|
||||
|
||||
RuleFor(v => v.Name)
|
||||
.NotEmpty()
|
||||
.WithMessage("Name of past visa can not be empty")
|
||||
.Matches(Constants.EnglishPhraseRegex)
|
||||
.WithMessage("Name of past visa can contain only english letters, digits and special symbols")
|
||||
.MaximumLength(ConfigurationConstraints.VisaNameLength)
|
||||
.WithMessage($"Past visa name length must be less than {ConfigurationConstraints.VisaNameLength}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.DateTimeProvider;
|
||||
using FluentValidation;
|
||||
using VisaApiClient;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient.Validation.VisaApplications.Validators;
|
||||
|
||||
public class PastVisitModelValidator : AbstractValidator<PastVisitModel>
|
||||
{
|
||||
public PastVisitModelValidator(IDateTimeProvider dateTimeProvider)
|
||||
{
|
||||
RuleFor(v => v.StartDate)
|
||||
.NotEmpty()
|
||||
.WithMessage("Start date of past visit can not be empty")
|
||||
.LessThan(v => v.EndDate)
|
||||
.WithMessage("Start date of past visit must be earlier than end date")
|
||||
.LessThan(dateTimeProvider.Now())
|
||||
.WithMessage("Start date of past visit must be in past");
|
||||
|
||||
RuleFor(v => v.EndDate)
|
||||
.NotEmpty()
|
||||
.WithMessage("End date of past visit can not be empty");
|
||||
|
||||
RuleFor(v => v.DestinationCountry)
|
||||
.NotEmpty()
|
||||
.WithMessage("Destination Country of past visit can not be null")
|
||||
.Matches(Constants.EnglishPhraseRegex)
|
||||
.WithMessage("Destination Country of past visit can contain only english letters, digits and special symbols")
|
||||
.MaximumLength(ConfigurationConstraints.CountryNameLength)
|
||||
.WithMessage($"Destination Country of past visit length must be less than {ConfigurationConstraints.CountryNameLength}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.DateTimeProvider;
|
||||
using FluentValidation;
|
||||
using VisaApiClient;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient.Validation.VisaApplications.Validators;
|
||||
|
||||
public class PermissionToDestCountryModelValidator : AbstractValidator<PermissionToDestCountryModel?>
|
||||
{
|
||||
public PermissionToDestCountryModelValidator(IDateTimeProvider dateTimeProvider)
|
||||
{
|
||||
RuleFor(p => p!.ExpirationDate)
|
||||
.NotEmpty()
|
||||
.WithMessage("Expiration date of permission to destination Country can not be empty")
|
||||
.GreaterThan(dateTimeProvider.Now())
|
||||
.WithMessage("Permission to destination Country must not be expired");
|
||||
|
||||
RuleFor(p => p!.Issuer)
|
||||
.NotEmpty()
|
||||
.WithMessage("Issuer of permission for destination Country can not be empty")
|
||||
.Matches(Constants.EnglishPhraseRegex)
|
||||
.WithMessage("Issuer of permission for destination Country can contain only english letters, digits and special symbols")
|
||||
.MaximumLength(ConfigurationConstraints.IssuerNameLength)
|
||||
.WithMessage($"Issuer of permission to destination Country length must be less than {ConfigurationConstraints.IssuerNameLength}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.DateTimeProvider;
|
||||
using FluentValidation;
|
||||
using VisaApiClient;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient.Validation.VisaApplications.Validators;
|
||||
|
||||
public class ReentryPermitModelValidator : AbstractValidator<ReentryPermitModel?>
|
||||
{
|
||||
public ReentryPermitModelValidator(IDateTimeProvider dateTimeProvider)
|
||||
{
|
||||
RuleFor(p => p!.Number)
|
||||
.NotEmpty()
|
||||
.WithMessage("Re-entry permit number can not be empty")
|
||||
.Matches(Constants.EnglishPhraseRegex)
|
||||
.WithMessage("Re-entry permit number can contain only english letters, digits and special symbols")
|
||||
.MaximumLength(ConfigurationConstraints.ReentryPermitNumberLength)
|
||||
.WithMessage($"Re-entry permit number length must be less than {ConfigurationConstraints.ReentryPermitNumberLength}");
|
||||
|
||||
RuleFor(p => p!.ExpirationDate)
|
||||
.NotEmpty()
|
||||
.WithMessage("Re-entry permit expiration date can not be empty")
|
||||
.GreaterThan(dateTimeProvider.Now())
|
||||
.WithMessage("Re-entry permit must not be expired");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using BlazorWebAssemblyVisaApiClient.Infrastructure.Services.UserDataProvider;
|
||||
using BlazorWebAssemblyVisaApiClient.Validation.VisaApplications.Models;
|
||||
using FluentValidation;
|
||||
using VisaApiClient;
|
||||
|
||||
namespace BlazorWebAssemblyVisaApiClient.Validation.VisaApplications.Validators;
|
||||
|
||||
public class VisaApplicationCreateRequestValidator : AbstractValidator<VisaApplicationCreateRequestModel>
|
||||
{
|
||||
public VisaApplicationCreateRequestValidator(
|
||||
IValidator<ReentryPermitModel?> reentryPermitModelValidator,
|
||||
IValidator<PastVisaModel> pastVisaModelValidator,
|
||||
IValidator<PermissionToDestCountryModel?> permissionToDestCountryModelValidator,
|
||||
IValidator<PastVisitModel> pastVisitModelValidator,
|
||||
IUserDataProvider userDataProvider)
|
||||
{
|
||||
RuleFor(r => r.PermissionToDestCountry)
|
||||
.NotEmpty()
|
||||
.WithMessage("For transit you must provide permission to destination country")
|
||||
.SetValidator(permissionToDestCountryModelValidator)
|
||||
.When(r => r.VisaCategory is VisaCategoryModel.Transit);
|
||||
|
||||
RuleFor(r => r.ReentryPermit)
|
||||
.NotEmpty()
|
||||
.WithMessage("Non-residents must provide re-entry permission")
|
||||
.SetValidator(reentryPermitModelValidator)
|
||||
.WhenAsync(async (_, _) =>
|
||||
(await userDataProvider.GetApplicant()).IsNonResident);
|
||||
|
||||
RuleFor(r => r.DestinationCountry)
|
||||
.NotEmpty()
|
||||
.WithMessage("Destination country can not be empty");
|
||||
|
||||
RuleFor(r => r.VisaCategory)
|
||||
.IsInEnum();
|
||||
|
||||
RuleFor(r => r.RequestedNumberOfEntries)
|
||||
.IsInEnum();
|
||||
|
||||
RuleFor(r => r.ValidDaysRequested)
|
||||
.GreaterThan(0)
|
||||
.WithMessage($"Valid days requested should be positive number and less than {ConfigurationConstraints.MaxValidDays}")
|
||||
.LessThanOrEqualTo(ConfigurationConstraints.MaxValidDays)
|
||||
.WithMessage($"Valid days requested must be less than or equal to {ConfigurationConstraints.MaxValidDays}");
|
||||
|
||||
RuleForEach(r => r.PastVisas)
|
||||
.SetValidator(pastVisaModelValidator);
|
||||
|
||||
RuleForEach(r => r.PastVisits)
|
||||
.SetValidator(pastVisitModelValidator);
|
||||
}
|
||||
}
|
||||
11
BlazorWebAssemblyVisaApiClient/_Imports.razor
Normal file
11
BlazorWebAssemblyVisaApiClient/_Imports.razor
Normal file
@@ -0,0 +1,11 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||
@using Microsoft.JSInterop
|
||||
@using BlazorWebAssemblyVisaApiClient
|
||||
@using BlazorWebAssemblyVisaApiClient.Layout
|
||||
@using BlazorBootstrap
|
||||
134
BlazorWebAssemblyVisaApiClient/wwwroot/css/app.css
Normal file
134
BlazorWebAssemblyVisaApiClient/wwwroot/css/app.css
Normal file
@@ -0,0 +1,134 @@
|
||||
html, body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a, .btn-link {
|
||||
color: #0071c1;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1.1rem;
|
||||
}
|
||||
|
||||
.valid.modified:not([type=checkbox]) {
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
outline: 1px solid red;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.blazor-error-boundary {
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
padding: 1rem 1rem 1rem 3.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blazor-error-boundary::after {
|
||||
content: "An error has occurred."
|
||||
}
|
||||
|
||||
.loading-progress {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
margin: 20vh auto 1rem auto;
|
||||
}
|
||||
|
||||
.loading-progress circle {
|
||||
fill: none;
|
||||
stroke: #e0e0e0;
|
||||
stroke-width: 0.6rem;
|
||||
transform-origin: 50% 50%;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.loading-progress circle:last-child {
|
||||
stroke: #1b6ec2;
|
||||
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
|
||||
transition: stroke-dasharray 0.05s ease-in-out;
|
||||
}
|
||||
|
||||
.loading-progress-text {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
|
||||
}
|
||||
|
||||
.loading-progress-text:after {
|
||||
content: var(--blazor-load-percentage-text, "Loading");
|
||||
}
|
||||
|
||||
.fullscreen {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.with-centered-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.with-centered-content > * {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.horizontal-centered-content > * {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form {
|
||||
width: fit-content;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.form-block {
|
||||
margin: 10px 20px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
code {
|
||||
color: #c02d76;
|
||||
}
|
||||
7
BlazorWebAssemblyVisaApiClient/wwwroot/css/bootstrap/bootstrap.min.css
vendored
Normal file
7
BlazorWebAssemblyVisaApiClient/wwwroot/css/bootstrap/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
BlazorWebAssemblyVisaApiClient/wwwroot/favicon.png
Normal file
BIN
BlazorWebAssemblyVisaApiClient/wwwroot/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
BlazorWebAssemblyVisaApiClient/wwwroot/icon-192.png
Normal file
BIN
BlazorWebAssemblyVisaApiClient/wwwroot/icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
43
BlazorWebAssemblyVisaApiClient/wwwroot/index.html
Normal file
43
BlazorWebAssemblyVisaApiClient/wwwroot/index.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BlazorWebAssemblyVisaApiClient</title>
|
||||
<base href="/" />
|
||||
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet" />
|
||||
<link href="_content/Blazor.Bootstrap/blazor.bootstrap.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="css/app.css" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<link href="BlazorWebAssemblyVisaApiClient.styles.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<svg class="loading-progress">
|
||||
<circle r="40%" cx="50%" cy="50%" />
|
||||
<circle r="40%" cx="50%" cy="50%" />
|
||||
</svg>
|
||||
<div class="loading-progress-text"></div>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
|
||||
<!-- Add chart.js reference if chart components are used in your application. -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.0.1/chart.umd.js" integrity="sha512-gQhCDsnnnUfaRzD8k1L5llCCV6O9HN09zClIzzeJ8OJ9MpGmIlCxm+pdCkqTwqJ4JcjbojFr79rl2F1mzcoLMQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<!-- Add chartjs-plugin-datalabels.min.js reference if chart components with data label feature is used in your application. -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-datalabels/2.2.0/chartjs-plugin-datalabels.min.js" integrity="sha512-JPcRR8yFa8mmCsfrw4TNte1ZvF1e3+1SdGMslZvmrzDYxS69J7J49vkFL8u6u8PlPJK+H3voElBtUCzaXj+6ig==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<!-- Add sortable.js reference if SortableList component is used in your application. -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
|
||||
<script src="_content/Blazor.Bootstrap/blazor.bootstrap.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user