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

@@ -1,11 +1,13 @@
@page "/report/{Id:guid}"
@attribute [Authorize]
@rendermode InteractiveServer
@implements IDisposable
@inject ICVCheckService CVCheckService
@inject NavigationManager NavigationManager
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject ILogger<Report> Logger
@inject IJSRuntime JSRuntime
<PageTitle>Verification Report - TrueCV</PageTitle>
@@ -96,7 +98,7 @@
</p>
</div>
<div class="col-auto">
<button class="btn btn-outline-primary" @onclick="DownloadReport">
<button class="btn btn-outline-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-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"/>
@@ -468,10 +470,48 @@
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()
@@ -524,9 +564,76 @@
await LoadData();
}
private void DownloadReport()
private async Task DownloadReport()
{
// TODO: Implement report download functionality
if (_report is null || _check is null) return;
try
{
var reportData = new
{
CandidateName = _check.OriginalFileName,
GeneratedAt = _report.GeneratedAt,
OverallScore = _report.OverallScore,
ScoreLabel = _report.ScoreLabel,
EmploymentVerifications = _report.EmploymentVerifications.Select(v => new
{
v.ClaimedCompany,
ClaimedStartDate = v.ClaimedStartDate?.ToString("yyyy-MM"),
ClaimedEndDate = v.ClaimedEndDate?.ToString("yyyy-MM"),
v.MatchedCompanyName,
v.MatchedCompanyNumber,
v.MatchScore,
v.IsVerified,
v.VerificationNotes
}),
TimelineAnalysis = new
{
_report.TimelineAnalysis.TotalGapMonths,
_report.TimelineAnalysis.TotalOverlapMonths,
Gaps = _report.TimelineAnalysis.Gaps.Select(g => new
{
StartDate = g.StartDate.ToString("yyyy-MM"),
EndDate = g.EndDate.ToString("yyyy-MM"),
g.Months
}),
Overlaps = _report.TimelineAnalysis.Overlaps.Select(o => new
{
o.Company1,
o.Company2,
OverlapStart = o.OverlapStart.ToString("yyyy-MM"),
OverlapEnd = o.OverlapEnd.ToString("yyyy-MM"),
o.Months
})
},
Flags = _report.Flags.Select(f => new
{
f.Severity,
f.Title,
f.Description,
f.ScoreImpact
})
};
var json = System.Text.Json.JsonSerializer.Serialize(reportData, new System.Text.Json.JsonSerializerOptions
{
WriteIndented = true
});
var fileName = $"TrueCV_Report_{Path.GetFileNameWithoutExtension(_check.OriginalFileName)}_{DateTime.Now:yyyyMMdd}.json";
await DownloadFileAsync(fileName, json, "application/json");
}
catch (Exception ex)
{
Logger.LogError(ex, "Error downloading report");
}
}
private async Task DownloadFileAsync(string fileName, string content, string contentType)
{
var bytes = System.Text.Encoding.UTF8.GetBytes(content);
var base64 = Convert.ToBase64String(bytes);
await JSRuntime.InvokeVoidAsync("downloadFile", fileName, base64, contentType);
}
private static string GetScoreColorClass(int score)