- Replace 'diploma mill' language with objective 'unaccredited institution' terminology - Rename DiplomaMills.cs to UnaccreditedInstitutions.cs with neutral language - Update EducationVerificationResult.IsUnaccredited property - Update flag titles to 'Unaccredited Institution' and 'Institution Requires Verification' - Add legal disclaimer to verification report page - Add Privacy Policy page (/privacy) with UK GDPR compliance info - Add Terms of Service page (/terms) with candidate notice requirements - Add footer links to Privacy and Terms pages - Update all tests to use new terminology 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1091 lines
51 KiB
Plaintext
1091 lines
51 KiB
Plaintext
@page "/report/{Id:guid}"
|
||
@attribute [Authorize]
|
||
@rendermode InteractiveServer
|
||
@implements IDisposable
|
||
|
||
@inject ICVCheckService CVCheckService
|
||
@inject NavigationManager NavigationManager
|
||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||
@inject ILogger<Report> Logger
|
||
@inject IJSRuntime JSRuntime
|
||
@inject IAuditService AuditService
|
||
@inject IPdfReportService PdfReportService
|
||
|
||
<PageTitle>Verification Report - RealCV</PageTitle>
|
||
|
||
<div class="container py-5">
|
||
@if (_isLoading)
|
||
{
|
||
<div class="text-center py-5">
|
||
<div class="loading-placeholder">
|
||
<div class="placeholder-glow mb-4">
|
||
<div class="placeholder col-12 rounded-4" style="height: 180px;"></div>
|
||
</div>
|
||
<div class="placeholder-glow mb-3">
|
||
<div class="placeholder col-12 rounded-4" style="height: 280px;"></div>
|
||
</div>
|
||
</div>
|
||
<div class="mt-4">
|
||
<div class="spinner-border text-primary" role="status" style="width: 2.5rem; height: 2.5rem;">
|
||
<span class="visually-hidden">Loading report...</span>
|
||
</div>
|
||
<p class="mt-3 text-muted fw-medium">Loading verification report...</p>
|
||
</div>
|
||
</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 analysing 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="page-header mb-4">
|
||
<nav aria-label="breadcrumb" class="mb-2">
|
||
<ol class="breadcrumb small">
|
||
<li class="breadcrumb-item"><a href="/dashboard" class="text-decoration-none">Dashboard</a></li>
|
||
<li class="breadcrumb-item active text-muted" aria-current="page">Report</li>
|
||
</ol>
|
||
</nav>
|
||
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3">
|
||
<div>
|
||
<h1 class="fw-bold mb-1">Verification Report</h1>
|
||
@if (!string.IsNullOrWhiteSpace(_report.CandidateName))
|
||
{
|
||
<h2 class="h4 text-primary mb-2">@_report.CandidateName</h2>
|
||
}
|
||
<p class="text-muted mb-0">
|
||
<span class="fw-medium text-dark">@_check.OriginalFileName</span>
|
||
<span class="mx-2">|</span>
|
||
Generated @_report.GeneratedAt.ToString("dd MMM yyyy") at @_report.GeneratedAt.ToString("HH:mm")
|
||
</p>
|
||
</div>
|
||
<button class="btn btn-primary" @onclick="DownloadReport" disabled="@(_report is null)">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-download 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 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 PDF
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Score Card -->
|
||
<div class="row mb-4">
|
||
<div class="col-12">
|
||
<div class="card border-0 shadow-sm overflow-hidden">
|
||
<div class="score-header">
|
||
<div class="row align-items-center py-2 px-3">
|
||
<div class="col-md-4 text-center">
|
||
<div class="score-roundel @GetScoreColorClass(_report!.OverallScore)">
|
||
<svg class="score-ring" viewBox="0 0 120 120">
|
||
<circle class="score-ring-bg" cx="60" cy="60" r="54" />
|
||
@{
|
||
var circumference = 339.292;
|
||
var progressLength = circumference * _report.OverallScore / 100;
|
||
var gapLength = circumference - progressLength;
|
||
}
|
||
@if (_report.OverallScore > 0)
|
||
{
|
||
<circle class="score-ring-progress" cx="60" cy="60" r="54"
|
||
stroke-dasharray="@progressLength @gapLength"
|
||
stroke-dashoffset="0" />
|
||
}
|
||
</svg>
|
||
<div class="score-value-container">
|
||
<span class="score-value">@_report.OverallScore</span>
|
||
<span class="score-max">/100</span>
|
||
</div>
|
||
</div>
|
||
<div class="mt-1 text-white truecv-score-label">RealCV Score</div>
|
||
</div>
|
||
<div class="col-md-8">
|
||
<div class="row g-2 text-center text-md-start">
|
||
<div class="col-4">
|
||
<div class="stat-item">
|
||
<div class="stat-icon">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" 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-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>
|
||
</div>
|
||
<h5 class="mb-0 fw-bold text-white">@_report.EmploymentVerifications.Count</h5>
|
||
<small class="stat-label">Employers Checked</small>
|
||
</div>
|
||
</div>
|
||
<div class="col-4">
|
||
<div class="stat-item">
|
||
<div class="stat-icon">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
|
||
<path d="M6.146 7.146a.5.5 0 0 1 .708 0L8 8.293l1.146-1.147a.5.5 0 1 1 .708.708L8.707 9l1.147 1.146a.5.5 0 0 1-.708.708L8 9.707l-1.146 1.147a.5.5 0 0 1-.708-.708L7.293 9 6.146 7.854a.5.5 0 0 1 0-.708z"/>
|
||
<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>
|
||
<h5 class="mb-0 fw-bold text-white">@_report.TimelineAnalysis.TotalGapMonths</h5>
|
||
<small class="stat-label">Gap Months</small>
|
||
</div>
|
||
</div>
|
||
<div class="col-4">
|
||
<div class="stat-item">
|
||
<div class="stat-icon">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" 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>
|
||
</div>
|
||
<h5 class="mb-0 fw-bold text-white">@_report.Flags.Count</h5>
|
||
<small class="stat-label">Flags Raised</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Employment Verification -->
|
||
<div class="card border-0 shadow-sm mb-4">
|
||
<div class="card-header py-3" style="background-color: var(--realcv-bg-surface);">
|
||
<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="employment-list">
|
||
<div class="employment-header">
|
||
<div style="text-align: center;"></div>
|
||
<div>Employer</div>
|
||
<div style="text-align: center;">Period</div>
|
||
<div style="text-align: center;">Match</div>
|
||
<div style="text-align: center;">Pts</div>
|
||
</div>
|
||
@for (int i = 0; i < _report.EmploymentVerifications.Count; i++)
|
||
{
|
||
var verification = _report.EmploymentVerifications[i];
|
||
var index = i;
|
||
var companyPoints = GetPointsForCompany(verification.ClaimedCompany, verification.MatchedCompanyName, index);
|
||
|
||
<div class="employment-row @(verification.IsVerified ? "employment-row-verified" : "employment-row-unverified")">
|
||
<div class="employment-status-icon">
|
||
@if (verification.IsVerified)
|
||
{
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="text-success" viewBox="0 0 16 16">
|
||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.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-.01-1.05z"/>
|
||
</svg>
|
||
}
|
||
else
|
||
{
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="text-warning" viewBox="0 0 16 16">
|
||
<path d="M8.982 1.566a1.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.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
|
||
</svg>
|
||
}
|
||
</div>
|
||
<div class="employment-company">
|
||
<span class="employment-company-name">@verification.ClaimedCompany</span>
|
||
@if (!string.IsNullOrEmpty(verification.VerificationNotes))
|
||
{
|
||
<span class="employment-note-inline">@verification.VerificationNotes</span>
|
||
}
|
||
</div>
|
||
<div class="employment-dates">
|
||
@if (verification.ClaimedStartDate.HasValue)
|
||
{
|
||
<span>@verification.ClaimedStartDate.Value.ToString("MMM yyyy") – @(verification.ClaimedEndDate?.ToString("MMM yyyy") ?? "Present")</span>
|
||
}
|
||
else
|
||
{
|
||
<span class="text-muted">—</span>
|
||
}
|
||
</div>
|
||
<div class="employment-score">
|
||
<span class="badge @GetMatchScoreBadgeClass(verification.MatchScore)">@verification.MatchScore%</span>
|
||
</div>
|
||
<div class="employment-points">
|
||
@if (companyPoints < 0)
|
||
{
|
||
<span class="text-danger fw-medium">@companyPoints</span>
|
||
}
|
||
else if (!verification.IsVerified && !_firstOccurrenceIndices.Contains(index))
|
||
{
|
||
<span class="text-muted" title="Penalty counted on first occurrence">—</span>
|
||
}
|
||
else
|
||
{
|
||
<span class="text-success">0</span>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
</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 py-3" style="background-color: var(--realcv-bg-surface);">
|
||
<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 py-3" style="background-color: var(--realcv-bg-surface);">
|
||
<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 py-3" style="background-color: var(--realcv-bg-surface);">
|
||
<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">
|
||
@{
|
||
// Deduplicate flags and fix any variable-style names
|
||
var uniqueFlags = _report.Flags
|
||
.GroupBy(f => (f.Title, f.Description))
|
||
.Select(g => g.First())
|
||
.Select(f => new {
|
||
f.Severity,
|
||
Title = FormatFlagTitle(f.Title),
|
||
f.Description,
|
||
ScoreImpact = Math.Abs(f.ScoreImpact)
|
||
})
|
||
.ToList();
|
||
var criticalFlags = uniqueFlags.Where(f => f.Severity == "Critical").ToList();
|
||
var warningFlags = uniqueFlags.Where(f => f.Severity == "Warning").ToList();
|
||
var infoFlags = uniqueFlags.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="flag-item flag-critical">
|
||
<div class="d-flex justify-content-between align-items-start">
|
||
<strong class="flag-title">@flag.Title</strong>
|
||
<span class="flag-points bg-danger-subtle text-danger">-@flag.ScoreImpact pts</span>
|
||
</div>
|
||
<p class="flag-description">@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="flag-item flag-warning">
|
||
<div class="d-flex justify-content-between align-items-start">
|
||
<strong class="flag-title">@flag.Title</strong>
|
||
<span class="flag-points bg-warning-subtle text-warning">-@flag.ScoreImpact pts</span>
|
||
</div>
|
||
<p class="flag-description">@flag.Description</p>
|
||
</div>
|
||
}
|
||
}
|
||
|
||
@if (infoFlags.Count > 0)
|
||
{
|
||
<h6 class="text-primary 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="flag-item flag-info">
|
||
<strong class="flag-title">@flag.Title</strong>
|
||
<p class="flag-description">@flag.Description</p>
|
||
</div>
|
||
}
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- Legal Disclaimer -->
|
||
<div class="card border-0 shadow-sm mb-4">
|
||
<div class="card-body">
|
||
<h6 class="text-muted mb-3">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-info-circle me-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="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>
|
||
Important Information
|
||
</h6>
|
||
<div class="small text-muted">
|
||
<p class="mb-2">
|
||
<strong>This report is for informational purposes only.</strong> The verification results are based on
|
||
publicly available data from Companies House and other official sources. This analysis should be
|
||
used as one input among many in your hiring decision-making process.
|
||
</p>
|
||
<p class="mb-2">
|
||
<strong>Limitations:</strong> This automated verification cannot confirm whether a specific individual
|
||
actually worked at a verified company, only that the company exists and was active during the claimed
|
||
employment period. Education verification is based on institutional recognition status only.
|
||
</p>
|
||
<p class="mb-2">
|
||
<strong>Not a substitute for thorough background checks:</strong> We recommend supplementing this
|
||
report with direct reference checks, qualification verification with issuing institutions, and
|
||
other appropriate due diligence measures.
|
||
</p>
|
||
<p class="mb-0">
|
||
<strong>Candidate rights:</strong> Data subjects have the right to request access to, correction of,
|
||
or deletion of their personal data. For enquiries, please contact us via our website.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<style>
|
||
/* Score Header - Blue gradient */
|
||
.score-header {
|
||
background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%);
|
||
color: white;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.score-header::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.06'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* Score Roundel */
|
||
.score-roundel {
|
||
position: relative;
|
||
width: 100px;
|
||
height: 100px;
|
||
margin: 0 auto;
|
||
background: rgba(255, 255, 255, 0.15);
|
||
border-radius: 50%;
|
||
padding: 6px;
|
||
}
|
||
|
||
.score-roundel .score-ring {
|
||
transform: rotate(-90deg);
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.score-roundel .score-ring-bg {
|
||
fill: none;
|
||
stroke-width: 8;
|
||
}
|
||
|
||
.score-roundel .score-ring-progress {
|
||
fill: none;
|
||
stroke-width: 8;
|
||
stroke-linecap: round;
|
||
transform-origin: center;
|
||
transition: stroke-dasharray 0.6s ease;
|
||
}
|
||
|
||
/* Score color variants - colours both the background ring and progress */
|
||
.score-roundel.score-high .score-ring-bg {
|
||
stroke: rgba(255, 255, 255, 0.3);
|
||
}
|
||
.score-roundel.score-high .score-ring-progress {
|
||
stroke: #4ade80;
|
||
}
|
||
|
||
.score-roundel.score-medium .score-ring-bg {
|
||
stroke: rgba(255, 255, 255, 0.3);
|
||
}
|
||
.score-roundel.score-medium .score-ring-progress {
|
||
stroke: #fbbf24;
|
||
}
|
||
|
||
.score-roundel.score-low .score-ring-bg {
|
||
stroke: rgba(255, 255, 255, 0.3);
|
||
}
|
||
|
||
.score-roundel.score-low .score-ring-progress {
|
||
stroke: #f87171;
|
||
}
|
||
|
||
.score-roundel .score-value-container {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
text-align: center;
|
||
}
|
||
|
||
.score-roundel .score-value {
|
||
font-size: 1.75rem;
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
color: white;
|
||
}
|
||
|
||
.score-roundel .score-max {
|
||
font-size: 0.75rem;
|
||
opacity: 0.85;
|
||
color: white;
|
||
}
|
||
|
||
.truecv-score-label {
|
||
font-family: 'Inter', sans-serif;
|
||
font-weight: 700;
|
||
font-size: 0.875rem;
|
||
letter-spacing: 0.025em;
|
||
}
|
||
|
||
/* Stat Items */
|
||
.stat-item {
|
||
padding: 0.5rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
text-align: center;
|
||
}
|
||
|
||
.stat-icon {
|
||
width: 44px;
|
||
height: 44px;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
border-radius: 12px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 0.5rem;
|
||
color: white;
|
||
}
|
||
|
||
.stat-label {
|
||
color: rgba(255, 255, 255, 0.95);
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
/* Flag Items */
|
||
.flag-item {
|
||
border-radius: 12px;
|
||
padding: 1rem 1.25rem;
|
||
margin-bottom: 0.75rem;
|
||
border-left: 4px solid;
|
||
}
|
||
|
||
.flag-item.flag-critical {
|
||
background-color: #fdf2f2;
|
||
border-left-color: #dc2626;
|
||
}
|
||
|
||
.flag-item.flag-warning {
|
||
background-color: #fdfbf0;
|
||
border-left-color: #d97706;
|
||
}
|
||
|
||
.flag-item.flag-info {
|
||
background-color: #f0f5fa;
|
||
border-left-color: var(--realcv-primary);
|
||
}
|
||
|
||
.flag-title {
|
||
font-weight: 600;
|
||
margin-bottom: 0.25rem;
|
||
color: var(--realcv-gray-700);
|
||
}
|
||
|
||
.flag-description {
|
||
color: #4b5563;
|
||
font-size: 0.875rem;
|
||
margin: 0.5rem 0 0 0;
|
||
}
|
||
|
||
.flag-points {
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
padding: 0.25rem 0.5rem;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
/* Employment List - Compact Row Layout */
|
||
.employment-list {
|
||
border-top: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.employment-header {
|
||
display: grid;
|
||
grid-template-columns: 24px 1fr 120px 60px 50px;
|
||
gap: 0.5rem;
|
||
padding: 0.5rem 0.75rem;
|
||
background-color: #f8fafc;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
color: #475569;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.025em;
|
||
align-items: center;
|
||
}
|
||
|
||
.employment-header div {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.employment-header div:nth-child(3),
|
||
.employment-header div:nth-child(4),
|
||
.employment-header div:nth-child(5) {
|
||
justify-content: center;
|
||
text-align: center;
|
||
}
|
||
|
||
.employment-row {
|
||
display: grid;
|
||
grid-template-columns: 24px 1fr 120px 60px 50px;
|
||
gap: 0.5rem;
|
||
align-items: center;
|
||
padding: 0.5rem 0.75rem;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
transition: background-color 0.15s ease;
|
||
}
|
||
|
||
.employment-row:hover {
|
||
background-color: #f9fafb;
|
||
}
|
||
|
||
.employment-row-verified {
|
||
border-left: 3px solid #22c55e;
|
||
}
|
||
|
||
.employment-row-unverified {
|
||
border-left: 3px solid #f59e0b;
|
||
}
|
||
|
||
.employment-status-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.employment-company {
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
gap: 0.125rem;
|
||
min-width: 0;
|
||
}
|
||
|
||
.employment-company-name {
|
||
font-weight: 600;
|
||
font-size: 0.8125rem;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.employment-note-inline {
|
||
font-size: 0.75rem;
|
||
color: #4b5563;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.employment-dates {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 0.8125rem;
|
||
color: #4b5563;
|
||
white-space: nowrap;
|
||
text-align: center;
|
||
}
|
||
|
||
.employment-score {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.employment-points {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 0.8125rem;
|
||
}
|
||
|
||
/* Mobile Responsiveness */
|
||
@@media (max-width: 768px) {
|
||
.score-header .row {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.score-header .col-md-4 {
|
||
border-right: none !important;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||
padding-bottom: 1.5rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.stat-item h3 {
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
.score-roundel {
|
||
width: 120px;
|
||
height: 120px;
|
||
}
|
||
|
||
.score-roundel .score-value {
|
||
font-size: 2rem;
|
||
}
|
||
|
||
.table-responsive {
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.page-header .d-flex {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.page-header .btn {
|
||
width: 100%;
|
||
}
|
||
|
||
.employment-row {
|
||
grid-template-columns: 20px 1fr 50px;
|
||
gap: 0.25rem 0.5rem;
|
||
padding: 0.5rem;
|
||
}
|
||
|
||
.employment-dates {
|
||
display: none;
|
||
}
|
||
|
||
.employment-points {
|
||
display: none;
|
||
}
|
||
|
||
.employment-note-inline {
|
||
display: none;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
@code {
|
||
[Parameter]
|
||
public Guid Id { get; set; }
|
||
|
||
private CVCheckDto? _check;
|
||
private VeracityReport? _report;
|
||
private bool _isLoading = true;
|
||
private string? _errorMessage;
|
||
private Guid _userId;
|
||
private System.Threading.Timer? _pollingTimer;
|
||
private bool _isPolling;
|
||
|
||
protected override async Task OnInitializedAsync()
|
||
{
|
||
await LoadData();
|
||
StartPollingIfNeeded();
|
||
}
|
||
|
||
private void StartPollingIfNeeded()
|
||
{
|
||
if (_check is not null && (_check.Status == "Processing" || _check.Status == "Pending") && !_isPolling)
|
||
{
|
||
_isPolling = true;
|
||
_pollingTimer = new System.Threading.Timer(async _ =>
|
||
{
|
||
await InvokeAsync(async () =>
|
||
{
|
||
await LoadData();
|
||
|
||
// Stop polling if processing is complete or failed
|
||
if (_check is null || _check.Status == "Completed" || _check.Status == "Failed")
|
||
{
|
||
StopPolling();
|
||
}
|
||
|
||
StateHasChanged();
|
||
});
|
||
}, null, TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(3));
|
||
}
|
||
}
|
||
|
||
private void StopPolling()
|
||
{
|
||
_isPolling = false;
|
||
_pollingTimer?.Dispose();
|
||
_pollingTimer = null;
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
StopPolling();
|
||
}
|
||
|
||
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.";
|
||
}
|
||
else
|
||
{
|
||
ComputeFirstOccurrences(); // Pre-compute which companies are first occurrences
|
||
await AuditService.LogAsync(_userId, AuditActions.ReportViewed, "CVCheck", Id, $"Score: {_report.OverallScore}");
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Logger.LogError(ex, "Error loading report data");
|
||
_errorMessage = "An error occurred while loading the report. Please try again.";
|
||
}
|
||
finally
|
||
{
|
||
_isLoading = false;
|
||
}
|
||
}
|
||
|
||
private async Task RefreshStatus()
|
||
{
|
||
await LoadData();
|
||
}
|
||
|
||
private async Task DownloadReport()
|
||
{
|
||
if (_report is null || _check is null) return;
|
||
|
||
try
|
||
{
|
||
var candidateName = Path.GetFileNameWithoutExtension(_check.OriginalFileName);
|
||
var pdfBytes = PdfReportService.GenerateSingleReport(candidateName, _report);
|
||
|
||
var fileName = $"RealCV_Report_{candidateName}_{DateTime.Now:yyyyMMdd}.pdf";
|
||
await DownloadFileAsync(fileName, pdfBytes, "application/pdf");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Logger.LogError(ex, "Error downloading report");
|
||
}
|
||
}
|
||
|
||
private async Task DownloadFileAsync(string fileName, byte[] bytes, string contentType)
|
||
{
|
||
var base64 = Convert.ToBase64String(bytes);
|
||
await JSRuntime.InvokeVoidAsync("downloadFile", fileName, base64, contentType);
|
||
}
|
||
|
||
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"
|
||
};
|
||
}
|
||
|
||
private static string FormatFlagTitle(string title)
|
||
{
|
||
// Convert variable-style names to readable sentences
|
||
return title switch
|
||
{
|
||
"UnverifiedDirectorClaim" => "Unverified Director Claim",
|
||
"EmploymentBeforeIncorporation" => "Employment Before Company Existed",
|
||
"EmploymentAtDissolvedCompany" => "Employment at Dissolved Company",
|
||
"CurrentEmploymentAtDissolvedCompany" => "Current Employment at Dissolved Company",
|
||
"EmploymentAtDormantCompany" => "Employment at Dormant Company",
|
||
"SeniorRoleAtMicroCompany" => "Senior Role at Micro Company",
|
||
"SicCodeMismatch" => "Role/Industry Mismatch",
|
||
"ImplausibleJobTitle" => "Implausible Job Title",
|
||
_ => title
|
||
};
|
||
}
|
||
|
||
// Lookup for first occurrence of each sequential group of the same company (pre-computed when report loads)
|
||
private HashSet<int> _firstOccurrenceIndices = new();
|
||
|
||
private void ComputeFirstOccurrences()
|
||
{
|
||
_firstOccurrenceIndices.Clear();
|
||
if (_report?.EmploymentVerifications is null) return;
|
||
|
||
// Only mark as duplicate if the PREVIOUS entry (sequential) is the same company
|
||
// Non-sequential entries of the same company should each count separately
|
||
for (int i = 0; i < _report.EmploymentVerifications.Count; i++)
|
||
{
|
||
var currentCompany = _report.EmploymentVerifications[i].ClaimedCompany;
|
||
|
||
if (i == 0)
|
||
{
|
||
// First entry is always a first occurrence
|
||
_firstOccurrenceIndices.Add(i);
|
||
}
|
||
else
|
||
{
|
||
var previousCompany = _report.EmploymentVerifications[i - 1].ClaimedCompany;
|
||
// Only skip if same company as immediately preceding entry
|
||
if (!string.Equals(currentCompany, previousCompany, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
_firstOccurrenceIndices.Add(i);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private int GetPointsForCompany(string claimedCompany, string? matchedCompany, int index)
|
||
{
|
||
if (_report?.Flags is null || _report?.EmploymentVerifications is null) return 0;
|
||
|
||
// Only show points for the first occurrence of each company
|
||
if (!_firstOccurrenceIndices.Contains(index))
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
var totalPoints = 0;
|
||
|
||
// Get the verification result for this company
|
||
var verification = _report.EmploymentVerifications.ElementAtOrDefault(index);
|
||
|
||
// Check if there's an "Unverified Company" flag for this company
|
||
// Search by both title pattern and description containing company name
|
||
var companyFlags = _report.Flags
|
||
.Where(f => f.ScoreImpact < 0 &&
|
||
((!string.IsNullOrEmpty(f.Description) && f.Description.Contains(claimedCompany, StringComparison.OrdinalIgnoreCase)) ||
|
||
(!string.IsNullOrEmpty(matchedCompany) && !string.IsNullOrEmpty(f.Description) && f.Description.Contains(matchedCompany, StringComparison.OrdinalIgnoreCase))))
|
||
.GroupBy(f => (f.Title, f.Description))
|
||
.Select(g => g.First())
|
||
.ToList();
|
||
|
||
totalPoints = companyFlags.Sum(f => f.ScoreImpact);
|
||
|
||
// If company is unverified but no flag was found in description search,
|
||
// check if there's a generic "Unverified Company" flag that might use different wording
|
||
if (verification != null && !verification.IsVerified && totalPoints == 0)
|
||
{
|
||
// Look for any Unverified Company flag that might apply
|
||
var unverifiedFlag = _report.Flags
|
||
.FirstOrDefault(f => f.Title == "Unverified Company" && f.ScoreImpact < 0 &&
|
||
!string.IsNullOrEmpty(f.Description) &&
|
||
f.Description.Contains(claimedCompany, StringComparison.OrdinalIgnoreCase));
|
||
|
||
if (unverifiedFlag != null)
|
||
{
|
||
totalPoints = unverifiedFlag.ScoreImpact;
|
||
}
|
||
else
|
||
{
|
||
// Company is unverified but no specific flag found - show the standard penalty
|
||
totalPoints = -10;
|
||
}
|
||
}
|
||
|
||
return totalPoints;
|
||
}
|
||
}
|