Initial commit: TrueCV CV verification platform
Clean architecture solution with: - Domain: Entities (User, CVCheck, CVFlag, CompanyCache) and Enums - Application: Service interfaces, DTOs, and models - Infrastructure: EF Core, Identity, Hangfire, external API clients, services - Web: Blazor Server UI with pages and components Features: - CV upload and parsing (PDF/DOCX) using Claude API - Employment verification against Companies House API - Timeline analysis for gaps and overlaps - Veracity scoring algorithm - Background job processing with Hangfire - Azure Blob Storage for file storage Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
148
src/TrueCV.Web/Components/Pages/Account/Login.razor
Normal file
148
src/TrueCV.Web/Components/Pages/Account/Login.razor
Normal file
@@ -0,0 +1,148 @@
|
||||
@page "/account/login"
|
||||
@using TrueCV.Web.Components.Layout
|
||||
@layout MainLayout
|
||||
@rendermode InteractiveServer
|
||||
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using TrueCV.Infrastructure.Identity
|
||||
|
||||
@inject SignInManager<ApplicationUser> SignInManager
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<PageTitle>Login - TrueCV</PageTitle>
|
||||
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-5">
|
||||
<div class="card border-0 shadow">
|
||||
<div class="card-body p-5">
|
||||
<div class="text-center mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="bi bi-patch-check-fill text-primary mb-3" viewBox="0 0 16 16">
|
||||
<path d="M10.067.87a2.89 2.89 0 0 0-4.134 0l-.622.638-.89-.011a2.89 2.89 0 0 0-2.924 2.924l.01.89-.636.622a2.89 2.89 0 0 0 0 4.134l.637.622-.011.89a2.89 2.89 0 0 0 2.924 2.924l.89-.01.622.636a2.89 2.89 0 0 0 4.134 0l.622-.637.89.011a2.89 2.89 0 0 0 2.924-2.924l-.01-.89.636-.622a2.89 2.89 0 0 0 0-4.134l-.637-.622.011-.89a2.89 2.89 0 0 0-2.924-2.924l-.89.01-.622-.636zm.287 5.984-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7 8.793l2.646-2.647a.5.5 0 0 1 .708.708z"/>
|
||||
</svg>
|
||||
<h3 class="fw-bold">Welcome Back</h3>
|
||||
<p class="text-muted">Sign in to your TrueCV account</p>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(_errorMessage))
|
||||
{
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
@_errorMessage
|
||||
<button type="button" class="btn-close" @onclick="() => _errorMessage = null" aria-label="Close"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<EditForm Model="_model" OnValidSubmit="HandleLogin" FormName="login">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email address</label>
|
||||
<InputText id="email" class="form-control form-control-lg" @bind-Value="_model.Email"
|
||||
placeholder="name@example.com" />
|
||||
<ValidationMessage For="() => _model.Email" class="text-danger" />
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<InputText id="password" type="password" class="form-control form-control-lg"
|
||||
@bind-Value="_model.Password" placeholder="Enter your password" />
|
||||
<ValidationMessage For="() => _model.Password" class="text-danger" />
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<InputCheckbox id="rememberMe" class="form-check-input" @bind-Value="_model.RememberMe" />
|
||||
<label class="form-check-label" for="rememberMe">
|
||||
Remember me
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary btn-lg" disabled="@_isLoading">
|
||||
@if (_isLoading)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||
<span>Signing in...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Sign In</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
|
||||
<hr class="my-4" />
|
||||
|
||||
<div class="text-center">
|
||||
<p class="mb-0">
|
||||
Don't have an account?
|
||||
<a href="/account/register" class="text-decoration-none fw-medium">Create one</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private LoginModel _model = new();
|
||||
private bool _isLoading;
|
||||
private string? _errorMessage;
|
||||
|
||||
[SupplyParameterFromQuery]
|
||||
public string? ReturnUrl { get; set; }
|
||||
|
||||
private async Task HandleLogin()
|
||||
{
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
var result = await SignInManager.PasswordSignInAsync(
|
||||
_model.Email,
|
||||
_model.Password,
|
||||
_model.RememberMe,
|
||||
lockoutOnFailure: false);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
var returnUrl = string.IsNullOrEmpty(ReturnUrl) ? "/dashboard" : ReturnUrl;
|
||||
NavigationManager.NavigateTo(returnUrl, forceLoad: true);
|
||||
}
|
||||
else if (result.IsLockedOut)
|
||||
{
|
||||
_errorMessage = "This account has been locked out. Please try again later.";
|
||||
}
|
||||
else if (result.IsNotAllowed)
|
||||
{
|
||||
_errorMessage = "This account is not allowed to sign in.";
|
||||
}
|
||||
else
|
||||
{
|
||||
_errorMessage = "Invalid email or password.";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_errorMessage = $"An error occurred: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class LoginModel
|
||||
{
|
||||
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Email is required")]
|
||||
[System.ComponentModel.DataAnnotations.EmailAddress(ErrorMessage = "Invalid email format")]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Password is required")]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
public bool RememberMe { get; set; }
|
||||
}
|
||||
}
|
||||
163
src/TrueCV.Web/Components/Pages/Account/Register.razor
Normal file
163
src/TrueCV.Web/Components/Pages/Account/Register.razor
Normal file
@@ -0,0 +1,163 @@
|
||||
@page "/account/register"
|
||||
@using TrueCV.Web.Components.Layout
|
||||
@layout MainLayout
|
||||
@rendermode InteractiveServer
|
||||
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using TrueCV.Infrastructure.Identity
|
||||
|
||||
@inject UserManager<ApplicationUser> UserManager
|
||||
@inject SignInManager<ApplicationUser> SignInManager
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<PageTitle>Register - TrueCV</PageTitle>
|
||||
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-5">
|
||||
<div class="card border-0 shadow">
|
||||
<div class="card-body p-5">
|
||||
<div class="text-center mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="bi bi-patch-check-fill text-primary mb-3" viewBox="0 0 16 16">
|
||||
<path d="M10.067.87a2.89 2.89 0 0 0-4.134 0l-.622.638-.89-.011a2.89 2.89 0 0 0-2.924 2.924l.01.89-.636.622a2.89 2.89 0 0 0 0 4.134l.637.622-.011.89a2.89 2.89 0 0 0 2.924 2.924l.89-.01.622.636a2.89 2.89 0 0 0 4.134 0l.622-.637.89.011a2.89 2.89 0 0 0 2.924-2.924l-.01-.89.636-.622a2.89 2.89 0 0 0 0-4.134l-.637-.622.011-.89a2.89 2.89 0 0 0-2.924-2.924l-.89.01-.622-.636zm.287 5.984-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7 8.793l2.646-2.647a.5.5 0 0 1 .708.708z"/>
|
||||
</svg>
|
||||
<h3 class="fw-bold">Create Account</h3>
|
||||
<p class="text-muted">Start verifying CVs with confidence</p>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(_errorMessage))
|
||||
{
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
@_errorMessage
|
||||
<button type="button" class="btn-close" @onclick="() => _errorMessage = null" aria-label="Close"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<EditForm Model="_model" OnValidSubmit="HandleRegister" FormName="register">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email address</label>
|
||||
<InputText id="email" class="form-control form-control-lg" @bind-Value="_model.Email"
|
||||
placeholder="name@example.com" />
|
||||
<ValidationMessage For="() => _model.Email" class="text-danger" />
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<InputText id="password" type="password" class="form-control form-control-lg"
|
||||
@bind-Value="_model.Password" placeholder="Create a password" />
|
||||
<ValidationMessage For="() => _model.Password" class="text-danger" />
|
||||
<div class="form-text">Password must be at least 6 characters.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="confirmPassword" class="form-label">Confirm Password</label>
|
||||
<InputText id="confirmPassword" type="password" class="form-control form-control-lg"
|
||||
@bind-Value="_model.ConfirmPassword" placeholder="Confirm your password" />
|
||||
<ValidationMessage For="() => _model.ConfirmPassword" class="text-danger" />
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary btn-lg" disabled="@_isLoading">
|
||||
@if (_isLoading)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||
<span>Creating account...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Create Account</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
|
||||
<hr class="my-4" />
|
||||
|
||||
<div class="text-center">
|
||||
<p class="mb-0">
|
||||
Already have an account?
|
||||
<a href="/account/login" class="text-decoration-none fw-medium">Sign in</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<small class="text-muted">
|
||||
By creating an account, you agree to our
|
||||
<a href="#" class="text-decoration-none">Terms of Service</a>
|
||||
and
|
||||
<a href="#" class="text-decoration-none">Privacy Policy</a>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private RegisterModel _model = new();
|
||||
private bool _isLoading;
|
||||
private string? _errorMessage;
|
||||
|
||||
private async Task HandleRegister()
|
||||
{
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (_model.Password != _model.ConfirmPassword)
|
||||
{
|
||||
_errorMessage = "Passwords do not match.";
|
||||
_isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var user = new ApplicationUser
|
||||
{
|
||||
UserName = _model.Email,
|
||||
Email = _model.Email,
|
||||
Plan = Domain.Enums.UserPlan.Free,
|
||||
ChecksUsedThisMonth = 0
|
||||
};
|
||||
|
||||
var result = await UserManager.CreateAsync(user, _model.Password);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await SignInManager.SignInAsync(user, isPersistent: false);
|
||||
NavigationManager.NavigateTo("/dashboard", forceLoad: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
var errors = result.Errors.Select(e => e.Description);
|
||||
_errorMessage = string.Join(" ", errors);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_errorMessage = $"An error occurred: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RegisterModel
|
||||
{
|
||||
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Email is required")]
|
||||
[System.ComponentModel.DataAnnotations.EmailAddress(ErrorMessage = "Invalid email format")]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Password is required")]
|
||||
[System.ComponentModel.DataAnnotations.MinLength(6, ErrorMessage = "Password must be at least 6 characters")]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Please confirm your password")]
|
||||
[System.ComponentModel.DataAnnotations.Compare(nameof(Password), ErrorMessage = "Passwords do not match")]
|
||||
public string ConfirmPassword { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
261
src/TrueCV.Web/Components/Pages/Check.razor
Normal file
261
src/TrueCV.Web/Components/Pages/Check.razor
Normal file
@@ -0,0 +1,261 @@
|
||||
@page "/check"
|
||||
@attribute [Authorize]
|
||||
@rendermode InteractiveServer
|
||||
|
||||
@inject ICVCheckService CVCheckService
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||
|
||||
<PageTitle>Upload CV - TrueCV</PageTitle>
|
||||
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="text-center mb-4">
|
||||
<h1 class="fw-bold">Upload CV for Verification</h1>
|
||||
<p class="text-muted lead">Upload a CV in PDF or DOCX format to begin the verification process</p>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(_errorMessage))
|
||||
{
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
@_errorMessage
|
||||
<button type="button" class="btn-close" @onclick="() => _errorMessage = null" aria-label="Close"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="card border-0 shadow">
|
||||
<div class="card-body p-5">
|
||||
@if (_isUploading)
|
||||
{
|
||||
<div class="text-center py-5">
|
||||
<div class="spinner-border text-primary mb-3" role="status" style="width: 3rem; height: 3rem;">
|
||||
<span class="visually-hidden">Uploading...</span>
|
||||
</div>
|
||||
<h5 class="mb-2">Uploading your CV...</h5>
|
||||
<p class="text-muted">Please wait while we process your file</p>
|
||||
<div class="progress" style="height: 8px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
role="progressbar"
|
||||
style="width: @(_uploadProgress)%"
|
||||
aria-valuenow="@_uploadProgress"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="upload-area @(_isDragging ? "dragging" : "")"
|
||||
@ondragenter="HandleDragEnter"
|
||||
@ondragleave="HandleDragLeave"
|
||||
@ondragover:preventDefault
|
||||
@ondrop="HandleDrop"
|
||||
@ondrop:preventDefault>
|
||||
|
||||
<InputFile OnChange="HandleFileSelected"
|
||||
accept=".pdf,.docx"
|
||||
class="d-none"
|
||||
id="fileInput" />
|
||||
|
||||
<label for="fileInput" class="d-block text-center py-5 cursor-pointer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor" class="bi bi-cloud-arrow-up text-primary mb-3" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M7.646 5.146a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1-.708.708L8.5 6.707V10.5a.5.5 0 0 1-1 0V6.707L6.354 7.854a.5.5 0 1 1-.708-.708l2-2z"/>
|
||||
<path d="M4.406 3.342A5.53 5.53 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773 16 11.569 14.502 13 12.687 13H3.781C1.708 13 0 11.366 0 9.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383zm.653.757c-.757.653-1.153 1.44-1.153 2.056v.448l-.445.049C2.064 6.805 1 7.952 1 9.318 1 10.785 2.23 12 3.781 12h8.906C13.98 12 15 10.988 15 9.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 4.825 10.328 3 8 3a4.53 4.53 0 0 0-2.941 1.1z"/>
|
||||
</svg>
|
||||
<h5 class="mb-2">Drag and drop your CV here</h5>
|
||||
<p class="text-muted mb-3">or click to browse files</p>
|
||||
<span class="badge bg-light text-dark">Accepted formats: PDF, DOCX</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@if (_selectedFile is not null)
|
||||
{
|
||||
<div class="mt-4 p-3 bg-light rounded d-flex align-items-center justify-content-between">
|
||||
<div class="d-flex align-items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-file-earmark-text text-primary me-3" viewBox="0 0 16 16">
|
||||
<path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"/>
|
||||
<path d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5L9.5 0zm0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="mb-0 fw-medium">@_selectedFile.Name</p>
|
||||
<small class="text-muted">@FormatFileSize(_selectedFile.Size)</small>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-outline-danger btn-sm" @onclick="ClearFile">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-x-lg" viewBox="0 0 16 16">
|
||||
<path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<button class="btn btn-primary btn-lg px-5" @onclick="UploadFile">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-upload me-2" viewBox="0 0 16 16">
|
||||
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
||||
<path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708l3-3z"/>
|
||||
</svg>
|
||||
Start Verification
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<small class="text-muted">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-shield-check me-1" viewBox="0 0 16 16">
|
||||
<path d="M5.338 1.59a61.44 61.44 0 0 0-2.837.856.481.481 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.725 10.725 0 0 0 2.287 2.233c.346.244.652.42.893.533.12.057.218.095.293.118a.55.55 0 0 0 .101.025.615.615 0 0 0 .1-.025c.076-.023.174-.061.294-.118.24-.113.547-.29.893-.533a10.726 10.726 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.775 11.775 0 0 1-2.517 2.453 7.159 7.159 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7.158 7.158 0 0 1-1.048-.625 11.777 11.777 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 62.456 62.456 0 0 1 5.072.56z"/>
|
||||
<path d="M10.854 5.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7.5 7.793l2.646-2.647a.5.5 0 0 1 .708 0z"/>
|
||||
</svg>
|
||||
Your files are processed securely and stored encrypted
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.upload-area {
|
||||
border: 2px dashed #dee2e6;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.upload-area:hover,
|
||||
.upload-area.dragging {
|
||||
border-color: var(--bs-primary);
|
||||
background-color: rgba(var(--bs-primary-rgb), 0.05);
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
private IBrowserFile? _selectedFile;
|
||||
private bool _isUploading;
|
||||
private bool _isDragging;
|
||||
private int _uploadProgress;
|
||||
private string? _errorMessage;
|
||||
|
||||
private const long MaxFileSize = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
private void HandleDragEnter()
|
||||
{
|
||||
_isDragging = true;
|
||||
}
|
||||
|
||||
private void HandleDragLeave()
|
||||
{
|
||||
_isDragging = false;
|
||||
}
|
||||
|
||||
private void HandleDrop()
|
||||
{
|
||||
_isDragging = false;
|
||||
}
|
||||
|
||||
private void HandleFileSelected(InputFileChangeEventArgs e)
|
||||
{
|
||||
_errorMessage = null;
|
||||
var file = e.File;
|
||||
|
||||
if (!IsValidFileType(file.Name))
|
||||
{
|
||||
_errorMessage = "Invalid file type. Please upload a PDF or DOCX file.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.Size > MaxFileSize)
|
||||
{
|
||||
_errorMessage = "File size exceeds 10MB limit.";
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedFile = file;
|
||||
}
|
||||
|
||||
private void ClearFile()
|
||||
{
|
||||
_selectedFile = null;
|
||||
_errorMessage = null;
|
||||
}
|
||||
|
||||
private async Task UploadFile()
|
||||
{
|
||||
if (_selectedFile is null) return;
|
||||
|
||||
try
|
||||
{
|
||||
_isUploading = true;
|
||||
_uploadProgress = 0;
|
||||
_errorMessage = null;
|
||||
|
||||
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||||
var userIdClaim = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
|
||||
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
|
||||
{
|
||||
_errorMessage = "Unable to identify user. Please log in again.";
|
||||
_isUploading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Simulate progress for better UX
|
||||
var progressTask = SimulateProgress();
|
||||
|
||||
await using var stream = _selectedFile.OpenReadStream(MaxFileSize);
|
||||
using var memoryStream = new MemoryStream();
|
||||
await stream.CopyToAsync(memoryStream);
|
||||
memoryStream.Position = 0;
|
||||
|
||||
var checkId = await CVCheckService.CreateCheckAsync(userId, memoryStream, _selectedFile.Name);
|
||||
|
||||
_uploadProgress = 100;
|
||||
await Task.Delay(500); // Brief pause to show completion
|
||||
|
||||
NavigationManager.NavigateTo($"/report/{checkId}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_errorMessage = $"An error occurred while uploading: {ex.Message}";
|
||||
_isUploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SimulateProgress()
|
||||
{
|
||||
while (_uploadProgress < 90 && _isUploading)
|
||||
{
|
||||
await Task.Delay(200);
|
||||
_uploadProgress += 10;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsValidFileType(string fileName)
|
||||
{
|
||||
var extension = Path.GetExtension(fileName).ToLowerInvariant();
|
||||
return extension is ".pdf" or ".docx";
|
||||
}
|
||||
|
||||
private static string FormatFileSize(long bytes)
|
||||
{
|
||||
string[] sizes = ["B", "KB", "MB", "GB"];
|
||||
int order = 0;
|
||||
double size = bytes;
|
||||
|
||||
while (size >= 1024 && order < sizes.Length - 1)
|
||||
{
|
||||
order++;
|
||||
size /= 1024;
|
||||
}
|
||||
|
||||
return $"{size:0.##} {sizes[order]}";
|
||||
}
|
||||
}
|
||||
283
src/TrueCV.Web/Components/Pages/Dashboard.razor
Normal file
283
src/TrueCV.Web/Components/Pages/Dashboard.razor
Normal file
@@ -0,0 +1,283 @@
|
||||
@page "/dashboard"
|
||||
@attribute [Authorize]
|
||||
@rendermode InteractiveServer
|
||||
|
||||
@inject ICVCheckService CVCheckService
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||
|
||||
<PageTitle>Dashboard - TrueCV</PageTitle>
|
||||
|
||||
<div class="container py-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="fw-bold mb-1">Dashboard</h1>
|
||||
<p class="text-muted mb-0">View and manage your CV verification checks</p>
|
||||
</div>
|
||||
<a href="/check" class="btn btn-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-lg me-1" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2Z"/>
|
||||
</svg>
|
||||
New Check
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if (_isLoading)
|
||||
{
|
||||
<div class="text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="mt-3 text-muted">Loading your checks...</p>
|
||||
</div>
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(_errorMessage))
|
||||
{
|
||||
<div class="alert alert-danger" role="alert">
|
||||
@_errorMessage
|
||||
</div>
|
||||
}
|
||||
else if (_checks.Count == 0)
|
||||
{
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body text-center py-5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor" class="bi bi-file-earmark-text text-muted mb-3" viewBox="0 0 16 16">
|
||||
<path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"/>
|
||||
<path d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5L9.5 0zm0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
|
||||
</svg>
|
||||
<h4>No CV Checks Yet</h4>
|
||||
<p class="text-muted mb-4">Start by uploading your first CV for verification</p>
|
||||
<a href="/check" class="btn btn-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-upload me-1" viewBox="0 0 16 16">
|
||||
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
||||
<path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708l3-3z"/>
|
||||
</svg>
|
||||
Upload CV
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Stats Cards -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="bg-primary bg-opacity-10 rounded-circle p-3 me-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-file-earmark-check text-primary" viewBox="0 0 16 16">
|
||||
<path d="M10.854 7.854a.5.5 0 0 0-.708-.708L7.5 9.793 6.354 8.646a.5.5 0 1 0-.708.708l1.5 1.5a.5.5 0 0 0 .708 0l3-3z"/>
|
||||
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="mb-0">@_checks.Count</h3>
|
||||
<small class="text-muted">Total Checks</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="bg-success bg-opacity-10 rounded-circle p-3 me-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-check-circle text-success" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="mb-0">@_checks.Count(c => c.Status == "Completed")</h3>
|
||||
<small class="text-muted">Completed</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="bg-warning bg-opacity-10 rounded-circle p-3 me-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-hourglass-split text-warning" viewBox="0 0 16 16">
|
||||
<path d="M2.5 15a.5.5 0 1 1 0-1h1v-1a4.5 4.5 0 0 1 2.557-4.06c.29-.139.443-.377.443-.59v-.7c0-.213-.154-.451-.443-.59A4.5 4.5 0 0 1 3.5 3V2h-1a.5.5 0 0 1 0-1h11a.5.5 0 0 1 0 1h-1v1a4.5 4.5 0 0 1-2.557 4.06c-.29.139-.443.377-.443.59v.7c0 .213.154.451.443.59A4.5 4.5 0 0 1 12.5 13v1h1a.5.5 0 0 1 0 1h-11zm2-13v1c0 .537.12 1.045.337 1.5h6.326c.216-.455.337-.963.337-1.5V2h-7zm3 6.35c0 .701-.478 1.236-1.011 1.492A3.5 3.5 0 0 0 4.5 13s.866-1.299 3-1.48V8.35zm1 0v3.17c2.134.181 3 1.48 3 1.48a3.5 3.5 0 0 0-1.989-3.158C8.978 9.586 8.5 9.052 8.5 8.351z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="mb-0">@_checks.Count(c => c.Status is "Pending" or "Processing")</h3>
|
||||
<small class="text-muted">In Progress</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Checks List -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="mb-0 fw-bold">Recent CV Checks</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>File Name</th>
|
||||
<th>Date</th>
|
||||
<th class="text-center">Status</th>
|
||||
<th class="text-center">Score</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var check in _checks)
|
||||
{
|
||||
<tr class="@(check.Status == "Completed" ? "cursor-pointer" : "")"
|
||||
@onclick="() => ViewReport(check)">
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-file-earmark-text text-primary me-2" viewBox="0 0 16 16">
|
||||
<path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"/>
|
||||
<path d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5L9.5 0zm0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
|
||||
</svg>
|
||||
<span class="fw-medium">@check.OriginalFileName</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-muted">@check.CreatedAt.ToString("dd MMM yyyy HH:mm")</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
@switch (check.Status)
|
||||
{
|
||||
case "Completed":
|
||||
<span class="badge bg-success">Completed</span>
|
||||
break;
|
||||
case "Processing":
|
||||
<span class="badge bg-primary">
|
||||
<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true" style="width: 0.7rem; height: 0.7rem;"></span>
|
||||
Processing
|
||||
</span>
|
||||
break;
|
||||
case "Pending":
|
||||
<span class="badge bg-secondary">Pending</span>
|
||||
break;
|
||||
case "Failed":
|
||||
<span class="badge bg-danger">Failed</span>
|
||||
break;
|
||||
default:
|
||||
<span class="badge bg-secondary">@check.Status</span>
|
||||
break;
|
||||
}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
@if (check.VeracityScore.HasValue)
|
||||
{
|
||||
<span class="badge @GetScoreBadgeClass(check.VeracityScore.Value) fs-6">
|
||||
@check.VeracityScore
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">-</span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
@if (check.Status == "Completed")
|
||||
{
|
||||
<a href="/report/@check.Id" class="btn btn-sm btn-outline-primary" @onclick:stopPropagation="true">
|
||||
View Report
|
||||
</a>
|
||||
}
|
||||
else if (check.Status is "Pending" or "Processing")
|
||||
{
|
||||
<button class="btn btn-sm btn-outline-secondary" disabled>
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a href="/check" class="btn btn-sm btn-outline-warning">
|
||||
Retry
|
||||
</a>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cursor-pointer:hover {
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
private List<CVCheckDto> _checks = [];
|
||||
private bool _isLoading = true;
|
||||
private string? _errorMessage;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadChecks();
|
||||
}
|
||||
|
||||
private async Task LoadChecks()
|
||||
{
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||||
var userIdClaim = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
|
||||
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
|
||||
{
|
||||
_errorMessage = "Unable to identify user. Please log in again.";
|
||||
return;
|
||||
}
|
||||
|
||||
_checks = await CVCheckService.GetUserChecksAsync(userId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_errorMessage = $"An error occurred while loading checks: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ViewReport(CVCheckDto check)
|
||||
{
|
||||
if (check.Status == "Completed")
|
||||
{
|
||||
NavigationManager.NavigateTo($"/report/{check.Id}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetScoreBadgeClass(int score)
|
||||
{
|
||||
return score switch
|
||||
{
|
||||
> 70 => "bg-success",
|
||||
>= 50 => "bg-warning text-dark",
|
||||
_ => "bg-danger"
|
||||
};
|
||||
}
|
||||
}
|
||||
36
src/TrueCV.Web/Components/Pages/Error.razor
Normal file
36
src/TrueCV.Web/Components/Pages/Error.razor
Normal file
@@ -0,0 +1,36 @@
|
||||
@page "/Error"
|
||||
@using System.Diagnostics
|
||||
|
||||
<PageTitle>Error</PageTitle>
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
|
||||
@code{
|
||||
[CascadingParameter]
|
||||
private HttpContext? HttpContext { get; set; }
|
||||
|
||||
private string? RequestId { get; set; }
|
||||
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
protected override void OnInitialized() =>
|
||||
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
|
||||
}
|
||||
146
src/TrueCV.Web/Components/Pages/Home.razor
Normal file
146
src/TrueCV.Web/Components/Pages/Home.razor
Normal file
@@ -0,0 +1,146 @@
|
||||
@page "/"
|
||||
|
||||
<PageTitle>TrueCV - Verify CVs with Confidence</PageTitle>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="bg-primary text-white py-5">
|
||||
<div class="container">
|
||||
<div class="row align-items-center py-5">
|
||||
<div class="col-lg-6">
|
||||
<h1 class="display-4 fw-bold mb-4">Verify CVs with Confidence</h1>
|
||||
<p class="lead mb-4">
|
||||
TrueCV uses AI-powered analysis and official company records to verify employment history,
|
||||
detect timeline inconsistencies, and flag potential issues in candidate CVs.
|
||||
</p>
|
||||
<div class="d-flex gap-3">
|
||||
<a href="/check" class="btn btn-light btn-lg px-4">
|
||||
Start Verification
|
||||
</a>
|
||||
<a href="#features" class="btn btn-outline-light btn-lg px-4">
|
||||
Learn More
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 text-center mt-4 mt-lg-0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="300" fill="currentColor" class="opacity-25" viewBox="0 0 16 16">
|
||||
<path d="M10.067.87a2.89 2.89 0 0 0-4.134 0l-.622.638-.89-.011a2.89 2.89 0 0 0-2.924 2.924l.01.89-.636.622a2.89 2.89 0 0 0 0 4.134l.637.622-.011.89a2.89 2.89 0 0 0 2.924 2.924l.89-.01.622.636a2.89 2.89 0 0 0 4.134 0l.622-.637.89.011a2.89 2.89 0 0 0 2.924-2.924l-.01-.89.636-.622a2.89 2.89 0 0 0 0-4.134l-.637-.622.011-.89a2.89 2.89 0 0 0-2.924-2.924l-.89.01-.622-.636zm.287 5.984-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7 8.793l2.646-2.647a.5.5 0 0 1 .708.708z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section id="features" class="py-5 bg-light">
|
||||
<div class="container">
|
||||
<div class="text-center mb-5">
|
||||
<h2 class="fw-bold">How TrueCV Works</h2>
|
||||
<p class="text-muted lead">Comprehensive CV verification in three key areas</p>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<!-- Employment Verification -->
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-0 shadow-sm">
|
||||
<div class="card-body text-center p-4">
|
||||
<div class="bg-primary bg-opacity-10 rounded-circle d-inline-flex p-3 mb-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-building-check text-primary" viewBox="0 0 16 16">
|
||||
<path d="M12.5 16a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Zm1.679-4.493-1.335 2.226a.75.75 0 0 1-1.174.144l-.774-.773a.5.5 0 0 1 .708-.708l.547.548 1.17-1.951a.5.5 0 1 1 .858.514Z"/>
|
||||
<path d="M2 1a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6.5a.5.5 0 0 1-1 0V1H3v14h3v-2.5a.5.5 0 0 1 .5-.5H8v4H3a1 1 0 0 1-1-1V1Z"/>
|
||||
<path d="M4.5 2a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1Zm3 0a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1Zm3 0a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1Zm-6 3a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1Zm3 0a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1Zm3 0a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1Zm-6 3a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1Zm3 0a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h4 class="card-title fw-bold">Employment Verification</h4>
|
||||
<p class="card-text text-muted">
|
||||
Cross-reference claimed employers with official Companies House records to verify
|
||||
company existence and match accuracy.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timeline Analysis -->
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-0 shadow-sm">
|
||||
<div class="card-body text-center p-4">
|
||||
<div class="bg-primary bg-opacity-10 rounded-circle d-inline-flex p-3 mb-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-calendar-range text-primary" viewBox="0 0 16 16">
|
||||
<path d="M9 7a1 1 0 0 1 1-1h5v2h-5a1 1 0 0 1-1-1zM1 9h4a1 1 0 0 1 0 2H1V9z"/>
|
||||
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h4 class="card-title fw-bold">Timeline Analysis</h4>
|
||||
<p class="card-text text-muted">
|
||||
Detect unexplained employment gaps and overlapping job periods that may indicate
|
||||
inconsistencies in the candidate's work history.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI-Powered Parsing -->
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-0 shadow-sm">
|
||||
<div class="card-body text-center p-4">
|
||||
<div class="bg-primary bg-opacity-10 rounded-circle d-inline-flex p-3 mb-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-cpu text-primary" viewBox="0 0 16 16">
|
||||
<path d="M5 0a.5.5 0 0 1 .5.5V2h1V.5a.5.5 0 0 1 1 0V2h1V.5a.5.5 0 0 1 1 0V2h1V.5a.5.5 0 0 1 1 0V2A2.5 2.5 0 0 1 14 4.5h1.5a.5.5 0 0 1 0 1H14v1h1.5a.5.5 0 0 1 0 1H14v1h1.5a.5.5 0 0 1 0 1H14v1h1.5a.5.5 0 0 1 0 1H14a2.5 2.5 0 0 1-2.5 2.5v1.5a.5.5 0 0 1-1 0V14h-1v1.5a.5.5 0 0 1-1 0V14h-1v1.5a.5.5 0 0 1-1 0V14h-1v1.5a.5.5 0 0 1-1 0V14A2.5 2.5 0 0 1 2 11.5H.5a.5.5 0 0 1 0-1H2v-1H.5a.5.5 0 0 1 0-1H2v-1H.5a.5.5 0 0 1 0-1H2v-1H.5a.5.5 0 0 1 0-1H2A2.5 2.5 0 0 1 4.5 2V.5A.5.5 0 0 1 5 0zm-.5 3A1.5 1.5 0 0 0 3 4.5v7A1.5 1.5 0 0 0 4.5 13h7a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 11.5 3h-7zM5 6.5A1.5 1.5 0 0 1 6.5 5h3A1.5 1.5 0 0 1 11 6.5v3A1.5 1.5 0 0 1 9.5 11h-3A1.5 1.5 0 0 1 5 9.5v-3zM6.5 6a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h4 class="card-title fw-bold">AI-Powered Parsing</h4>
|
||||
<p class="card-text text-muted">
|
||||
Advanced AI extracts and structures CV data from PDF and DOCX files, ensuring
|
||||
accurate information capture for analysis.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- How It Works Section -->
|
||||
<section class="py-5">
|
||||
<div class="container">
|
||||
<div class="text-center mb-5">
|
||||
<h2 class="fw-bold">Get Started in Minutes</h2>
|
||||
<p class="text-muted lead">Simple three-step verification process</p>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 justify-content-center">
|
||||
<div class="col-md-4">
|
||||
<div class="text-center">
|
||||
<div class="bg-primary text-white rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 60px; height: 60px;">
|
||||
<span class="fw-bold fs-4">1</span>
|
||||
</div>
|
||||
<h5 class="fw-bold">Upload CV</h5>
|
||||
<p class="text-muted">Upload the candidate's CV in PDF or DOCX format</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="text-center">
|
||||
<div class="bg-primary text-white rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 60px; height: 60px;">
|
||||
<span class="fw-bold fs-4">2</span>
|
||||
</div>
|
||||
<h5 class="fw-bold">AI Analysis</h5>
|
||||
<p class="text-muted">Our AI parses the CV and verifies against official records</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="text-center">
|
||||
<div class="bg-primary text-white rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 60px; height: 60px;">
|
||||
<span class="fw-bold fs-4">3</span>
|
||||
</div>
|
||||
<h5 class="fw-bold">Get Report</h5>
|
||||
<p class="text-muted">Receive a detailed veracity report with actionable insights</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-5">
|
||||
<a href="/check" class="btn btn-primary btn-lg px-5">
|
||||
Start Your First Check
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
549
src/TrueCV.Web/Components/Pages/Report.razor
Normal file
549
src/TrueCV.Web/Components/Pages/Report.razor
Normal file
@@ -0,0 +1,549 @@
|
||||
@page "/report/{Id:guid}"
|
||||
@attribute [Authorize]
|
||||
@rendermode InteractiveServer
|
||||
|
||||
@inject ICVCheckService CVCheckService
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||
|
||||
<PageTitle>Verification Report - TrueCV</PageTitle>
|
||||
|
||||
<div class="container py-5">
|
||||
@if (_isLoading)
|
||||
{
|
||||
<div class="text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status" style="width: 3rem; height: 3rem;">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="mt-3 text-muted">Loading report...</p>
|
||||
</div>
|
||||
}
|
||||
else if (_errorMessage is not null)
|
||||
{
|
||||
<div class="alert alert-danger" role="alert">
|
||||
@_errorMessage
|
||||
</div>
|
||||
<a href="/dashboard" class="btn btn-primary">Back to Dashboard</a>
|
||||
}
|
||||
else if (_check is not null && _check.Status != "Completed")
|
||||
{
|
||||
<div class="text-center py-5">
|
||||
<div class="card border-0 shadow-sm mx-auto" style="max-width: 500px;">
|
||||
<div class="card-body p-5">
|
||||
@if (_check.Status == "Processing")
|
||||
{
|
||||
<div class="spinner-border text-primary mb-3" role="status" style="width: 3rem; height: 3rem;">
|
||||
<span class="visually-hidden">Processing...</span>
|
||||
</div>
|
||||
<h4 class="mb-2">Processing Your CV</h4>
|
||||
<p class="text-muted mb-4">Our AI is analyzing the document. This usually takes 1-2 minutes.</p>
|
||||
<div class="progress" style="height: 8px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
role="progressbar"
|
||||
style="width: 60%">
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (_check.Status == "Pending")
|
||||
{
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="bi bi-hourglass-split text-warning mb-3" viewBox="0 0 16 16">
|
||||
<path d="M2.5 15a.5.5 0 1 1 0-1h1v-1a4.5 4.5 0 0 1 2.557-4.06c.29-.139.443-.377.443-.59v-.7c0-.213-.154-.451-.443-.59A4.5 4.5 0 0 1 3.5 3V2h-1a.5.5 0 0 1 0-1h11a.5.5 0 0 1 0 1h-1v1a4.5 4.5 0 0 1-2.557 4.06c-.29.139-.443.377-.443.59v.7c0 .213.154.451.443.59A4.5 4.5 0 0 1 12.5 13v1h1a.5.5 0 0 1 0 1h-11zm2-13v1c0 .537.12 1.045.337 1.5h6.326c.216-.455.337-.963.337-1.5V2h-7zm3 6.35c0 .701-.478 1.236-1.011 1.492A3.5 3.5 0 0 0 4.5 13s.866-1.299 3-1.48V8.35zm1 0v3.17c2.134.181 3 1.48 3 1.48a3.5 3.5 0 0 0-1.989-3.158C8.978 9.586 8.5 9.052 8.5 8.351z"/>
|
||||
</svg>
|
||||
<h4 class="mb-2">Queued for Processing</h4>
|
||||
<p class="text-muted">Your CV is in the queue and will be processed shortly.</p>
|
||||
}
|
||||
else if (_check.Status == "Failed")
|
||||
{
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="bi bi-exclamation-triangle text-danger mb-3" viewBox="0 0 16 16">
|
||||
<path d="M7.938 2.016A.13.13 0 0 1 8.002 2a.13.13 0 0 1 .063.016.146.146 0 0 1 .054.057l6.857 11.667c.036.06.035.124.002.183a.163.163 0 0 1-.054.06.116.116 0 0 1-.066.017H1.146a.115.115 0 0 1-.066-.017.163.163 0 0 1-.054-.06.176.176 0 0 1 .002-.183L7.884 2.073a.147.147 0 0 1 .054-.057zm1.044-.45a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566z"/>
|
||||
<path d="M7.002 12a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 5.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995z"/>
|
||||
</svg>
|
||||
<h4 class="mb-2">Processing Failed</h4>
|
||||
<p class="text-muted">We encountered an error processing your CV. Please try uploading again.</p>
|
||||
}
|
||||
|
||||
<p class="text-muted small mt-4">
|
||||
File: @_check.OriginalFileName<br />
|
||||
Uploaded: @_check.CreatedAt.ToString("dd MMM yyyy HH:mm")
|
||||
</p>
|
||||
|
||||
<button class="btn btn-outline-primary mt-3" @onclick="RefreshStatus">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise me-1" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
|
||||
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
|
||||
</svg>
|
||||
Refresh Status
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (_report is not null && _check is not null)
|
||||
{
|
||||
<!-- Report Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/dashboard">Dashboard</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Report</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<h1 class="fw-bold">Verification Report</h1>
|
||||
<p class="text-muted">
|
||||
@_check.OriginalFileName | Generated @_report.GeneratedAt.ToString("dd MMM yyyy HH:mm")
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-outline-primary" @onclick="DownloadReport">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-download me-1" viewBox="0 0 16 16">
|
||||
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
||||
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
|
||||
</svg>
|
||||
Download Report
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Score Card -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-4 text-center border-end">
|
||||
<div class="score-circle @GetScoreColorClass(_report.OverallScore) mx-auto mb-2">
|
||||
<span class="score-value">@_report.OverallScore</span>
|
||||
</div>
|
||||
<h5 class="mb-0">@_report.ScoreLabel</h5>
|
||||
<small class="text-muted">Veracity Score</small>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="row text-center">
|
||||
<div class="col-4">
|
||||
<h3 class="mb-0 text-primary">@_report.EmploymentVerifications.Count</h3>
|
||||
<small class="text-muted">Employers Checked</small>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<h3 class="mb-0 text-warning">@_report.TimelineAnalysis.TotalGapMonths</h3>
|
||||
<small class="text-muted">Months of Gaps</small>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<h3 class="mb-0 text-danger">@_report.Flags.Count</h3>
|
||||
<small class="text-muted">Flags Raised</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Employment Verification -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="mb-0 fw-bold">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-building me-2" viewBox="0 0 16 16">
|
||||
<path d="M4 2.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm3 0a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1Zm3.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1ZM4 5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1ZM7.5 5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1Zm2.5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1ZM4.5 8a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1Zm2.5.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1ZM10.5 8a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1Z"/>
|
||||
<path d="M2 1a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V1Zm11 0H3v14h3v-2.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5V15h3V1Z"/>
|
||||
</svg>
|
||||
Employment Verification
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Claimed Employer</th>
|
||||
<th>Period</th>
|
||||
<th>Matched Company</th>
|
||||
<th class="text-center">Match Score</th>
|
||||
<th class="text-center">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var verification in _report.EmploymentVerifications)
|
||||
{
|
||||
<tr>
|
||||
<td class="fw-medium">@verification.ClaimedCompany</td>
|
||||
<td>
|
||||
@if (verification.ClaimedStartDate.HasValue)
|
||||
{
|
||||
<span>@verification.ClaimedStartDate.Value.ToString("MMM yyyy")</span>
|
||||
<span> - </span>
|
||||
@if (verification.ClaimedEndDate.HasValue)
|
||||
{
|
||||
<span>@verification.ClaimedEndDate.Value.ToString("MMM yyyy")</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Present</span>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">Not specified</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (!string.IsNullOrEmpty(verification.MatchedCompanyName))
|
||||
{
|
||||
<span>@verification.MatchedCompanyName</span>
|
||||
@if (!string.IsNullOrEmpty(verification.MatchedCompanyNumber))
|
||||
{
|
||||
<br /><small class="text-muted">@verification.MatchedCompanyNumber</small>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">No match found</span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge @GetMatchScoreBadgeClass(verification.MatchScore)">
|
||||
@verification.MatchScore%
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
@if (verification.IsVerified)
|
||||
{
|
||||
<span class="badge bg-success">Verified</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-warning text-dark">Unverified</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
@if (!string.IsNullOrEmpty(verification.VerificationNotes))
|
||||
{
|
||||
<tr class="table-light">
|
||||
<td colspan="5" class="small text-muted py-1 ps-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="bi bi-info-circle me-1" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/>
|
||||
</svg>
|
||||
@verification.VerificationNotes
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timeline Analysis -->
|
||||
<div class="row mb-4">
|
||||
<!-- Gaps -->
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="mb-0 fw-bold">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-clock-history me-2 text-warning" viewBox="0 0 16 16">
|
||||
<path d="M8.515 1.019A7 7 0 0 0 8 1V0a8 8 0 0 1 .589.022l-.074.997zm2.004.45a7.003 7.003 0 0 0-.985-.299l.219-.976c.383.086.76.2 1.126.342l-.36.933zm1.37.71a7.01 7.01 0 0 0-.439-.27l.493-.87a8.025 8.025 0 0 1 .979.654l-.615.789a6.996 6.996 0 0 0-.418-.302zm1.834 1.79a6.99 6.99 0 0 0-.653-.796l.724-.69c.27.285.52.59.747.91l-.818.576zm.744 1.352a7.08 7.08 0 0 0-.214-.468l.893-.45a7.976 7.976 0 0 1 .45 1.088l-.95.313a7.023 7.023 0 0 0-.179-.483zm.53 2.507a6.991 6.991 0 0 0-.1-1.025l.985-.17c.067.386.106.778.116 1.17l-1 .025zm-.131 1.538c.033-.17.06-.339.081-.51l.993.123a7.957 7.957 0 0 1-.23 1.155l-.964-.267c.046-.165.086-.332.12-.501zm-.952 2.379c.184-.29.346-.594.486-.908l.914.405c-.16.36-.345.706-.555 1.038l-.845-.535zm-.964 1.205c.122-.122.239-.248.35-.378l.758.653a8.073 8.073 0 0 1-.401.432l-.707-.707z"/>
|
||||
<path d="M8 1a7 7 0 1 0 4.95 11.95l.707.707A8.001 8.001 0 1 1 8 0v1z"/>
|
||||
<path d="M7.5 3a.5.5 0 0 1 .5.5v5.21l3.248 1.856a.5.5 0 0 1-.496.868l-3.5-2A.5.5 0 0 1 7 9V3.5a.5.5 0 0 1 .5-.5z"/>
|
||||
</svg>
|
||||
Employment Gaps
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (_report.TimelineAnalysis.Gaps.Count > 0)
|
||||
{
|
||||
<ul class="list-group list-group-flush">
|
||||
@foreach (var gap in _report.TimelineAnalysis.Gaps)
|
||||
{
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
<span>
|
||||
@gap.StartDate.ToString("MMM yyyy") - @gap.EndDate.ToString("MMM yyyy")
|
||||
</span>
|
||||
<span class="badge bg-warning text-dark rounded-pill">@gap.Months months</span>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<div class="mt-3 p-2 bg-light rounded">
|
||||
<small class="text-muted">Total gap time: <strong>@_report.TimelineAnalysis.TotalGapMonths months</strong></small>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-center py-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-check-circle text-success mb-2" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z"/>
|
||||
</svg>
|
||||
<p class="mb-0 text-muted">No significant gaps detected</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overlaps -->
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="mb-0 fw-bold">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-intersect me-2 text-danger" viewBox="0 0 16 16">
|
||||
<path d="M0 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v2h2a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2H2a2 2 0 0 1-2-2V2zm5 10v2a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1h-2v5a2 2 0 0 1-2 2H5zm6-8V2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2V6a2 2 0 0 1 2-2h5z"/>
|
||||
</svg>
|
||||
Timeline Overlaps
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (_report.TimelineAnalysis.Overlaps.Count > 0)
|
||||
{
|
||||
<ul class="list-group list-group-flush">
|
||||
@foreach (var overlap in _report.TimelineAnalysis.Overlaps)
|
||||
{
|
||||
<li class="list-group-item px-0">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<small class="text-muted">@overlap.Company1</small> &
|
||||
<small class="text-muted">@overlap.Company2</small>
|
||||
</div>
|
||||
<span class="badge bg-danger rounded-pill">@overlap.Months months</span>
|
||||
</div>
|
||||
<small class="text-muted">
|
||||
@overlap.OverlapStart.ToString("MMM yyyy") - @overlap.OverlapEnd.ToString("MMM yyyy")
|
||||
</small>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<div class="mt-3 p-2 bg-light rounded">
|
||||
<small class="text-muted">Total overlap time: <strong>@_report.TimelineAnalysis.TotalOverlapMonths months</strong></small>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-center py-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-check-circle text-success mb-2" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z"/>
|
||||
</svg>
|
||||
<p class="mb-0 text-muted">No overlapping positions detected</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Flags -->
|
||||
@if (_report.Flags.Count > 0)
|
||||
{
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="mb-0 fw-bold">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-flag me-2 text-danger" viewBox="0 0 16 16">
|
||||
<path d="M14.778.085A.5.5 0 0 1 15 .5V8a.5.5 0 0 1-.314.464L14.5 8l.186.464-.003.001-.006.003-.023.009a12.435 12.435 0 0 1-.397.15c-.264.095-.631.223-1.047.35-.816.252-1.879.523-2.71.523-.847 0-1.548-.28-2.158-.525l-.028-.01C7.68 8.71 7.14 8.5 6.5 8.5c-.7 0-1.638.23-2.437.477A19.626 19.626 0 0 0 3 9.342V15.5a.5.5 0 0 1-1 0V.5a.5.5 0 0 1 1 0v.282c.226-.079.496-.17.79-.26C4.606.272 5.67 0 6.5 0c.84 0 1.524.277 2.121.519l.043.018C9.286.788 9.828 1 10.5 1c.7 0 1.638-.23 2.437-.477a19.587 19.587 0 0 0 1.349-.476l.019-.007.004-.002h.001"/>
|
||||
</svg>
|
||||
Flags Raised
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@{
|
||||
var criticalFlags = _report.Flags.Where(f => f.Severity == "Critical").ToList();
|
||||
var warningFlags = _report.Flags.Where(f => f.Severity == "Warning").ToList();
|
||||
var infoFlags = _report.Flags.Where(f => f.Severity == "Info").ToList();
|
||||
}
|
||||
|
||||
@if (criticalFlags.Count > 0)
|
||||
{
|
||||
<h6 class="text-danger fw-bold mb-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-exclamation-octagon me-1" viewBox="0 0 16 16">
|
||||
<path d="M4.54.146A.5.5 0 0 1 4.893 0h6.214a.5.5 0 0 1 .353.146l4.394 4.394a.5.5 0 0 1 .146.353v6.214a.5.5 0 0 1-.146.353l-4.394 4.394a.5.5 0 0 1-.353.146H4.893a.5.5 0 0 1-.353-.146L.146 11.46A.5.5 0 0 1 0 11.107V4.893a.5.5 0 0 1 .146-.353L4.54.146zM5.1 1 1 5.1v5.8L5.1 15h5.8l4.1-4.1V5.1L10.9 1H5.1z"/>
|
||||
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
|
||||
</svg>
|
||||
Critical Issues
|
||||
</h6>
|
||||
@foreach (var flag in criticalFlags)
|
||||
{
|
||||
<div class="alert alert-danger mb-2">
|
||||
<strong>@flag.Title</strong>
|
||||
<span class="badge bg-danger ms-2">-@flag.ScoreImpact pts</span>
|
||||
<p class="mb-0 mt-1 small">@flag.Description</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (warningFlags.Count > 0)
|
||||
{
|
||||
<h6 class="text-warning fw-bold mb-3 @(criticalFlags.Count > 0 ? "mt-4" : "")">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-exclamation-triangle me-1" viewBox="0 0 16 16">
|
||||
<path d="M7.938 2.016A.13.13 0 0 1 8.002 2a.13.13 0 0 1 .063.016.146.146 0 0 1 .054.057l6.857 11.667c.036.06.035.124.002.183a.163.163 0 0 1-.054.06.116.116 0 0 1-.066.017H1.146a.115.115 0 0 1-.066-.017.163.163 0 0 1-.054-.06.176.176 0 0 1 .002-.183L7.884 2.073a.147.147 0 0 1 .054-.057zm1.044-.45a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566z"/>
|
||||
<path d="M7.002 12a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 5.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995z"/>
|
||||
</svg>
|
||||
Warnings
|
||||
</h6>
|
||||
@foreach (var flag in warningFlags)
|
||||
{
|
||||
<div class="alert alert-warning mb-2">
|
||||
<strong>@flag.Title</strong>
|
||||
<span class="badge bg-warning text-dark ms-2">-@flag.ScoreImpact pts</span>
|
||||
<p class="mb-0 mt-1 small">@flag.Description</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (infoFlags.Count > 0)
|
||||
{
|
||||
<h6 class="text-info fw-bold mb-3 @(criticalFlags.Count > 0 || warningFlags.Count > 0 ? "mt-4" : "")">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-info-circle me-1" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/>
|
||||
</svg>
|
||||
Information
|
||||
</h6>
|
||||
@foreach (var flag in infoFlags)
|
||||
{
|
||||
<div class="alert alert-info mb-2">
|
||||
<strong>@flag.Title</strong>
|
||||
<span class="badge bg-info text-dark ms-2">-@flag.ScoreImpact pts</span>
|
||||
<p class="mb-0 mt-1 small">@flag.Description</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.score-circle {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 8px solid;
|
||||
}
|
||||
|
||||
.score-circle.score-high {
|
||||
border-color: #198754;
|
||||
background-color: rgba(25, 135, 84, 0.1);
|
||||
}
|
||||
|
||||
.score-circle.score-medium {
|
||||
border-color: #ffc107;
|
||||
background-color: rgba(255, 193, 7, 0.1);
|
||||
}
|
||||
|
||||
.score-circle.score-low {
|
||||
border-color: #dc3545;
|
||||
background-color: rgba(220, 53, 69, 0.1);
|
||||
}
|
||||
|
||||
.score-value {
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.score-high .score-value {
|
||||
color: #198754;
|
||||
}
|
||||
|
||||
.score-medium .score-value {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.score-low .score-value {
|
||||
color: #dc3545;
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
private CVCheckDto? _check;
|
||||
private VeracityReport? _report;
|
||||
private bool _isLoading = true;
|
||||
private string? _errorMessage;
|
||||
private Guid _userId;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadData();
|
||||
}
|
||||
|
||||
private async Task LoadData()
|
||||
{
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||||
var userIdClaim = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
|
||||
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out _userId))
|
||||
{
|
||||
_errorMessage = "Unable to identify user. Please log in again.";
|
||||
return;
|
||||
}
|
||||
|
||||
_check = await CVCheckService.GetCheckForUserAsync(Id, _userId);
|
||||
|
||||
if (_check is null)
|
||||
{
|
||||
_errorMessage = "Report not found or you don't have access to view it.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (_check.Status == "Completed")
|
||||
{
|
||||
_report = await CVCheckService.GetReportAsync(Id, _userId);
|
||||
|
||||
if (_report is null)
|
||||
{
|
||||
_errorMessage = "Unable to load the report data.";
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_errorMessage = $"An error occurred: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshStatus()
|
||||
{
|
||||
await LoadData();
|
||||
}
|
||||
|
||||
private void DownloadReport()
|
||||
{
|
||||
// TODO: Implement report download functionality
|
||||
}
|
||||
|
||||
private static string GetScoreColorClass(int score)
|
||||
{
|
||||
return score switch
|
||||
{
|
||||
> 70 => "score-high",
|
||||
>= 50 => "score-medium",
|
||||
_ => "score-low"
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetMatchScoreBadgeClass(int score)
|
||||
{
|
||||
return score switch
|
||||
{
|
||||
>= 80 => "bg-success",
|
||||
>= 50 => "bg-warning text-dark",
|
||||
_ => "bg-danger"
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user