2026-01-18 19:20:50 +01:00
|
|
|
@page "/dashboard"
|
|
|
|
|
@attribute [Authorize]
|
|
|
|
|
@rendermode InteractiveServer
|
|
|
|
|
|
|
|
|
|
@inject ICVCheckService CVCheckService
|
|
|
|
|
@inject NavigationManager NavigationManager
|
|
|
|
|
@inject AuthenticationStateProvider AuthenticationStateProvider
|
2026-01-20 16:45:43 +01:00
|
|
|
@inject ILogger<Dashboard> Logger
|
2026-01-20 19:15:33 +01:00
|
|
|
@inject IJSRuntime JSRuntime
|
2026-01-20 19:28:22 +01:00
|
|
|
@inject TrueCV.Web.Services.PdfReportService PdfReportService
|
2026-01-18 19:20:50 +01:00
|
|
|
|
|
|
|
|
<PageTitle>Dashboard - TrueCV</PageTitle>
|
|
|
|
|
|
|
|
|
|
<div class="container py-5">
|
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
|
|
|
<div>
|
|
|
|
|
<h1 class="fw-bold mb-1">Dashboard</h1>
|
|
|
|
|
<p class="text-muted mb-0">View and manage your CV verification checks</p>
|
|
|
|
|
</div>
|
2026-01-20 19:15:33 +01:00
|
|
|
<div class="d-flex gap-2">
|
2026-01-20 19:28:22 +01:00
|
|
|
<button class="btn btn-outline-secondary" @onclick="ExportToPdf" disabled="@(_isExporting || !HasCompletedChecks())">
|
2026-01-20 19:15:33 +01:00
|
|
|
@if (_isExporting)
|
|
|
|
|
{
|
|
|
|
|
<span class="spinner-border spinner-border-sm me-1" role="status"></span>
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2026-01-20 19:28:22 +01:00
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-pdf me-1" viewBox="0 0 16 16">
|
|
|
|
|
<path d="M4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H4zm0 1h8a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1z"/>
|
|
|
|
|
<path d="M4.603 12.087a.81.81 0 0 1-.438-.42c-.195-.388-.13-.776.08-1.102.198-.307.526-.568.897-.787a7.68 7.68 0 0 1 1.482-.645 19.701 19.701 0 0 0 1.062-2.227 7.269 7.269 0 0 1-.43-1.295c-.086-.4-.119-.796-.046-1.136.075-.354.274-.672.65-.823.192-.077.4-.12.602-.077a.7.7 0 0 1 .477.365c.088.164.12.356.127.538.007.187-.012.395-.047.614-.084.51-.27 1.134-.52 1.794a10.954 10.954 0 0 0 .98 1.686 5.753 5.753 0 0 1 1.334.05c.364.065.734.195.96.465.12.144.193.32.2.518.007.192-.047.382-.138.563a1.04 1.04 0 0 1-.354.416.856.856 0 0 1-.51.138c-.331-.014-.654-.196-.933-.417a5.716 5.716 0 0 1-.911-.95 11.642 11.642 0 0 0-1.997.406 11.311 11.311 0 0 1-1.021 1.51c-.29.35-.608.655-.926.787a.793.793 0 0 1-.58.029zm1.379-1.901c-.166.076-.32.156-.459.238-.328.194-.541.383-.647.547-.094.145-.096.25-.04.361.01.022.02.036.026.044a.27.27 0 0 0 .035-.012c.137-.056.355-.235.635-.572a8.18 8.18 0 0 0 .45-.606zm1.64-1.33a12.647 12.647 0 0 1 1.01-.193 11.666 11.666 0 0 1-.51-.858 20.741 20.741 0 0 1-.5 1.05zm2.446.45c.15.162.296.3.435.41.24.19.407.253.498.256a.107.107 0 0 0 .07-.015.307.307 0 0 0 .094-.125.436.436 0 0 0 .059-.2.095.095 0 0 0-.026-.063c-.052-.062-.2-.152-.518-.209a3.881 3.881 0 0 0-.612-.053zM8.078 5.8a6.7 6.7 0 0 0 .2-.828c.031-.188.043-.343.038-.465a.613.613 0 0 0-.032-.198.517.517 0 0 0-.145.04c-.087.035-.158.106-.196.283-.04.192-.03.469.046.822.024.111.054.227.09.346z"/>
|
2026-01-20 19:15:33 +01:00
|
|
|
</svg>
|
|
|
|
|
}
|
2026-01-20 19:28:22 +01:00
|
|
|
Export PDF
|
2026-01-20 19:15:33 +01:00
|
|
|
</button>
|
|
|
|
|
<a href="/check" class="btn btn-primary">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-lg me-1" viewBox="0 0 16 16">
|
|
|
|
|
<path fill-rule="evenodd" d="M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2Z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
New Check
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
2026-01-18 19:20:50 +01:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
@if (_isLoading)
|
|
|
|
|
{
|
|
|
|
|
<div class="text-center py-5">
|
|
|
|
|
<div class="spinner-border text-primary" role="status">
|
|
|
|
|
<span class="visually-hidden">Loading...</span>
|
|
|
|
|
</div>
|
|
|
|
|
<p class="mt-3 text-muted">Loading your checks...</p>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
else if (!string.IsNullOrEmpty(_errorMessage))
|
|
|
|
|
{
|
|
|
|
|
<div class="alert alert-danger" role="alert">
|
|
|
|
|
@_errorMessage
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
else if (_checks.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
<div class="card border-0 shadow-sm">
|
|
|
|
|
<div class="card-body text-center py-5">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor" class="bi bi-file-earmark-text text-muted mb-3" viewBox="0 0 16 16">
|
|
|
|
|
<path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"/>
|
|
|
|
|
<path d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5L9.5 0zm0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
<h4>No CV Checks Yet</h4>
|
|
|
|
|
<p class="text-muted mb-4">Start by uploading your first CV for verification</p>
|
|
|
|
|
<a href="/check" class="btn btn-primary">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-upload me-1" viewBox="0 0 16 16">
|
|
|
|
|
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
|
|
|
|
<path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708l3-3z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
Upload CV
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
<!-- Stats Cards -->
|
|
|
|
|
<div class="row mb-4">
|
|
|
|
|
<div class="col-md-4">
|
|
|
|
|
<div class="card border-0 shadow-sm">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<div class="d-flex align-items-center">
|
|
|
|
|
<div class="bg-primary bg-opacity-10 rounded-circle p-3 me-3">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-file-earmark-check text-primary" viewBox="0 0 16 16">
|
|
|
|
|
<path d="M10.854 7.854a.5.5 0 0 0-.708-.708L7.5 9.793 6.354 8.646a.5.5 0 1 0-.708.708l1.5 1.5a.5.5 0 0 0 .708 0l3-3z"/>
|
|
|
|
|
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<h3 class="mb-0">@_checks.Count</h3>
|
|
|
|
|
<small class="text-muted">Total Checks</small>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-md-4">
|
|
|
|
|
<div class="card border-0 shadow-sm">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<div class="d-flex align-items-center">
|
|
|
|
|
<div class="bg-success bg-opacity-10 rounded-circle p-3 me-3">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-check-circle text-success" viewBox="0 0 16 16">
|
|
|
|
|
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
|
|
|
|
<path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<h3 class="mb-0">@_checks.Count(c => c.Status == "Completed")</h3>
|
|
|
|
|
<small class="text-muted">Completed</small>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-md-4">
|
|
|
|
|
<div class="card border-0 shadow-sm">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<div class="d-flex align-items-center">
|
|
|
|
|
<div class="bg-warning bg-opacity-10 rounded-circle p-3 me-3">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-hourglass-split text-warning" viewBox="0 0 16 16">
|
|
|
|
|
<path d="M2.5 15a.5.5 0 1 1 0-1h1v-1a4.5 4.5 0 0 1 2.557-4.06c.29-.139.443-.377.443-.59v-.7c0-.213-.154-.451-.443-.59A4.5 4.5 0 0 1 3.5 3V2h-1a.5.5 0 0 1 0-1h11a.5.5 0 0 1 0 1h-1v1a4.5 4.5 0 0 1-2.557 4.06c-.29.139-.443.377-.443.59v.7c0 .213.154.451.443.59A4.5 4.5 0 0 1 12.5 13v1h1a.5.5 0 0 1 0 1h-11zm2-13v1c0 .537.12 1.045.337 1.5h6.326c.216-.455.337-.963.337-1.5V2h-7zm3 6.35c0 .701-.478 1.236-1.011 1.492A3.5 3.5 0 0 0 4.5 13s.866-1.299 3-1.48V8.35zm1 0v3.17c2.134.181 3 1.48 3 1.48a3.5 3.5 0 0 0-1.989-3.158C8.978 9.586 8.5 9.052 8.5 8.351z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<h3 class="mb-0">@_checks.Count(c => c.Status is "Pending" or "Processing")</h3>
|
|
|
|
|
<small class="text-muted">In Progress</small>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Checks List -->
|
|
|
|
|
<div class="card border-0 shadow-sm">
|
|
|
|
|
<div class="card-header bg-white py-3">
|
|
|
|
|
<h5 class="mb-0 fw-bold">Recent CV Checks</h5>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="card-body p-0">
|
|
|
|
|
<div class="table-responsive">
|
|
|
|
|
<table class="table table-hover mb-0">
|
|
|
|
|
<thead class="table-light">
|
|
|
|
|
<tr>
|
|
|
|
|
<th>File Name</th>
|
|
|
|
|
<th>Date</th>
|
|
|
|
|
<th class="text-center">Status</th>
|
|
|
|
|
<th class="text-center">Score</th>
|
|
|
|
|
<th class="text-end">Actions</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
@foreach (var check in _checks)
|
|
|
|
|
{
|
|
|
|
|
<tr class="@(check.Status == "Completed" ? "cursor-pointer" : "")"
|
|
|
|
|
@onclick="() => ViewReport(check)">
|
|
|
|
|
<td>
|
|
|
|
|
<div class="d-flex align-items-center">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-file-earmark-text text-primary me-2" viewBox="0 0 16 16">
|
|
|
|
|
<path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"/>
|
|
|
|
|
<path d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5L9.5 0zm0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
<span class="fw-medium">@check.OriginalFileName</span>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
<td>
|
|
|
|
|
<span class="text-muted">@check.CreatedAt.ToString("dd MMM yyyy HH:mm")</span>
|
|
|
|
|
</td>
|
|
|
|
|
<td class="text-center">
|
|
|
|
|
@switch (check.Status)
|
|
|
|
|
{
|
|
|
|
|
case "Completed":
|
|
|
|
|
<span class="badge bg-success">Completed</span>
|
|
|
|
|
break;
|
|
|
|
|
case "Processing":
|
|
|
|
|
<span class="badge bg-primary">
|
|
|
|
|
<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true" style="width: 0.7rem; height: 0.7rem;"></span>
|
|
|
|
|
Processing
|
|
|
|
|
</span>
|
|
|
|
|
break;
|
|
|
|
|
case "Pending":
|
|
|
|
|
<span class="badge bg-secondary">Pending</span>
|
|
|
|
|
break;
|
|
|
|
|
case "Failed":
|
|
|
|
|
<span class="badge bg-danger">Failed</span>
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
<span class="badge bg-secondary">@check.Status</span>
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
</td>
|
|
|
|
|
<td class="text-center">
|
|
|
|
|
@if (check.VeracityScore.HasValue)
|
|
|
|
|
{
|
|
|
|
|
<span class="badge @GetScoreBadgeClass(check.VeracityScore.Value) fs-6">
|
|
|
|
|
@check.VeracityScore
|
|
|
|
|
</span>
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
<span class="text-muted">-</span>
|
|
|
|
|
}
|
|
|
|
|
</td>
|
|
|
|
|
<td class="text-end">
|
|
|
|
|
@if (check.Status == "Completed")
|
|
|
|
|
{
|
|
|
|
|
<a href="/report/@check.Id" class="btn btn-sm btn-outline-primary" @onclick:stopPropagation="true">
|
|
|
|
|
View Report
|
|
|
|
|
</a>
|
|
|
|
|
}
|
|
|
|
|
else if (check.Status is "Pending" or "Processing")
|
|
|
|
|
{
|
|
|
|
|
<button class="btn btn-sm btn-outline-secondary" disabled>
|
|
|
|
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
|
|
|
|
</button>
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
<a href="/check" class="btn btn-sm btn-outline-warning">
|
|
|
|
|
Retry
|
|
|
|
|
</a>
|
|
|
|
|
}
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
.cursor-pointer {
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cursor-pointer:hover {
|
|
|
|
|
background-color: rgba(0, 0, 0, 0.02);
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
@code {
|
|
|
|
|
private List<CVCheckDto> _checks = [];
|
|
|
|
|
private bool _isLoading = true;
|
2026-01-20 19:15:33 +01:00
|
|
|
private bool _isExporting;
|
2026-01-18 19:20:50 +01:00
|
|
|
private string? _errorMessage;
|
2026-01-20 19:15:33 +01:00
|
|
|
private Guid _userId;
|
2026-01-18 19:20:50 +01:00
|
|
|
|
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
|
|
|
{
|
|
|
|
|
await LoadChecks();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task LoadChecks()
|
|
|
|
|
{
|
|
|
|
|
_isLoading = true;
|
|
|
|
|
_errorMessage = null;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
|
|
|
|
|
var userIdClaim = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
|
|
|
|
|
2026-01-20 19:15:33 +01:00
|
|
|
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out _userId))
|
2026-01-18 19:20:50 +01:00
|
|
|
{
|
|
|
|
|
_errorMessage = "Unable to identify user. Please log in again.";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 19:15:33 +01:00
|
|
|
_checks = await CVCheckService.GetUserChecksAsync(_userId);
|
2026-01-18 19:20:50 +01:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-01-20 16:45:43 +01:00
|
|
|
Logger.LogError(ex, "Error loading CV checks");
|
|
|
|
|
_errorMessage = "An error occurred while loading checks. Please try again.";
|
2026-01-18 19:20:50 +01:00
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
_isLoading = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ViewReport(CVCheckDto check)
|
|
|
|
|
{
|
|
|
|
|
if (check.Status == "Completed")
|
|
|
|
|
{
|
|
|
|
|
NavigationManager.NavigateTo($"/report/{check.Id}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string GetScoreBadgeClass(int score)
|
|
|
|
|
{
|
|
|
|
|
return score switch
|
|
|
|
|
{
|
|
|
|
|
> 70 => "bg-success",
|
|
|
|
|
>= 50 => "bg-warning text-dark",
|
|
|
|
|
_ => "bg-danger"
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-01-20 19:15:33 +01:00
|
|
|
|
2026-01-20 19:28:22 +01:00
|
|
|
private async Task ExportToPdf()
|
2026-01-20 19:15:33 +01:00
|
|
|
{
|
|
|
|
|
if (_isExporting) return;
|
|
|
|
|
|
|
|
|
|
_isExporting = true;
|
|
|
|
|
StateHasChanged();
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
2026-01-20 19:28:22 +01:00
|
|
|
var reportDataList = new List<TrueCV.Web.Services.PdfReportData>();
|
2026-01-20 19:15:33 +01:00
|
|
|
foreach (var check in _checks)
|
|
|
|
|
{
|
|
|
|
|
if (check.Status != "Completed") continue;
|
|
|
|
|
|
|
|
|
|
var report = await CVCheckService.GetReportAsync(check.Id, _userId);
|
|
|
|
|
if (report is null) continue;
|
|
|
|
|
|
|
|
|
|
int verifiedCount = 0;
|
|
|
|
|
int unverifiedCount = 0;
|
|
|
|
|
foreach (var v in report.EmploymentVerifications)
|
|
|
|
|
{
|
|
|
|
|
if (v.IsVerified) verifiedCount++;
|
|
|
|
|
else unverifiedCount++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int criticalFlags = 0;
|
|
|
|
|
int warningFlags = 0;
|
|
|
|
|
foreach (var f in report.Flags)
|
|
|
|
|
{
|
|
|
|
|
if (f.Severity == "Critical") criticalFlags++;
|
|
|
|
|
else if (f.Severity == "Warning") warningFlags++;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 19:28:22 +01:00
|
|
|
reportDataList.Add(new TrueCV.Web.Services.PdfReportData
|
|
|
|
|
{
|
|
|
|
|
CandidateName = Path.GetFileNameWithoutExtension(check.OriginalFileName) ?? "Unknown",
|
|
|
|
|
UploadDate = check.CreatedAt,
|
|
|
|
|
Score = report.OverallScore,
|
|
|
|
|
ScoreLabel = report.ScoreLabel,
|
|
|
|
|
VerifiedEmployers = verifiedCount,
|
|
|
|
|
UnverifiedEmployers = unverifiedCount,
|
|
|
|
|
GapMonths = report.TimelineAnalysis.TotalGapMonths,
|
|
|
|
|
OverlapMonths = report.TimelineAnalysis.TotalOverlapMonths,
|
|
|
|
|
CriticalFlags = criticalFlags,
|
|
|
|
|
WarningFlags = warningFlags
|
|
|
|
|
});
|
2026-01-20 19:15:33 +01:00
|
|
|
}
|
|
|
|
|
|
2026-01-20 19:28:22 +01:00
|
|
|
var pdfBytes = PdfReportService.GenerateReport(reportDataList);
|
|
|
|
|
var base64 = Convert.ToBase64String(pdfBytes);
|
|
|
|
|
var fileName = "TrueCV_Report_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".pdf";
|
|
|
|
|
await JSRuntime.InvokeVoidAsync("downloadFile", fileName, base64, "application/pdf");
|
2026-01-20 19:15:33 +01:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-01-20 19:28:22 +01:00
|
|
|
Logger.LogError(ex, "Error exporting PDF");
|
2026-01-20 19:15:33 +01:00
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
_isExporting = false;
|
|
|
|
|
StateHasChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool HasCompletedChecks()
|
|
|
|
|
{
|
|
|
|
|
foreach (var c in _checks)
|
|
|
|
|
{
|
|
|
|
|
if (c.Status == "Completed") return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-01-18 19:20:50 +01:00
|
|
|
}
|