2026-01-18 19:20:50 +01:00
|
|
|
|
@page "/report/{Id:guid}"
|
|
|
|
|
|
@attribute [Authorize]
|
|
|
|
|
|
@rendermode InteractiveServer
|
2026-01-20 19:15:33 +01:00
|
|
|
|
@implements IDisposable
|
2026-01-18 19:20:50 +01:00
|
|
|
|
|
|
|
|
|
|
@inject ICVCheckService CVCheckService
|
|
|
|
|
|
@inject NavigationManager NavigationManager
|
|
|
|
|
|
@inject AuthenticationStateProvider AuthenticationStateProvider
|
2026-01-20 16:45:43 +01:00
|
|
|
|
@inject ILogger<Report> Logger
|
2026-01-20 19:15:33 +01:00
|
|
|
|
@inject IJSRuntime JSRuntime
|
2026-01-20 20:58:12 +01:00
|
|
|
|
@inject IAuditService AuditService
|
2026-01-21 02:09:26 +01:00
|
|
|
|
@inject IPdfReportService PdfReportService
|
2026-01-18 19:20:50 +01:00
|
|
|
|
|
|
|
|
|
|
<PageTitle>Verification Report - TrueCV</PageTitle>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="container py-5">
|
|
|
|
|
|
@if (_isLoading)
|
|
|
|
|
|
{
|
|
|
|
|
|
<div class="text-center py-5">
|
2026-01-20 23:35:36 +01:00
|
|
|
|
<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>
|
2026-01-18 19:20:50 +01:00
|
|
|
|
</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>
|
2026-01-20 23:35:36 +01:00
|
|
|
|
<p class="text-muted mb-4">Our AI is analysing the document. This usually takes 1-2 minutes.</p>
|
2026-01-18 19:20:50 +01:00
|
|
|
|
<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 -->
|
2026-01-20 23:35:36 +01:00
|
|
|
|
<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>
|
2026-01-21 09:01:01 +01:00
|
|
|
|
@if (!string.IsNullOrWhiteSpace(_report.CandidateName))
|
|
|
|
|
|
{
|
|
|
|
|
|
<h2 class="h4 text-primary mb-2">@_report.CandidateName</h2>
|
|
|
|
|
|
}
|
2026-01-20 23:35:36 +01:00
|
|
|
|
<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">
|
2026-01-18 19:20:50 +01:00
|
|
|
|
<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>
|
2026-01-20 23:35:36 +01:00
|
|
|
|
Download PDF
|
2026-01-18 19:20:50 +01:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Score Card -->
|
|
|
|
|
|
<div class="row mb-4">
|
|
|
|
|
|
<div class="col-12">
|
2026-01-20 23:35:36 +01:00
|
|
|
|
<div class="card border-0 shadow-sm overflow-hidden">
|
2026-01-20 23:45:30 +01:00
|
|
|
|
<div class="score-header">
|
2026-01-20 23:35:36 +01:00
|
|
|
|
<div class="row align-items-center py-4 px-3">
|
|
|
|
|
|
<div class="col-md-4 text-center">
|
2026-01-20 23:45:30 +01:00
|
|
|
|
<div class="score-roundel @GetScoreColorClass(_report!.OverallScore)">
|
2026-01-20 23:35:36 +01:00
|
|
|
|
<svg class="score-ring" viewBox="0 0 120 120">
|
|
|
|
|
|
<circle class="score-ring-bg" cx="60" cy="60" r="54" />
|
2026-01-20 23:59:13 +01:00
|
|
|
|
@{
|
|
|
|
|
|
var circumference = 339.292;
|
|
|
|
|
|
var progressLength = circumference * _report.OverallScore / 100;
|
|
|
|
|
|
var gapLength = circumference - progressLength;
|
|
|
|
|
|
}
|
2026-01-20 23:56:52 +01:00
|
|
|
|
@if (_report.OverallScore > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
<circle class="score-ring-progress" cx="60" cy="60" r="54"
|
2026-01-20 23:59:13 +01:00
|
|
|
|
stroke-dasharray="@progressLength @gapLength"
|
|
|
|
|
|
stroke-dashoffset="0" />
|
2026-01-20 23:56:52 +01:00
|
|
|
|
}
|
2026-01-20 23:35:36 +01:00
|
|
|
|
</svg>
|
|
|
|
|
|
<div class="score-value-container">
|
|
|
|
|
|
<span class="score-value">@_report.OverallScore</span>
|
|
|
|
|
|
<span class="score-max">/100</span>
|
|
|
|
|
|
</div>
|
2026-01-18 19:20:50 +01:00
|
|
|
|
</div>
|
2026-01-21 09:01:01 +01:00
|
|
|
|
<div class="mt-2 text-white truecv-score-label">TrueCV Score</div>
|
2026-01-18 19:20:50 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="col-md-8">
|
2026-01-20 23:35:36 +01:00
|
|
|
|
<div class="row g-4 text-center text-md-start">
|
2026-01-18 19:20:50 +01:00
|
|
|
|
<div class="col-4">
|
2026-01-20 23:35:36 +01:00
|
|
|
|
<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>
|
|
|
|
|
|
<h3 class="mb-0 fw-bold text-white">@_report.EmploymentVerifications.Count</h3>
|
2026-01-21 09:01:01 +01:00
|
|
|
|
<small class="stat-label">Employers Checked</small>
|
2026-01-20 23:35:36 +01:00
|
|
|
|
</div>
|
2026-01-18 19:20:50 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="col-4">
|
2026-01-20 23:35:36 +01:00
|
|
|
|
<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>
|
|
|
|
|
|
<h3 class="mb-0 fw-bold text-white">@_report.TimelineAnalysis.TotalGapMonths</h3>
|
2026-01-21 09:01:01 +01:00
|
|
|
|
<small class="stat-label">Gap Months</small>
|
2026-01-20 23:35:36 +01:00
|
|
|
|
</div>
|
2026-01-18 19:20:50 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="col-4">
|
2026-01-20 23:35:36 +01:00
|
|
|
|
<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>
|
|
|
|
|
|
<h3 class="mb-0 fw-bold text-white">@_report.Flags.Count</h3>
|
2026-01-21 09:01:01 +01:00
|
|
|
|
<small class="stat-label">Flags Raised</small>
|
2026-01-20 23:35:36 +01:00
|
|
|
|
</div>
|
2026-01-18 19:20:50 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Employment Verification -->
|
|
|
|
|
|
<div class="card border-0 shadow-sm mb-4">
|
2026-01-21 00:59:12 +01:00
|
|
|
|
<div class="card-header py-3" style="background-color: var(--truecv-bg-surface);">
|
2026-01-18 19:20:50 +01:00
|
|
|
|
<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">
|
2026-01-21 09:01:01 +01:00
|
|
|
|
<div class="employment-list">
|
|
|
|
|
|
<div class="employment-header">
|
|
|
|
|
|
<div></div>
|
|
|
|
|
|
<div>Employer</div>
|
|
|
|
|
|
<div>Period</div>
|
|
|
|
|
|
<div>Match</div>
|
|
|
|
|
|
<div>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>
|
2026-01-18 19:20:50 +01:00
|
|
|
|
@if (!string.IsNullOrEmpty(verification.VerificationNotes))
|
|
|
|
|
|
{
|
2026-01-21 09:01:01 +01:00
|
|
|
|
<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>
|
2026-01-18 19:20:50 +01:00
|
|
|
|
}
|
2026-01-21 09:01:01 +01:00
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
<span class="text-success">0</span>
|
|
|
|
|
|
}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
2026-01-18 19:20:50 +01:00
|
|
|
|
</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">
|
2026-01-21 00:59:12 +01:00
|
|
|
|
<div class="card-header py-3" style="background-color: var(--truecv-bg-surface);">
|
2026-01-18 19:20:50 +01:00
|
|
|
|
<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">
|
2026-01-21 00:59:12 +01:00
|
|
|
|
<div class="card-header py-3" style="background-color: var(--truecv-bg-surface);">
|
2026-01-18 19:20:50 +01:00
|
|
|
|
<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">
|
2026-01-21 00:59:12 +01:00
|
|
|
|
<div class="card-header py-3" style="background-color: var(--truecv-bg-surface);">
|
2026-01-18 19:20:50 +01:00
|
|
|
|
<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">
|
|
|
|
|
|
@{
|
2026-01-20 21:04:30 +01:00
|
|
|
|
// 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();
|
2026-01-18 19:20:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@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)
|
|
|
|
|
|
{
|
2026-01-20 23:35:36 +01:00
|
|
|
|
<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>
|
2026-01-18 19:20:50 +01:00
|
|
|
|
</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)
|
|
|
|
|
|
{
|
2026-01-20 23:35:36 +01:00
|
|
|
|
<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>
|
2026-01-18 19:20:50 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@if (infoFlags.Count > 0)
|
|
|
|
|
|
{
|
2026-01-20 23:35:36 +01:00
|
|
|
|
<h6 class="text-primary fw-bold mb-3 @(criticalFlags.Count > 0 || warningFlags.Count > 0 ? "mt-4" : "")">
|
2026-01-18 19:20:50 +01:00
|
|
|
|
<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)
|
|
|
|
|
|
{
|
2026-01-20 23:35:36 +01:00
|
|
|
|
<div class="flag-item flag-info">
|
|
|
|
|
|
<strong class="flag-title">@flag.Title</strong>
|
|
|
|
|
|
<p class="flag-description">@flag.Description</p>
|
2026-01-18 19:20:50 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<style>
|
2026-01-21 09:01:01 +01:00
|
|
|
|
/* Score Header - Blue gradient */
|
2026-01-20 23:35:36 +01:00
|
|
|
|
.score-header {
|
2026-01-21 09:01:01 +01:00
|
|
|
|
background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%);
|
2026-01-20 23:35:36 +01:00
|
|
|
|
color: white;
|
2026-01-21 09:01:01 +01:00
|
|
|
|
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;
|
2026-01-20 23:35:36 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 23:45:30 +01:00
|
|
|
|
/* Score Roundel */
|
|
|
|
|
|
.score-roundel {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
width: 140px;
|
|
|
|
|
|
height: 140px;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.15);
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
padding: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 23:59:13 +01:00
|
|
|
|
.score-roundel .score-ring {
|
|
|
|
|
|
transform: rotate(-90deg);
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 23:45:30 +01:00
|
|
|
|
.score-roundel .score-ring-bg {
|
|
|
|
|
|
fill: none;
|
|
|
|
|
|
stroke-width: 8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.score-roundel .score-ring-progress {
|
|
|
|
|
|
fill: none;
|
|
|
|
|
|
stroke-width: 8;
|
|
|
|
|
|
stroke-linecap: round;
|
2026-01-20 23:59:13 +01:00
|
|
|
|
transform-origin: center;
|
2026-01-20 23:45:30 +01:00
|
|
|
|
transition: stroke-dasharray 0.6s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 00:02:06 +01:00
|
|
|
|
/* Score color variants - colours both the background ring and progress */
|
|
|
|
|
|
.score-roundel.score-high .score-ring-bg {
|
2026-01-21 09:01:01 +01:00
|
|
|
|
stroke: rgba(255, 255, 255, 0.3);
|
2026-01-21 00:02:06 +01:00
|
|
|
|
}
|
2026-01-20 23:45:30 +01:00
|
|
|
|
.score-roundel.score-high .score-ring-progress {
|
2026-01-21 09:01:01 +01:00
|
|
|
|
stroke: #4ade80;
|
2026-01-20 23:45:30 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 00:02:06 +01:00
|
|
|
|
.score-roundel.score-medium .score-ring-bg {
|
2026-01-21 09:01:01 +01:00
|
|
|
|
stroke: rgba(255, 255, 255, 0.3);
|
2026-01-21 00:02:06 +01:00
|
|
|
|
}
|
2026-01-20 23:45:30 +01:00
|
|
|
|
.score-roundel.score-medium .score-ring-progress {
|
2026-01-21 09:01:01 +01:00
|
|
|
|
stroke: #fbbf24;
|
2026-01-20 23:45:30 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 00:02:06 +01:00
|
|
|
|
.score-roundel.score-low .score-ring-bg {
|
2026-01-21 09:01:01 +01:00
|
|
|
|
stroke: rgba(255, 255, 255, 0.3);
|
2026-01-21 00:02:06 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 23:45:30 +01:00
|
|
|
|
.score-roundel.score-low .score-ring-progress {
|
2026-01-21 09:01:01 +01:00
|
|
|
|
stroke: #f87171;
|
2026-01-20 23:45:30 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.score-roundel .score-value-container {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
|
text-align: center;
|
2026-01-20 23:35:36 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 23:45:30 +01:00
|
|
|
|
.score-roundel .score-value {
|
|
|
|
|
|
font-size: 2.5rem;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
color: white;
|
2026-01-20 23:35:36 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 23:45:30 +01:00
|
|
|
|
.score-roundel .score-max {
|
|
|
|
|
|
font-size: 1rem;
|
2026-01-21 09:01:01 +01:00
|
|
|
|
opacity: 0.85;
|
2026-01-20 23:45:30 +01:00
|
|
|
|
color: white;
|
2026-01-20 23:35:36 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 09:01:01 +01:00
|
|
|
|
.truecv-score-label {
|
|
|
|
|
|
font-family: 'Inter', sans-serif;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
|
letter-spacing: 0.025em;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 23:35:36 +01:00
|
|
|
|
/* Stat Items */
|
|
|
|
|
|
.stat-item {
|
|
|
|
|
|
padding: 0.5rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-icon {
|
|
|
|
|
|
width: 44px;
|
|
|
|
|
|
height: 44px;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.2);
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
display: inline-flex;
|
2026-01-18 19:20:50 +01:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-01-20 23:35:36 +01:00
|
|
|
|
margin-bottom: 0.5rem;
|
|
|
|
|
|
color: white;
|
2026-01-18 19:20:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 09:01:01 +01:00
|
|
|
|
.stat-label {
|
|
|
|
|
|
color: rgba(255, 255, 255, 0.85);
|
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 23:35:36 +01:00
|
|
|
|
/* Flag Items */
|
|
|
|
|
|
.flag-item {
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
padding: 1rem 1.25rem;
|
|
|
|
|
|
margin-bottom: 0.75rem;
|
|
|
|
|
|
border-left: 4px solid;
|
2026-01-18 19:20:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 23:35:36 +01:00
|
|
|
|
.flag-item.flag-critical {
|
2026-01-21 00:59:12 +01:00
|
|
|
|
background-color: #fdf2f2;
|
2026-01-20 23:35:36 +01:00
|
|
|
|
border-left-color: #dc2626;
|
2026-01-18 19:20:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 23:35:36 +01:00
|
|
|
|
.flag-item.flag-warning {
|
2026-01-21 00:59:12 +01:00
|
|
|
|
background-color: #fdfbf0;
|
2026-01-20 23:35:36 +01:00
|
|
|
|
border-left-color: #d97706;
|
2026-01-18 19:20:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 23:35:36 +01:00
|
|
|
|
.flag-item.flag-info {
|
2026-01-21 00:59:12 +01:00
|
|
|
|
background-color: #f0f5fa;
|
|
|
|
|
|
border-left-color: var(--truecv-primary);
|
2026-01-18 19:20:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 23:35:36 +01:00
|
|
|
|
.flag-title {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
margin-bottom: 0.25rem;
|
2026-01-21 00:59:12 +01:00
|
|
|
|
color: var(--truecv-gray-700);
|
2026-01-18 19:20:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 23:35:36 +01:00
|
|
|
|
.flag-description {
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
|
margin: 0.5rem 0 0 0;
|
2026-01-18 19:20:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 23:35:36 +01:00
|
|
|
|
.flag-points {
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
padding: 0.25rem 0.5rem;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 09:01:01 +01:00
|
|
|
|
/* Employment List - Compact Row Layout */
|
|
|
|
|
|
.employment-list {
|
|
|
|
|
|
border-top: 1px solid #e5e7eb;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.employment-header {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 24px 1fr auto 60px 50px;
|
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
|
padding: 0.5rem 0.75rem;
|
|
|
|
|
|
background-color: #f8fafc;
|
|
|
|
|
|
border-bottom: 1px solid #e5e7eb;
|
|
|
|
|
|
font-size: 0.6875rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
letter-spacing: 0.025em;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.employment-header div:nth-child(3),
|
|
|
|
|
|
.employment-header div:nth-child(4),
|
|
|
|
|
|
.employment-header div:nth-child(5) {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.employment-row {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 24px 1fr auto 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;
|
|
|
|
|
|
gap: 0.125rem;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.employment-company-name {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
color: #1f2937;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.employment-note-inline {
|
|
|
|
|
|
font-size: 0.6875rem;
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
line-height: 1.2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.employment-dates {
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.employment-score {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.employment-points {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 23:35:36 +01:00
|
|
|
|
/* 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 23:59:13 +01:00
|
|
|
|
.score-roundel {
|
2026-01-20 23:35:36 +01:00
|
|
|
|
width: 120px;
|
|
|
|
|
|
height: 120px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 23:59:13 +01:00
|
|
|
|
.score-roundel .score-value {
|
2026-01-20 23:35:36 +01:00
|
|
|
|
font-size: 2rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table-responsive {
|
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.page-header .d-flex {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.page-header .btn {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
2026-01-21 09:01:01 +01:00
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
2026-01-18 19:20:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
|
|
@code {
|
|
|
|
|
|
[Parameter]
|
|
|
|
|
|
public Guid Id { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
private CVCheckDto? _check;
|
|
|
|
|
|
private VeracityReport? _report;
|
|
|
|
|
|
private bool _isLoading = true;
|
|
|
|
|
|
private string? _errorMessage;
|
|
|
|
|
|
private Guid _userId;
|
2026-01-20 19:15:33 +01:00
|
|
|
|
private System.Threading.Timer? _pollingTimer;
|
|
|
|
|
|
private bool _isPolling;
|
2026-01-18 19:20:50 +01:00
|
|
|
|
|
|
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
await LoadData();
|
2026-01-20 19:15:33 +01:00
|
|
|
|
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();
|
2026-01-18 19:20:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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.";
|
|
|
|
|
|
}
|
2026-01-20 20:58:12 +01:00
|
|
|
|
else
|
|
|
|
|
|
{
|
Improve company verification filtering and fix duplicate points display
- Add NonEmploymentEntityPatterns dictionary with 10 categories of non-employer entities
(clubs, associations, trusts, charities, investment, property, religious, sports, educational, professional)
- Expand SkipWords from ~30 to 120+ words for better core identifier extraction
- Add GetSearchEntityTypes() and IsValidEmploymentEntity() helper methods
- Refactor FindBestMatch() to use pattern-based filtering instead of hardcoded checks
- Fix UI showing duplicate points for same company appearing multiple times
(now only shows points on first occurrence, subsequent rows show 0)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 22:33:16 +01:00
|
|
|
|
ComputeFirstOccurrences(); // Pre-compute which companies are first occurrences
|
2026-01-20 20:58:12 +01:00
|
|
|
|
await AuditService.LogAsync(_userId, AuditActions.ReportViewed, "CVCheck", Id, $"Score: {_report.OverallScore}");
|
|
|
|
|
|
}
|
2026-01-18 19:20:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2026-01-20 16:45:43 +01:00
|
|
|
|
Logger.LogError(ex, "Error loading report data");
|
|
|
|
|
|
_errorMessage = "An error occurred while loading the report. Please try again.";
|
2026-01-18 19:20:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
_isLoading = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task RefreshStatus()
|
|
|
|
|
|
{
|
|
|
|
|
|
await LoadData();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 19:15:33 +01:00
|
|
|
|
private async Task DownloadReport()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_report is null || _check is null) return;
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-01-20 22:49:22 +01:00
|
|
|
|
var candidateName = Path.GetFileNameWithoutExtension(_check.OriginalFileName);
|
|
|
|
|
|
var pdfBytes = PdfReportService.GenerateSingleReport(candidateName, _report);
|
2026-01-20 19:15:33 +01:00
|
|
|
|
|
2026-01-20 22:49:22 +01:00
|
|
|
|
var fileName = $"TrueCV_Report_{candidateName}_{DateTime.Now:yyyyMMdd}.pdf";
|
|
|
|
|
|
await DownloadFileAsync(fileName, pdfBytes, "application/pdf");
|
2026-01-20 19:15:33 +01:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Logger.LogError(ex, "Error downloading report");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 22:49:22 +01:00
|
|
|
|
private async Task DownloadFileAsync(string fileName, byte[] bytes, string contentType)
|
2026-01-18 19:20:50 +01:00
|
|
|
|
{
|
2026-01-20 19:15:33 +01:00
|
|
|
|
var base64 = Convert.ToBase64String(bytes);
|
|
|
|
|
|
await JSRuntime.InvokeVoidAsync("downloadFile", fileName, base64, contentType);
|
2026-01-18 19:20:50 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-01-20 21:04:30 +01:00
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-01-20 21:49:26 +01:00
|
|
|
|
|
Improve company verification filtering and fix duplicate points display
- Add NonEmploymentEntityPatterns dictionary with 10 categories of non-employer entities
(clubs, associations, trusts, charities, investment, property, religious, sports, educational, professional)
- Expand SkipWords from ~30 to 120+ words for better core identifier extraction
- Add GetSearchEntityTypes() and IsValidEmploymentEntity() helper methods
- Refactor FindBestMatch() to use pattern-based filtering instead of hardcoded checks
- Fix UI showing duplicate points for same company appearing multiple times
(now only shows points on first occurrence, subsequent rows show 0)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 22:33:16 +01:00
|
|
|
|
// Lookup for first occurrence of each company (pre-computed when report loads)
|
|
|
|
|
|
private HashSet<int> _firstOccurrenceIndices = new();
|
|
|
|
|
|
|
|
|
|
|
|
private void ComputeFirstOccurrences()
|
|
|
|
|
|
{
|
|
|
|
|
|
_firstOccurrenceIndices.Clear();
|
|
|
|
|
|
if (_report?.EmploymentVerifications is null) return;
|
|
|
|
|
|
|
|
|
|
|
|
var seenCompanies = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
|
for (int i = 0; i < _report.EmploymentVerifications.Count; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var company = _report.EmploymentVerifications[i].ClaimedCompany;
|
|
|
|
|
|
if (seenCompanies.Add(company))
|
|
|
|
|
|
{
|
|
|
|
|
|
_firstOccurrenceIndices.Add(i);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private int GetPointsForCompany(string claimedCompany, string? matchedCompany, int index)
|
2026-01-20 21:49:26 +01:00
|
|
|
|
{
|
|
|
|
|
|
if (_report?.Flags is null) return 0;
|
|
|
|
|
|
|
Improve company verification filtering and fix duplicate points display
- Add NonEmploymentEntityPatterns dictionary with 10 categories of non-employer entities
(clubs, associations, trusts, charities, investment, property, religious, sports, educational, professional)
- Expand SkipWords from ~30 to 120+ words for better core identifier extraction
- Add GetSearchEntityTypes() and IsValidEmploymentEntity() helper methods
- Refactor FindBestMatch() to use pattern-based filtering instead of hardcoded checks
- Fix UI showing duplicate points for same company appearing multiple times
(now only shows points on first occurrence, subsequent rows show 0)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 22:33:16 +01:00
|
|
|
|
// Only show points for the first occurrence of each company
|
|
|
|
|
|
if (!_firstOccurrenceIndices.Contains(index))
|
|
|
|
|
|
{
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 21:49:26 +01:00
|
|
|
|
// Sum up all flags that mention this company in their description
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
|
|
return companyFlags.Sum(f => f.ScoreImpact);
|
|
|
|
|
|
}
|
2026-01-18 19:20:50 +01:00
|
|
|
|
}
|