Add multi-file upload, auto-refresh reports, and CSV export

- Allow recruiters to upload multiple CVs at once with batch processing
- Add auto-refresh polling on Report page during CV processing
- Add CSV export button on Dashboard for completed check summaries
- Update logo and reset to Bootstrap default styling

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-20 19:15:33 +01:00
parent 4de233f56d
commit 7d6d5f12ea
12 changed files with 376 additions and 265 deletions

View File

@@ -6,6 +6,7 @@
@inject NavigationManager NavigationManager
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject ILogger<Dashboard> Logger
@inject IJSRuntime JSRuntime
<PageTitle>Dashboard - TrueCV</PageTitle>
@@ -15,12 +16,28 @@
<h1 class="fw-bold mb-1">Dashboard</h1>
<p class="text-muted mb-0">View and manage your CV verification checks</p>
</div>
<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 class="d-flex gap-2">
<button class="btn btn-outline-secondary" @onclick="ExportToCsv" disabled="@(_isExporting || !HasCompletedChecks())">
@if (_isExporting)
{
<span class="spinner-border spinner-border-sm me-1" role="status"></span>
}
else
{
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-download 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 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>
}
Export CSV
</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>
</div>
@if (_isLoading)
@@ -229,7 +246,9 @@
@code {
private List<CVCheckDto> _checks = [];
private bool _isLoading = true;
private bool _isExporting;
private string? _errorMessage;
private Guid _userId;
protected override async Task OnInitializedAsync()
{
@@ -246,13 +265,13 @@
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var userIdClaim = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out _userId))
{
_errorMessage = "Unable to identify user. Please log in again.";
return;
}
_checks = await CVCheckService.GetUserChecksAsync(userId);
_checks = await CVCheckService.GetUserChecksAsync(_userId);
}
catch (Exception ex)
{
@@ -282,4 +301,78 @@
_ => "bg-danger"
};
}
private async Task ExportToCsv()
{
if (_isExporting) return;
_isExporting = true;
StateHasChanged();
try
{
var csvBuilder = new System.Text.StringBuilder();
csvBuilder.AppendLine("Candidate,Upload Date,Score,Score Label,Employers Verified,Employers Unverified,Gap Months,Overlap Months,Critical Flags,Warning Flags");
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++;
}
var candidateName = EscapeCsv(Path.GetFileNameWithoutExtension(check.OriginalFileName));
var uploadDate = check.CreatedAt.ToString("yyyy-MM-dd HH:mm");
csvBuilder.AppendLine(candidateName + "," + uploadDate + "," + report.OverallScore + "," + EscapeCsv(report.ScoreLabel) + "," + verifiedCount + "," + unverifiedCount + "," + report.TimelineAnalysis.TotalGapMonths + "," + report.TimelineAnalysis.TotalOverlapMonths + "," + criticalFlags + "," + warningFlags);
}
var fileName = "TrueCV_Report_Summary_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".csv";
await JSRuntime.InvokeVoidAsync("downloadCsvFile", fileName, csvBuilder.ToString());
}
catch (Exception ex)
{
Logger.LogError(ex, "Error exporting CSV");
}
finally
{
_isExporting = false;
StateHasChanged();
}
}
private static string EscapeCsv(string? field)
{
if (string.IsNullOrEmpty(field)) return "";
if (field.Contains(",") || field.Contains("\"") || field.Contains("\n"))
{
return "\"" + field.Replace("\"", "\"\"") + "\"";
}
return field;
}
private bool HasCompletedChecks()
{
foreach (var c in _checks)
{
if (c.Status == "Completed") return true;
}
return false;
}
}