Replace CSV export with PDF report generation

- Add QuestPDF library for professional PDF generation
- Create PdfReportService with formatted table layout
- Export includes score (color-coded), verified employers, gaps, and flags
- Report has header, footer with page numbers, and alternating row colors

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-20 19:28:22 +01:00
parent 7d6d5f12ea
commit acf4d96fae
4 changed files with 164 additions and 26 deletions

View File

@@ -0,0 +1,133 @@
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace TrueCV.Web.Services;
public class PdfReportService
{
public byte[] GenerateReport(List<PdfReportData> data)
{
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(ComposeHeader);
page.Content().Element(c => ComposeContent(c, data));
page.Footer().Element(ComposeFooter);
});
});
return document.GeneratePdf();
}
private void ComposeHeader(IContainer container)
{
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 Summary").FontSize(14).FontColor(Colors.Grey.Darken1);
});
row.ConstantItem(100).AlignRight().Text(DateTime.Now.ToString("dd MMM yyyy")).FontSize(10).FontColor(Colors.Grey.Medium);
});
column.Item().PaddingTop(10).LineHorizontal(1).LineColor(Colors.Grey.Lighten2);
});
}
private void ComposeContent(IContainer container, List<PdfReportData> data)
{
container.PaddingVertical(20).Column(column =>
{
column.Item().Text("Summary of " + data.Count + " Verified CVs").Bold().FontSize(12);
column.Item().PaddingTop(15);
column.Item().Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn(3);
columns.RelativeColumn(2);
columns.ConstantColumn(50);
columns.RelativeColumn(2);
columns.ConstantColumn(45);
columns.ConstantColumn(45);
columns.ConstantColumn(45);
columns.ConstantColumn(45);
});
table.Header(header =>
{
header.Cell().Background(Colors.Blue.Darken2).Padding(5).Text("Candidate").Bold().FontColor(Colors.White);
header.Cell().Background(Colors.Blue.Darken2).Padding(5).Text("Date").Bold().FontColor(Colors.White);
header.Cell().Background(Colors.Blue.Darken2).Padding(5).AlignCenter().Text("Score").Bold().FontColor(Colors.White);
header.Cell().Background(Colors.Blue.Darken2).Padding(5).Text("Rating").Bold().FontColor(Colors.White);
header.Cell().Background(Colors.Blue.Darken2).Padding(5).AlignCenter().Text("Verified").Bold().FontColor(Colors.White);
header.Cell().Background(Colors.Blue.Darken2).Padding(5).AlignCenter().Text("Gaps").Bold().FontColor(Colors.White);
header.Cell().Background(Colors.Blue.Darken2).Padding(5).AlignCenter().Text("Critical").Bold().FontColor(Colors.White);
header.Cell().Background(Colors.Blue.Darken2).Padding(5).AlignCenter().Text("Warnings").Bold().FontColor(Colors.White);
});
bool alternate = false;
foreach (var item in data)
{
var bgColor = alternate ? Colors.Grey.Lighten4 : Colors.White;
var scoreColor = item.Score > 70 ? Colors.Green.Darken1 : (item.Score >= 50 ? Colors.Orange.Darken1 : Colors.Red.Darken1);
table.Cell().Background(bgColor).Padding(5).Text(item.CandidateName);
table.Cell().Background(bgColor).Padding(5).Text(item.UploadDate.ToString("dd MMM yy"));
table.Cell().Background(bgColor).Padding(5).AlignCenter().Text(item.Score.ToString()).Bold().FontColor(scoreColor);
table.Cell().Background(bgColor).Padding(5).Text(item.ScoreLabel);
table.Cell().Background(bgColor).Padding(5).AlignCenter().Text(item.VerifiedEmployers + "/" + (item.VerifiedEmployers + item.UnverifiedEmployers));
table.Cell().Background(bgColor).Padding(5).AlignCenter().Text(item.GapMonths + "mo");
table.Cell().Background(bgColor).Padding(5).AlignCenter().Text(item.CriticalFlags.ToString()).FontColor(item.CriticalFlags > 0 ? Colors.Red.Darken1 : Colors.Grey.Medium);
table.Cell().Background(bgColor).Padding(5).AlignCenter().Text(item.WarningFlags.ToString()).FontColor(item.WarningFlags > 0 ? Colors.Orange.Darken1 : Colors.Grey.Medium);
alternate = !alternate;
}
});
});
}
private void ComposeFooter(IContainer container)
{
container.Column(column =>
{
column.Item().LineHorizontal(1).LineColor(Colors.Grey.Lighten2);
column.Item().PaddingTop(10).Row(row =>
{
row.RelativeItem().Text("Generated by TrueCV - CV Verification Platform").FontSize(8).FontColor(Colors.Grey.Medium);
row.RelativeItem().AlignRight().Text(x =>
{
x.Span("Page ").FontSize(8).FontColor(Colors.Grey.Medium);
x.CurrentPageNumber().FontSize(8).FontColor(Colors.Grey.Medium);
x.Span(" of ").FontSize(8).FontColor(Colors.Grey.Medium);
x.TotalPages().FontSize(8).FontColor(Colors.Grey.Medium);
});
});
});
}
}
public class PdfReportData
{
public string CandidateName { get; set; } = "";
public DateTime UploadDate { get; set; }
public int Score { get; set; }
public string ScoreLabel { get; set; } = "";
public int VerifiedEmployers { get; set; }
public int UnverifiedEmployers { get; set; }
public int GapMonths { get; set; }
public int OverlapMonths { get; set; }
public int CriticalFlags { get; set; }
public int WarningFlags { get; set; }
}