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 ILogger<Report> Logger
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
@inject IAuditService AuditService
|
@inject IAuditService AuditService
|
||||||
|
@inject PdfReportService PdfReportService
|
||||||
|
|
||||||
<PageTitle>Verification Report - TrueCV</PageTitle>
|
<PageTitle>Verification Report - TrueCV</PageTitle>
|
||||||
|
|
||||||
@@ -603,58 +604,11 @@
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var reportData = new
|
var candidateName = Path.GetFileNameWithoutExtension(_check.OriginalFileName);
|
||||||
{
|
var pdfBytes = PdfReportService.GenerateSingleReport(candidateName, _report);
|
||||||
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
|
var fileName = $"TrueCV_Report_{candidateName}_{DateTime.Now:yyyyMMdd}.pdf";
|
||||||
{
|
await DownloadFileAsync(fileName, pdfBytes, "application/pdf");
|
||||||
WriteIndented = true
|
|
||||||
});
|
|
||||||
|
|
||||||
var fileName = $"TrueCV_Report_{Path.GetFileNameWithoutExtension(_check.OriginalFileName)}_{DateTime.Now:yyyyMMdd}.json";
|
|
||||||
await DownloadFileAsync(fileName, json, "application/json");
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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);
|
var base64 = Convert.ToBase64String(bytes);
|
||||||
await JSRuntime.InvokeVoidAsync("downloadFile", fileName, base64, contentType);
|
await JSRuntime.InvokeVoidAsync("downloadFile", fileName, base64, contentType);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
@using TrueCV.Web
|
@using TrueCV.Web
|
||||||
@using TrueCV.Web.Components
|
@using TrueCV.Web.Components
|
||||||
@using TrueCV.Web.Components.Shared
|
@using TrueCV.Web.Components.Shared
|
||||||
|
@using TrueCV.Web.Services
|
||||||
@using TrueCV.Application.Interfaces
|
@using TrueCV.Application.Interfaces
|
||||||
@using TrueCV.Application.DTOs
|
@using TrueCV.Application.DTOs
|
||||||
@using TrueCV.Application.Models
|
@using TrueCV.Application.Models
|
||||||
|
|||||||
@@ -1,11 +1,214 @@
|
|||||||
using QuestPDF.Fluent;
|
using QuestPDF.Fluent;
|
||||||
using QuestPDF.Helpers;
|
using QuestPDF.Helpers;
|
||||||
using QuestPDF.Infrastructure;
|
using QuestPDF.Infrastructure;
|
||||||
|
using TrueCV.Application.Models;
|
||||||
|
|
||||||
namespace TrueCV.Web.Services;
|
namespace TrueCV.Web.Services;
|
||||||
|
|
||||||
public class PdfReportService
|
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)
|
public byte[] GenerateReport(List<PdfReportData> data)
|
||||||
{
|
{
|
||||||
QuestPDF.Settings.License = LicenseType.Community;
|
QuestPDF.Settings.License = LicenseType.Community;
|
||||||
|
|||||||
Reference in New Issue
Block a user