Change report download from JSON to PDF
- Add GenerateSingleReport() method to PdfReportService for individual CV reports - PDF includes: score header, employment verification table, timeline analysis, gaps/overlaps sections, and color-coded flags (critical/warning/info) - Update Report.razor to use PdfReportService instead of JSON serialization - Add TrueCV.Web.Services to _Imports.razor Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
@inject ILogger<Report> Logger
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject IAuditService AuditService
|
||||
@inject PdfReportService PdfReportService
|
||||
|
||||
<PageTitle>Verification Report - TrueCV</PageTitle>
|
||||
|
||||
@@ -603,58 +604,11 @@
|
||||
|
||||
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 candidateName = Path.GetFileNameWithoutExtension(_check.OriginalFileName);
|
||||
var pdfBytes = PdfReportService.GenerateSingleReport(candidateName, _report);
|
||||
|
||||
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");
|
||||
var fileName = $"TrueCV_Report_{candidateName}_{DateTime.Now:yyyyMMdd}.pdf";
|
||||
await DownloadFileAsync(fileName, pdfBytes, "application/pdf");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -662,9 +616,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DownloadFileAsync(string fileName, string content, string contentType)
|
||||
private async Task DownloadFileAsync(string fileName, byte[] bytes, string contentType)
|
||||
{
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(content);
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
await JSRuntime.InvokeVoidAsync("downloadFile", fileName, base64, contentType);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
@using TrueCV.Web
|
||||
@using TrueCV.Web.Components
|
||||
@using TrueCV.Web.Components.Shared
|
||||
@using TrueCV.Web.Services
|
||||
@using TrueCV.Application.Interfaces
|
||||
@using TrueCV.Application.DTOs
|
||||
@using TrueCV.Application.Models
|
||||
|
||||
@@ -1,11 +1,214 @@
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using TrueCV.Application.Models;
|
||||
|
||||
namespace TrueCV.Web.Services;
|
||||
|
||||
public class PdfReportService
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a detailed PDF report for a single CV verification.
|
||||
/// </summary>
|
||||
public byte[] GenerateSingleReport(string candidateName, VeracityReport report)
|
||||
{
|
||||
QuestPDF.Settings.License = LicenseType.Community;
|
||||
|
||||
var document = Document.Create(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Size(PageSizes.A4);
|
||||
page.Margin(40);
|
||||
page.DefaultTextStyle(x => x.FontSize(10));
|
||||
|
||||
page.Header().Element(c => ComposeSingleReportHeader(c, candidateName, report));
|
||||
page.Content().Element(c => ComposeSingleReportContent(c, report));
|
||||
page.Footer().Element(ComposeFooter);
|
||||
});
|
||||
});
|
||||
|
||||
return document.GeneratePdf();
|
||||
}
|
||||
|
||||
private void ComposeSingleReportHeader(IContainer container, string candidateName, VeracityReport report)
|
||||
{
|
||||
var scoreColor = report.OverallScore > 70 ? Colors.Green.Darken1 :
|
||||
(report.OverallScore >= 50 ? Colors.Orange.Darken1 : Colors.Red.Darken1);
|
||||
|
||||
container.Column(column =>
|
||||
{
|
||||
column.Item().Row(row =>
|
||||
{
|
||||
row.RelativeItem().Column(col =>
|
||||
{
|
||||
col.Item().Text("TrueCV").Bold().FontSize(24).FontColor(Colors.Blue.Darken2);
|
||||
col.Item().Text("CV Verification Report").FontSize(14).FontColor(Colors.Grey.Darken1);
|
||||
});
|
||||
row.ConstantItem(100).AlignRight().Column(col =>
|
||||
{
|
||||
col.Item().AlignRight().Text(report.GeneratedAt.ToString("dd MMM yyyy")).FontSize(10).FontColor(Colors.Grey.Medium);
|
||||
col.Item().AlignRight().Text(report.GeneratedAt.ToString("HH:mm")).FontSize(9).FontColor(Colors.Grey.Medium);
|
||||
});
|
||||
});
|
||||
|
||||
column.Item().PaddingTop(15).Row(row =>
|
||||
{
|
||||
row.RelativeItem().Column(col =>
|
||||
{
|
||||
col.Item().Text("Candidate: " + candidateName).Bold().FontSize(12);
|
||||
});
|
||||
row.ConstantItem(120).Border(2).BorderColor(scoreColor).Padding(10).AlignCenter().Column(col =>
|
||||
{
|
||||
col.Item().AlignCenter().Text(report.OverallScore.ToString()).Bold().FontSize(28).FontColor(scoreColor);
|
||||
col.Item().AlignCenter().Text(report.ScoreLabel).FontSize(10).FontColor(scoreColor);
|
||||
});
|
||||
});
|
||||
|
||||
column.Item().PaddingTop(10).LineHorizontal(1).LineColor(Colors.Grey.Lighten2);
|
||||
});
|
||||
}
|
||||
|
||||
private void ComposeSingleReportContent(IContainer container, VeracityReport report)
|
||||
{
|
||||
container.PaddingVertical(15).Column(column =>
|
||||
{
|
||||
// Employment Verification Section
|
||||
column.Item().Text("Employment Verification").Bold().FontSize(12).FontColor(Colors.Blue.Darken2);
|
||||
column.Item().PaddingTop(10).Table(table =>
|
||||
{
|
||||
table.ColumnsDefinition(columns =>
|
||||
{
|
||||
columns.RelativeColumn(3); // Claimed
|
||||
columns.RelativeColumn(2); // Period
|
||||
columns.RelativeColumn(3); // Matched
|
||||
columns.ConstantColumn(45); // Score
|
||||
columns.ConstantColumn(55); // Status
|
||||
});
|
||||
|
||||
table.Header(header =>
|
||||
{
|
||||
header.Cell().Background(Colors.Blue.Darken2).Padding(5).Text("Claimed Employer").Bold().FontSize(9).FontColor(Colors.White);
|
||||
header.Cell().Background(Colors.Blue.Darken2).Padding(5).Text("Period").Bold().FontSize(9).FontColor(Colors.White);
|
||||
header.Cell().Background(Colors.Blue.Darken2).Padding(5).Text("Matched Company").Bold().FontSize(9).FontColor(Colors.White);
|
||||
header.Cell().Background(Colors.Blue.Darken2).Padding(5).AlignCenter().Text("Match").Bold().FontSize(9).FontColor(Colors.White);
|
||||
header.Cell().Background(Colors.Blue.Darken2).Padding(5).AlignCenter().Text("Status").Bold().FontSize(9).FontColor(Colors.White);
|
||||
});
|
||||
|
||||
bool alternate = false;
|
||||
foreach (var emp in report.EmploymentVerifications)
|
||||
{
|
||||
var bgColor = alternate ? Colors.Grey.Lighten4 : Colors.White;
|
||||
var period = emp.ClaimedStartDate.HasValue
|
||||
? $"{emp.ClaimedStartDate.Value:MMM yyyy} - {(emp.ClaimedEndDate.HasValue ? emp.ClaimedEndDate.Value.ToString("MMM yyyy") : "Present")}"
|
||||
: "Not specified";
|
||||
|
||||
table.Cell().Background(bgColor).Padding(4).Text(emp.ClaimedCompany).FontSize(9);
|
||||
table.Cell().Background(bgColor).Padding(4).Text(period).FontSize(8);
|
||||
table.Cell().Background(bgColor).Padding(4).Text(emp.MatchedCompanyName ?? "No match").FontSize(9).FontColor(emp.IsVerified ? Colors.Black : Colors.Grey.Medium);
|
||||
table.Cell().Background(bgColor).Padding(4).AlignCenter().Text(emp.MatchScore + "%").FontSize(9);
|
||||
table.Cell().Background(bgColor).Padding(4).AlignCenter().Text(emp.IsVerified ? "Verified" : "Unverified").FontSize(8)
|
||||
.FontColor(emp.IsVerified ? Colors.Green.Darken1 : Colors.Orange.Darken1);
|
||||
|
||||
alternate = !alternate;
|
||||
}
|
||||
});
|
||||
|
||||
// Timeline Section
|
||||
column.Item().PaddingTop(20).Text("Timeline Analysis").Bold().FontSize(12).FontColor(Colors.Blue.Darken2);
|
||||
column.Item().PaddingTop(10).Row(row =>
|
||||
{
|
||||
row.RelativeItem().Border(1).BorderColor(Colors.Grey.Lighten2).Padding(10).Column(col =>
|
||||
{
|
||||
col.Item().Text("Employment Gaps").Bold().FontSize(10);
|
||||
if (report.TimelineAnalysis.Gaps.Count > 0)
|
||||
{
|
||||
foreach (var gap in report.TimelineAnalysis.Gaps)
|
||||
{
|
||||
col.Item().PaddingTop(5).Text($"{gap.StartDate:MMM yyyy} - {gap.EndDate:MMM yyyy} ({gap.Months} months)").FontSize(9);
|
||||
}
|
||||
col.Item().PaddingTop(5).Text($"Total: {report.TimelineAnalysis.TotalGapMonths} months").Bold().FontSize(9).FontColor(Colors.Orange.Darken1);
|
||||
}
|
||||
else
|
||||
{
|
||||
col.Item().PaddingTop(5).Text("No significant gaps detected").FontSize(9).FontColor(Colors.Green.Darken1);
|
||||
}
|
||||
});
|
||||
|
||||
row.ConstantItem(15);
|
||||
|
||||
row.RelativeItem().Border(1).BorderColor(Colors.Grey.Lighten2).Padding(10).Column(col =>
|
||||
{
|
||||
col.Item().Text("Concurrent Employment").Bold().FontSize(10);
|
||||
if (report.TimelineAnalysis.Overlaps.Count > 0)
|
||||
{
|
||||
foreach (var overlap in report.TimelineAnalysis.Overlaps)
|
||||
{
|
||||
col.Item().PaddingTop(5).Text($"{overlap.Company1} & {overlap.Company2}").FontSize(9);
|
||||
col.Item().Text($" {overlap.OverlapStart:MMM yyyy} - {overlap.OverlapEnd:MMM yyyy} ({overlap.Months} mo)").FontSize(8).FontColor(Colors.Grey.Darken1);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
col.Item().PaddingTop(5).Text("No overlapping positions").FontSize(9).FontColor(Colors.Green.Darken1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Flags Section
|
||||
if (report.Flags.Count > 0)
|
||||
{
|
||||
column.Item().PaddingTop(20).Text("Flags Raised").Bold().FontSize(12).FontColor(Colors.Blue.Darken2);
|
||||
|
||||
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();
|
||||
|
||||
if (criticalFlags.Count > 0)
|
||||
{
|
||||
column.Item().PaddingTop(10).Background(Colors.Red.Lighten5).Border(1).BorderColor(Colors.Red.Lighten3).Padding(10).Column(col =>
|
||||
{
|
||||
col.Item().Text("Critical Issues").Bold().FontSize(10).FontColor(Colors.Red.Darken1);
|
||||
foreach (var flag in criticalFlags)
|
||||
{
|
||||
col.Item().PaddingTop(5).Text($"• {flag.Title} ({flag.ScoreImpact} pts)").FontSize(9).Bold();
|
||||
col.Item().Text($" {flag.Description}").FontSize(8).FontColor(Colors.Grey.Darken1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (warningFlags.Count > 0)
|
||||
{
|
||||
column.Item().PaddingTop(10).Background(Colors.Orange.Lighten5).Border(1).BorderColor(Colors.Orange.Lighten3).Padding(10).Column(col =>
|
||||
{
|
||||
col.Item().Text("Warnings").Bold().FontSize(10).FontColor(Colors.Orange.Darken1);
|
||||
foreach (var flag in warningFlags)
|
||||
{
|
||||
col.Item().PaddingTop(5).Text($"• {flag.Title} ({flag.ScoreImpact} pts)").FontSize(9).Bold();
|
||||
col.Item().Text($" {flag.Description}").FontSize(8).FontColor(Colors.Grey.Darken1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (infoFlags.Count > 0)
|
||||
{
|
||||
column.Item().PaddingTop(10).Background(Colors.Blue.Lighten5).Border(1).BorderColor(Colors.Blue.Lighten3).Padding(10).Column(col =>
|
||||
{
|
||||
col.Item().Text("Information").Bold().FontSize(10).FontColor(Colors.Blue.Darken1);
|
||||
foreach (var flag in infoFlags)
|
||||
{
|
||||
col.Item().PaddingTop(5).Text($"• {flag.Title} ({flag.ScoreImpact} pts)").FontSize(9).Bold();
|
||||
col.Item().Text($" {flag.Description}").FontSize(8).FontColor(Colors.Grey.Darken1);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a summary PDF report for multiple CV verifications (batch report).
|
||||
/// </summary>
|
||||
public byte[] GenerateReport(List<PdfReportData> data)
|
||||
{
|
||||
QuestPDF.Settings.License = LicenseType.Community;
|
||||
|
||||
Reference in New Issue
Block a user