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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user