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
This commit is contained in:
parent
5bd0e19f06
commit
ea237183f9
|
|
@ -1715,7 +1715,7 @@ public enum CashBookEntryType { Income = 0, Expense = 1 }
|
||||||
| ✓ | K14 | Web | Categories UI | 2 |
|
| ✓ | K14 | Web | Categories UI | 2 |
|
||||||
| ✓ | K15 | Web | Reports UI | 2 |
|
| ✓ | K15 | Web | Reports UI | 2 |
|
||||||
| ✓ | K16 | Application | Excel Export (ClosedXML) | 3 |
|
| ✓ | K16 | Application | Excel Export (ClosedXML) | 3 |
|
||||||
| ☐ | K17 | Application | PDF Export (QuestPDF) | 1 |
|
| ✓ | K17 | Application | PDF Export (QuestPDF) | 1 |
|
||||||
| ☐ | K18 | Web | Export Controller | 1 |
|
| ☐ | K18 | Web | Export Controller | 1 |
|
||||||
| ☐ | K19 | Web | Membership Fees Feature | 2 |
|
| ☐ | K19 | Web | Membership Fees Feature | 2 |
|
||||||
| ☐ | K20 | Web | Club Settings Extension | 3 |
|
| ☐ | K20 | Web | Club Settings Extension | 3 |
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
<PackageReference Include="AutoMapper" Version="15.1.0" />
|
<PackageReference Include="AutoMapper" Version="15.1.0" />
|
||||||
<PackageReference Include="ClosedXML" Version="0.105.0" />
|
<PackageReference Include="ClosedXML" Version="0.105.0" />
|
||||||
<PackageReference Include="FluentValidation" Version="12.1.1" />
|
<PackageReference Include="FluentValidation" Version="12.1.1" />
|
||||||
|
<PackageReference Include="QuestPDF" Version="2025.12.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@ using ClosedXML.Excel;
|
||||||
using Koogle.Application.DTOs;
|
using Koogle.Application.DTOs;
|
||||||
using Koogle.Application.Interfaces;
|
using Koogle.Application.Interfaces;
|
||||||
using Koogle.Domain.Enums;
|
using Koogle.Domain.Enums;
|
||||||
|
using QuestPDF.Fluent;
|
||||||
|
using QuestPDF.Helpers;
|
||||||
|
using QuestPDF.Infrastructure;
|
||||||
|
|
||||||
namespace Koogle.Application.Services;
|
namespace Koogle.Application.Services;
|
||||||
|
|
||||||
|
|
@ -10,6 +13,12 @@ namespace Koogle.Application.Services;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CashBookExportService : ICashBookExportService
|
public class CashBookExportService : ICashBookExportService
|
||||||
{
|
{
|
||||||
|
static CashBookExportService()
|
||||||
|
{
|
||||||
|
// QuestPDF Community License
|
||||||
|
QuestPDF.Settings.License = LicenseType.Community;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<byte[]> ExportToExcelAsync(CashBookReportDto report, string clubName)
|
public Task<byte[]> ExportToExcelAsync(CashBookReportDto report, string clubName)
|
||||||
{
|
{
|
||||||
|
|
@ -27,8 +36,153 @@ public class CashBookExportService : ICashBookExportService
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<byte[]> ExportToPdfAsync(CashBookReportDto report, string clubName)
|
public Task<byte[]> ExportToPdfAsync(CashBookReportDto report, string clubName)
|
||||||
{
|
{
|
||||||
// Will be implemented in K17 with QuestPDF
|
var document = Document.Create(container =>
|
||||||
throw new NotImplementedException("PDF export will be available in a future version.");
|
{
|
||||||
|
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)
|
private static void CreateSummarySheet(XLWorkbook workbook, CashBookReportDto report, string clubName)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue