@page "/report/{Id:guid}" @attribute [Authorize] @rendermode InteractiveServer @implements IDisposable @inject ICVCheckService CVCheckService @inject NavigationManager NavigationManager @inject AuthenticationStateProvider AuthenticationStateProvider @inject ILogger Logger @inject IJSRuntime JSRuntime @inject IAuditService AuditService @inject IPdfReportService PdfReportService Verification Report - TrueCV
@if (_isLoading) {
Loading report...

Loading verification report...

} else if (_errorMessage is not null) { Back to Dashboard } else if (_check is not null && _check.Status != "Completed") {
@if (_check.Status == "Processing") {
Processing...

Processing Your CV

Our AI is analysing the document. This usually takes 1-2 minutes.

} else if (_check.Status == "Pending") {

Queued for Processing

Your CV is in the queue and will be processed shortly.

} else if (_check.Status == "Failed") {

Processing Failed

We encountered an error processing your CV. Please try uploading again.

}

File: @_check.OriginalFileName
Uploaded: @_check.CreatedAt.ToString("dd MMM yyyy HH:mm")

} else if (_report is not null && _check is not null) {
@{ var circumference = 339.292; var progressLength = circumference * _report.OverallScore / 100; var gapLength = circumference - progressLength; } @if (_report.OverallScore > 0) { }
@_report.OverallScore /100
TrueCV Score

@_report.EmploymentVerifications.Count

Employers Checked

@_report.TimelineAnalysis.TotalGapMonths

Gap Months

@_report.Flags.Count

Flags Raised
Employment Verification
Employer
Period
Match
Pts
@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);
@if (verification.IsVerified) { } else { }
@verification.ClaimedCompany @if (!string.IsNullOrEmpty(verification.VerificationNotes)) { @verification.VerificationNotes }
@if (verification.ClaimedStartDate.HasValue) { @verification.ClaimedStartDate.Value.ToString("MMM yyyy") – @(verification.ClaimedEndDate?.ToString("MMM yyyy") ?? "Present") } else { }
@verification.MatchScore%
@if (companyPoints < 0) { @companyPoints } else if (!verification.IsVerified && !_firstOccurrenceIndices.Contains(index)) { } else { 0 }
}
Employment Gaps
@if (_report.TimelineAnalysis.Gaps.Count > 0) {
    @foreach (var gap in _report.TimelineAnalysis.Gaps) {
  • @gap.StartDate.ToString("MMM yyyy") - @gap.EndDate.ToString("MMM yyyy") @gap.Months months
  • }
Total gap time: @_report.TimelineAnalysis.TotalGapMonths months
} else {

No significant gaps detected

}
Timeline Overlaps
@if (_report.TimelineAnalysis.Overlaps.Count > 0) {
    @foreach (var overlap in _report.TimelineAnalysis.Overlaps) {
  • @overlap.Company1 & @overlap.Company2
    @overlap.Months months
    @overlap.OverlapStart.ToString("MMM yyyy") - @overlap.OverlapEnd.ToString("MMM yyyy")
  • }
Total overlap time: @_report.TimelineAnalysis.TotalOverlapMonths months
} else {

No overlapping positions detected

}
@if (_report.Flags.Count > 0) {
Flags Raised
@{ // 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) {
Critical Issues
@foreach (var flag in criticalFlags) {
@flag.Title -@flag.ScoreImpact pts

@flag.Description

} } @if (warningFlags.Count > 0) {
Warnings
@foreach (var flag in warningFlags) {
@flag.Title -@flag.ScoreImpact pts

@flag.Description

} } @if (infoFlags.Count > 0) {
Information
@foreach (var flag in infoFlags) {
@flag.Title

@flag.Description

} }
} }
@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 = $"TrueCV_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 _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; } }