From ea237183f9ce23804faa023267015adb550bdffc Mon Sep 17 00:00:00 2001 From: beo3000 Date: Sat, 3 Jan 2026 22:03:17 +0100 Subject: [PATCH] K17 fertig. Erweitert: src/Koogle.Application/Services/CashBookExportService.cs PDF-Struktur (QuestPDF 2025.12.1): - Header: Titel, Vereinsname, Berichtszeitraum - Zusammenfassung: Salden-Tabelle (Anfang/Einnahmen/Ausgaben/End) - Kategorien: Einnahmen + Ausgaben mit farbigen Tabellen - Footer: Erstelldatum + Seitenzahlen --- docs/IMPLEMENTATION_PLAN.md | 2 +- .../Koogle.Application.csproj | 1 + .../Services/CashBookExportService.cs | 158 +++++++++++++++++- 3 files changed, 158 insertions(+), 3 deletions(-) diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index 1ff62ee..fb82152 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -1715,7 +1715,7 @@ public enum CashBookEntryType { Income = 0, Expense = 1 } | ✓ | K14 | Web | Categories UI | 2 | | ✓ | K15 | Web | Reports UI | 2 | | ✓ | K16 | Application | Excel Export (ClosedXML) | 3 | -| ☐ | K17 | Application | PDF Export (QuestPDF) | 1 | +| ✓ | K17 | Application | PDF Export (QuestPDF) | 1 | | ☐ | K18 | Web | Export Controller | 1 | | ☐ | K19 | Web | Membership Fees Feature | 2 | | ☐ | K20 | Web | Club Settings Extension | 3 | diff --git a/src/Koogle.Application/Koogle.Application.csproj b/src/Koogle.Application/Koogle.Application.csproj index 94c86af..2d405f0 100644 --- a/src/Koogle.Application/Koogle.Application.csproj +++ b/src/Koogle.Application/Koogle.Application.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Koogle.Application/Services/CashBookExportService.cs b/src/Koogle.Application/Services/CashBookExportService.cs index f807d38..a248b6f 100644 --- a/src/Koogle.Application/Services/CashBookExportService.cs +++ b/src/Koogle.Application/Services/CashBookExportService.cs @@ -2,6 +2,9 @@ using ClosedXML.Excel; using Koogle.Application.DTOs; using Koogle.Application.Interfaces; using Koogle.Domain.Enums; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; namespace Koogle.Application.Services; @@ -10,6 +13,12 @@ namespace Koogle.Application.Services; /// public class CashBookExportService : ICashBookExportService { + static CashBookExportService() + { + // QuestPDF Community License + QuestPDF.Settings.License = LicenseType.Community; + } + /// public Task ExportToExcelAsync(CashBookReportDto report, string clubName) { @@ -27,8 +36,153 @@ public class CashBookExportService : ICashBookExportService /// public Task ExportToPdfAsync(CashBookReportDto report, string clubName) { - // Will be implemented in K17 with QuestPDF - throw new NotImplementedException("PDF export will be available in a future version."); + 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 => ComposeHeader(c, report, clubName)); + page.Content().Element(c => ComposeContent(c, report)); + page.Footer().Element(ComposeFooter); + }); + }); + + var bytes = document.GeneratePdf(); + return Task.FromResult(bytes); + } + + private static void ComposeHeader(IContainer container, CashBookReportDto report, string clubName) + { + container.Column(column => + { + column.Item().Text("Kassenbericht").FontSize(20).Bold(); + column.Item().Text(clubName).FontSize(14); + column.Item().Text($"Zeitraum: {report.ReportStart:dd.MM.yyyy} - {report.ReportEnd:dd.MM.yyyy}").FontSize(11); + column.Item().PaddingBottom(10); + }); + } + + private static void ComposeContent(IContainer container, CashBookReportDto report) + { + container.Column(column => + { + // Summary section + column.Item().Text("Zusammenfassung").FontSize(14).Bold(); + column.Item().PaddingVertical(5).Table(table => + { + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(2); + columns.RelativeColumn(1); + }); + + table.Cell().Text("Anfangssaldo"); + table.Cell().AlignRight().Text(report.OpeningBalance.ToString("C")); + + table.Cell().Text("Einnahmen"); + table.Cell().AlignRight().Text($"+{report.TotalIncome:C}").FontColor(Colors.Green.Darken2); + + table.Cell().Text("Ausgaben"); + table.Cell().AlignRight().Text($"-{report.TotalExpense:C}").FontColor(Colors.Red.Darken2); + + table.Cell().Text("Differenz"); + table.Cell().AlignRight().Text((report.TotalIncome - report.TotalExpense).ToString("C")); + + table.Cell().Border(1).BorderColor(Colors.Grey.Lighten1).Padding(2).Text("Endsaldo").Bold(); + table.Cell().Border(1).BorderColor(Colors.Grey.Lighten1).Padding(2).AlignRight().Text(report.ClosingBalance.ToString("C")).Bold(); + }); + + column.Item().PaddingTop(15); + + // Income by category + if (report.IncomeByCategory.Count > 0) + { + column.Item().Text("Einnahmen nach Kategorie").FontSize(12).Bold(); + column.Item().PaddingVertical(5).Table(table => + { + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(3); + columns.RelativeColumn(1); + columns.RelativeColumn(1); + }); + + table.Header(header => + { + header.Cell().Background(Colors.Green.Lighten4).Padding(3).Text("Kategorie").Bold(); + header.Cell().Background(Colors.Green.Lighten4).Padding(3).AlignRight().Text("Anzahl").Bold(); + header.Cell().Background(Colors.Green.Lighten4).Padding(3).AlignRight().Text("Summe").Bold(); + }); + + foreach (var cat in report.IncomeByCategory.OrderByDescending(c => c.Total)) + { + table.Cell().Padding(2).Text(cat.CategoryName); + table.Cell().Padding(2).AlignRight().Text(cat.Count.ToString()); + table.Cell().Padding(2).AlignRight().Text(cat.Total.ToString("C")); + } + + table.Cell().Border(1).Padding(2).Text("Gesamt").Bold(); + table.Cell().Border(1).Padding(2).AlignRight().Text(report.IncomeByCategory.Sum(c => c.Count).ToString()).Bold(); + table.Cell().Border(1).Padding(2).AlignRight().Text(report.TotalIncome.ToString("C")).Bold(); + }); + } + + column.Item().PaddingTop(10); + + // Expense by category + if (report.ExpenseByCategory.Count > 0) + { + column.Item().Text("Ausgaben nach Kategorie").FontSize(12).Bold(); + column.Item().PaddingVertical(5).Table(table => + { + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(3); + columns.RelativeColumn(1); + columns.RelativeColumn(1); + }); + + table.Header(header => + { + header.Cell().Background(Colors.Red.Lighten4).Padding(3).Text("Kategorie").Bold(); + header.Cell().Background(Colors.Red.Lighten4).Padding(3).AlignRight().Text("Anzahl").Bold(); + header.Cell().Background(Colors.Red.Lighten4).Padding(3).AlignRight().Text("Summe").Bold(); + }); + + foreach (var cat in report.ExpenseByCategory.OrderByDescending(c => c.Total)) + { + table.Cell().Padding(2).Text(cat.CategoryName); + table.Cell().Padding(2).AlignRight().Text(cat.Count.ToString()); + table.Cell().Padding(2).AlignRight().Text(cat.Total.ToString("C")); + } + + table.Cell().Border(1).Padding(2).Text("Gesamt").Bold(); + table.Cell().Border(1).Padding(2).AlignRight().Text(report.ExpenseByCategory.Sum(c => c.Count).ToString()).Bold(); + table.Cell().Border(1).Padding(2).AlignRight().Text(report.TotalExpense.ToString("C")).Bold(); + }); + } + + column.Item().PaddingTop(15); + + // Statistics + column.Item().Text($"Anzahl Buchungen: {report.Entries.Count}").FontSize(10); + }); + } + + private static void ComposeFooter(IContainer container) + { + container.AlignCenter().Text(text => + { + text.Span("Erstellt am "); + text.Span(DateTime.Now.ToString("dd.MM.yyyy HH:mm")); + text.Span(" - Seite "); + text.CurrentPageNumber(); + text.Span(" von "); + text.TotalPages(); + }); } private static void CreateSummarySheet(XLWorkbook workbook, CashBookReportDto report, string clubName)