Improve report readability and add score breakdown

- Add Score Breakdown section showing how score is calculated
- Convert variable-style flag names to readable titles (e.g. UnverifiedDirectorClaim -> Unverified Director Claim)
- Deduplicate flags in report display for existing reports
- Make verification notes more user-friendly
- Add "How Scoring Works" explanation panel

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-20 21:04:30 +01:00
parent 0eee5473e4
commit 21c73ab1e2
12 changed files with 5153 additions and 4 deletions

View File

@@ -304,6 +304,7 @@ public sealed class ProcessCVCheckJob
"SeniorRoleAtMicroCompany" => "Senior Role at Micro Company",
"SicCodeMismatch" => "Role/Industry Mismatch",
"ImplausibleJobTitle" => "Implausible Job Title",
"UnverifiedDirectorClaim" => "Unverified Director Claim",
_ => companyFlag.Type
},
Description = companyFlag.Message,

View File

@@ -77,7 +77,7 @@ public sealed class CompanyVerifierService : ICompanyVerifierService
{
_logger.LogDebug("No fuzzy match above threshold for: {CompanyName}", companyName);
return CreateUnverifiedResult(companyName, startDate, endDate, jobTitle,
$"No company name matched above {FuzzyMatchThreshold}% threshold");
"Company name could not be verified against official records");
}
var match = bestMatch.Value;

View File

@@ -144,6 +144,70 @@
</div>
</div>
<!-- Score Breakdown -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white py-3">
<h5 class="mb-0 fw-bold">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-calculator me-2" viewBox="0 0 16 16">
<path d="M12 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h8zM4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H4z"/>
<path d="M4 2.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-7a.5.5 0 0 1-.5-.5v-2zm0 4a.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-1zm0 3a.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-1zm0 3a.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-6a.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-1zm0 3a.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-1zm0 3a.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-6a.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-1zm0 3a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-4z"/>
</svg>
Score Breakdown
</h5>
</div>
<div class="card-body">
@{
var deductedFlags = _report.Flags
.GroupBy(f => (f.Title, f.Description))
.Select(g => g.First())
.Where(f => f.ScoreImpact < 0)
.ToList();
var totalDeductions = deductedFlags.Sum(f => Math.Abs(f.ScoreImpact));
}
<div class="row">
<div class="col-md-6">
<table class="table table-sm">
<tbody>
<tr class="table-success">
<td><strong>Base Score</strong></td>
<td class="text-end"><strong>100 pts</strong></td>
</tr>
@foreach (var flag in deductedFlags.OrderByDescending(f => Math.Abs(f.ScoreImpact)))
{
<tr class="text-danger">
<td>@FormatFlagTitle(flag.Title)</td>
<td class="text-end">-@Math.Abs(flag.ScoreImpact) pts</td>
</tr>
}
<tr class="table-secondary">
<td><strong>Total Deductions</strong></td>
<td class="text-end text-danger"><strong>-@totalDeductions pts</strong></td>
</tr>
<tr class="table-primary">
<td><strong>Final Score</strong></td>
<td class="text-end"><strong>@_report.OverallScore pts</strong></td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-6">
<div class="alert alert-light border">
<h6 class="fw-bold mb-2">How Scoring Works</h6>
<ul class="small mb-0">
<li>Everyone starts with <strong>100 points</strong></li>
<li>Points are deducted for issues found during verification</li>
<li>Unverified companies: <strong>-10 pts</strong> each</li>
<li>Director claims not verified: <strong>-20 pts</strong> each</li>
<li>Employment before company existed: <strong>-20 pts</strong></li>
<li>Employment gaps: <strong>-1 to -10 pts</strong> per gap</li>
<li>Excessive overlaps: <strong>-2 pts</strong> per month</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Employment Verification -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white py-3">
@@ -351,9 +415,20 @@
</div>
<div class="card-body">
@{
var criticalFlags = _report.Flags.Where(f => f.Severity == "Critical").ToList();
var warningFlags = _report.Flags.Where(f => f.Severity == "Warning").ToList();
var infoFlags = _report.Flags.Where(f => f.Severity == "Info").ToList();
// 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)
@@ -660,4 +735,21 @@
_ => "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
};
}
}