diff --git a/src/TrueCV.Web/Components/Pages/Dashboard.razor b/src/TrueCV.Web/Components/Pages/Dashboard.razor index 8ad2bd4..f100ec0 100644 --- a/src/TrueCV.Web/Components/Pages/Dashboard.razor +++ b/src/TrueCV.Web/Components/Pages/Dashboard.razor @@ -7,6 +7,7 @@ @inject AuthenticationStateProvider AuthenticationStateProvider @inject ILogger Logger @inject IJSRuntime JSRuntime +@inject TrueCV.Web.Services.PdfReportService PdfReportService Dashboard - TrueCV @@ -17,19 +18,19 @@

View and manage your CV verification checks

- @@ -302,7 +303,7 @@ }; } - private async Task ExportToCsv() + private async Task ExportToPdf() { if (_isExporting) return; @@ -311,9 +312,7 @@ 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"); - + var reportDataList = new List(); foreach (var check in _checks) { if (check.Status != "Completed") continue; @@ -337,18 +336,29 @@ 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); + reportDataList.Add(new TrueCV.Web.Services.PdfReportData + { + CandidateName = Path.GetFileNameWithoutExtension(check.OriginalFileName) ?? "Unknown", + UploadDate = check.CreatedAt, + Score = report.OverallScore, + ScoreLabel = report.ScoreLabel, + VerifiedEmployers = verifiedCount, + UnverifiedEmployers = unverifiedCount, + GapMonths = report.TimelineAnalysis.TotalGapMonths, + OverlapMonths = report.TimelineAnalysis.TotalOverlapMonths, + CriticalFlags = criticalFlags, + WarningFlags = warningFlags + }); } - var fileName = "TrueCV_Report_Summary_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".csv"; - await JSRuntime.InvokeVoidAsync("downloadCsvFile", fileName, csvBuilder.ToString()); + var pdfBytes = PdfReportService.GenerateReport(reportDataList); + var base64 = Convert.ToBase64String(pdfBytes); + var fileName = "TrueCV_Report_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".pdf"; + await JSRuntime.InvokeVoidAsync("downloadFile", fileName, base64, "application/pdf"); } catch (Exception ex) { - Logger.LogError(ex, "Error exporting CSV"); + Logger.LogError(ex, "Error exporting PDF"); } finally { @@ -357,16 +367,6 @@ } } - 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) diff --git a/src/TrueCV.Web/Program.cs b/src/TrueCV.Web/Program.cs index 4a4a906..7d183ec 100644 --- a/src/TrueCV.Web/Program.cs +++ b/src/TrueCV.Web/Program.cs @@ -7,6 +7,7 @@ using TrueCV.Infrastructure.Data; using TrueCV.Infrastructure.Identity; using TrueCV.Web; using TrueCV.Web.Components; +using TrueCV.Web.Services; // Configure Serilog Log.Logger = new LoggerConfiguration() @@ -32,6 +33,9 @@ try // Add Infrastructure services (DbContext, Hangfire, HttpClients, Services) builder.Services.AddInfrastructure(builder.Configuration); + // Add Web services + builder.Services.AddScoped(); + // Add Identity with secure password requirements builder.Services.AddIdentity>(options => { diff --git a/src/TrueCV.Web/Services/PdfReportService.cs b/src/TrueCV.Web/Services/PdfReportService.cs new file mode 100644 index 0000000..03e6461 --- /dev/null +++ b/src/TrueCV.Web/Services/PdfReportService.cs @@ -0,0 +1,133 @@ +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace TrueCV.Web.Services; + +public class PdfReportService +{ + public byte[] GenerateReport(List 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 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; } +} diff --git a/src/TrueCV.Web/TrueCV.Web.csproj b/src/TrueCV.Web/TrueCV.Web.csproj index 4399483..fb92885 100644 --- a/src/TrueCV.Web/TrueCV.Web.csproj +++ b/src/TrueCV.Web/TrueCV.Web.csproj @@ -11,6 +11,7 @@ all +