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)