From 5bd0e19f06d1a31d42db9e434df7a9a423922384 Mon Sep 17 00:00:00 2001 From: beo3000 Date: Sat, 3 Jan 2026 21:59:13 +0100 Subject: [PATCH] K16 fertig. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Erstellte Dateien: - src/Koogle.Application/Interfaces/ICashBookExportService.cs - Interface mit ExportToExcelAsync + ExportToPdfAsync - src/Koogle.Application/Services/CashBookExportService.cs - Excel-Export via ClosedXML Excel-Struktur (3 Sheets): 1. Zusammenfassung - Salden, Summen, Statistik 2. Kategorien - Einnahmen/Ausgaben nach Kategorie 3. Buchungen - Alle Einträge mit Summenzeile NuGet: ClosedXML 0.105.0 hinzugefügt. --- docs/IMPLEMENTATION_PLAN.md | 2 +- src/Koogle.Application/DependencyInjection.cs | 1 + .../Interfaces/ICashBookExportService.cs | 25 ++ .../Koogle.Application.csproj | 1 + .../Services/CashBookExportService.cs | 216 ++++++++++++++++++ 5 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 src/Koogle.Application/Interfaces/ICashBookExportService.cs create mode 100644 src/Koogle.Application/Services/CashBookExportService.cs diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index 33de4b1..1ff62ee 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -1714,7 +1714,7 @@ public enum CashBookEntryType { Income = 0, Expense = 1 } | ✓ | K13 | Web | CashBook UI | 3 | | ✓ | K14 | Web | Categories 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 | | ☐ | K18 | Web | Export Controller | 1 | | ☐ | K19 | Web | Membership Fees Feature | 2 | diff --git a/src/Koogle.Application/DependencyInjection.cs b/src/Koogle.Application/DependencyInjection.cs index 80eb552..e4435cd 100644 --- a/src/Koogle.Application/DependencyInjection.cs +++ b/src/Koogle.Application/DependencyInjection.cs @@ -42,6 +42,7 @@ namespace Koogle.Application services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // Note: Game types are registered in Koogle.Web where Blazor components are defined // Use services.AddGameType() to register game types diff --git a/src/Koogle.Application/Interfaces/ICashBookExportService.cs b/src/Koogle.Application/Interfaces/ICashBookExportService.cs new file mode 100644 index 0000000..c81598a --- /dev/null +++ b/src/Koogle.Application/Interfaces/ICashBookExportService.cs @@ -0,0 +1,25 @@ +using Koogle.Application.DTOs; + +namespace Koogle.Application.Interfaces; + +/// +/// Service for exporting cash book reports to various formats. +/// +public interface ICashBookExportService +{ + /// + /// Exports a cash book report to Excel format. + /// + /// The report data to export. + /// Name of the club for the header. + /// Excel file as byte array. + Task ExportToExcelAsync(CashBookReportDto report, string clubName); + + /// + /// Exports a cash book report to PDF format. + /// + /// The report data to export. + /// Name of the club for the header. + /// PDF file as byte array. + Task ExportToPdfAsync(CashBookReportDto report, string clubName); +} diff --git a/src/Koogle.Application/Koogle.Application.csproj b/src/Koogle.Application/Koogle.Application.csproj index 7cc4dc4..94c86af 100644 --- a/src/Koogle.Application/Koogle.Application.csproj +++ b/src/Koogle.Application/Koogle.Application.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Koogle.Application/Services/CashBookExportService.cs b/src/Koogle.Application/Services/CashBookExportService.cs new file mode 100644 index 0000000..f807d38 --- /dev/null +++ b/src/Koogle.Application/Services/CashBookExportService.cs @@ -0,0 +1,216 @@ +using ClosedXML.Excel; +using Koogle.Application.DTOs; +using Koogle.Application.Interfaces; +using Koogle.Domain.Enums; + +namespace Koogle.Application.Services; + +/// +/// Service for exporting cash book reports to Excel and PDF formats. +/// +public class CashBookExportService : ICashBookExportService +{ + /// + public Task ExportToExcelAsync(CashBookReportDto report, string clubName) + { + using var workbook = new XLWorkbook(); + + CreateSummarySheet(workbook, report, clubName); + CreateCategorySheet(workbook, report); + CreateEntriesSheet(workbook, report); + + using var stream = new MemoryStream(); + workbook.SaveAs(stream); + return Task.FromResult(stream.ToArray()); + } + + /// + 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."); + } + + private static void CreateSummarySheet(XLWorkbook workbook, CashBookReportDto report, string clubName) + { + var ws = workbook.Worksheets.Add("Zusammenfassung"); + + // Header + ws.Cell("A1").Value = "Kassenbericht"; + ws.Cell("A1").Style.Font.Bold = true; + ws.Cell("A1").Style.Font.FontSize = 16; + + ws.Cell("A2").Value = clubName; + ws.Cell("A2").Style.Font.FontSize = 12; + + ws.Cell("A3").Value = $"Zeitraum: {report.ReportStart:dd.MM.yyyy} - {report.ReportEnd:dd.MM.yyyy}"; + + // Summary table + var row = 5; + ws.Cell(row, 1).Value = "Kennzahl"; + ws.Cell(row, 2).Value = "Betrag"; + ws.Range(row, 1, row, 2).Style.Font.Bold = true; + ws.Range(row, 1, row, 2).Style.Fill.BackgroundColor = XLColor.LightGray; + + row++; + ws.Cell(row, 1).Value = "Anfangssaldo"; + ws.Cell(row, 2).Value = report.OpeningBalance; + ws.Cell(row, 2).Style.NumberFormat.Format = "#,##0.00 €"; + + row++; + ws.Cell(row, 1).Value = "Einnahmen"; + ws.Cell(row, 2).Value = report.TotalIncome; + ws.Cell(row, 2).Style.NumberFormat.Format = "#,##0.00 €"; + ws.Cell(row, 2).Style.Font.FontColor = XLColor.DarkGreen; + + row++; + ws.Cell(row, 1).Value = "Ausgaben"; + ws.Cell(row, 2).Value = report.TotalExpense; + ws.Cell(row, 2).Style.NumberFormat.Format = "#,##0.00 €"; + ws.Cell(row, 2).Style.Font.FontColor = XLColor.DarkRed; + + row++; + ws.Cell(row, 1).Value = "Differenz"; + ws.Cell(row, 2).Value = report.TotalIncome - report.TotalExpense; + ws.Cell(row, 2).Style.NumberFormat.Format = "#,##0.00 €"; + + row++; + ws.Cell(row, 1).Value = "Endsaldo"; + ws.Cell(row, 2).Value = report.ClosingBalance; + ws.Cell(row, 2).Style.NumberFormat.Format = "#,##0.00 €"; + ws.Cell(row, 2).Style.Font.Bold = true; + + // Statistics + row += 2; + ws.Cell(row, 1).Value = "Anzahl Buchungen"; + ws.Cell(row, 2).Value = report.Entries.Count; + + ws.Columns().AdjustToContents(); + } + + private static void CreateCategorySheet(XLWorkbook workbook, CashBookReportDto report) + { + var ws = workbook.Worksheets.Add("Kategorien"); + + var row = 1; + + // Income categories + ws.Cell(row, 1).Value = "Einnahmen nach Kategorie"; + ws.Cell(row, 1).Style.Font.Bold = true; + ws.Cell(row, 1).Style.Font.FontSize = 14; + + row += 2; + ws.Cell(row, 1).Value = "Kategorie"; + ws.Cell(row, 2).Value = "Anzahl"; + ws.Cell(row, 3).Value = "Summe"; + ws.Range(row, 1, row, 3).Style.Font.Bold = true; + ws.Range(row, 1, row, 3).Style.Fill.BackgroundColor = XLColor.LightGreen; + + row++; + foreach (var cat in report.IncomeByCategory.OrderByDescending(c => c.Total)) + { + ws.Cell(row, 1).Value = cat.CategoryName; + ws.Cell(row, 2).Value = cat.Count; + ws.Cell(row, 3).Value = cat.Total; + ws.Cell(row, 3).Style.NumberFormat.Format = "#,##0.00 €"; + row++; + } + + // Total income + ws.Cell(row, 1).Value = "Gesamt"; + ws.Cell(row, 1).Style.Font.Bold = true; + ws.Cell(row, 2).Value = report.IncomeByCategory.Sum(c => c.Count); + ws.Cell(row, 3).Value = report.TotalIncome; + ws.Cell(row, 3).Style.NumberFormat.Format = "#,##0.00 €"; + ws.Cell(row, 3).Style.Font.Bold = true; + + // Expense categories + row += 3; + ws.Cell(row, 1).Value = "Ausgaben nach Kategorie"; + ws.Cell(row, 1).Style.Font.Bold = true; + ws.Cell(row, 1).Style.Font.FontSize = 14; + + row += 2; + ws.Cell(row, 1).Value = "Kategorie"; + ws.Cell(row, 2).Value = "Anzahl"; + ws.Cell(row, 3).Value = "Summe"; + ws.Range(row, 1, row, 3).Style.Font.Bold = true; + ws.Range(row, 1, row, 3).Style.Fill.BackgroundColor = XLColor.LightCoral; + + row++; + foreach (var cat in report.ExpenseByCategory.OrderByDescending(c => c.Total)) + { + ws.Cell(row, 1).Value = cat.CategoryName; + ws.Cell(row, 2).Value = cat.Count; + ws.Cell(row, 3).Value = cat.Total; + ws.Cell(row, 3).Style.NumberFormat.Format = "#,##0.00 €"; + row++; + } + + // Total expense + ws.Cell(row, 1).Value = "Gesamt"; + ws.Cell(row, 1).Style.Font.Bold = true; + ws.Cell(row, 2).Value = report.ExpenseByCategory.Sum(c => c.Count); + ws.Cell(row, 3).Value = report.TotalExpense; + ws.Cell(row, 3).Style.NumberFormat.Format = "#,##0.00 €"; + ws.Cell(row, 3).Style.Font.Bold = true; + + ws.Columns().AdjustToContents(); + } + + private static void CreateEntriesSheet(XLWorkbook workbook, CashBookReportDto report) + { + var ws = workbook.Worksheets.Add("Buchungen"); + + // Header + var headers = new[] { "Datum", "Kategorie", "Typ", "Beschreibung", "Person", "Beleg", "Betrag" }; + for (int i = 0; i < headers.Length; i++) + { + ws.Cell(1, i + 1).Value = headers[i]; + } + ws.Range(1, 1, 1, headers.Length).Style.Font.Bold = true; + ws.Range(1, 1, 1, headers.Length).Style.Fill.BackgroundColor = XLColor.LightGray; + + // Data rows + var row = 2; + foreach (var entry in report.Entries.OrderBy(e => e.BookingDate)) + { + ws.Cell(row, 1).Value = entry.BookingDate; + ws.Cell(row, 1).Style.NumberFormat.Format = "dd.MM.yyyy"; + + ws.Cell(row, 2).Value = entry.CategoryName; + + ws.Cell(row, 3).Value = entry.EntryType == CashBookEntryType.Income ? "Einnahme" : "Ausgabe"; + ws.Cell(row, 3).Style.Font.FontColor = entry.EntryType == CashBookEntryType.Income + ? XLColor.DarkGreen + : XLColor.DarkRed; + + ws.Cell(row, 4).Value = entry.Comment ?? string.Empty; + ws.Cell(row, 5).Value = entry.PersonName ?? string.Empty; + ws.Cell(row, 6).Value = entry.ReceiptReference ?? string.Empty; + + var amount = entry.EntryType == CashBookEntryType.Income ? entry.Amount : -entry.Amount; + ws.Cell(row, 7).Value = amount; + ws.Cell(row, 7).Style.NumberFormat.Format = "#,##0.00 €"; + ws.Cell(row, 7).Style.Font.FontColor = entry.EntryType == CashBookEntryType.Income + ? XLColor.DarkGreen + : XLColor.DarkRed; + + row++; + } + + // Sum row + if (report.Entries.Count > 0) + { + ws.Cell(row, 6).Value = "Summe:"; + ws.Cell(row, 6).Style.Font.Bold = true; + ws.Cell(row, 7).FormulaA1 = $"=SUM(G2:G{row - 1})"; + ws.Cell(row, 7).Style.NumberFormat.Format = "#,##0.00 €"; + ws.Cell(row, 7).Style.Font.Bold = true; + } + + // Auto-fit and table styling + ws.Columns().AdjustToContents(); + ws.Column(4).Width = 40; // Description column wider + } +}