K16 fertig.
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.
This commit is contained in:
parent
dfe74adf01
commit
5bd0e19f06
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ namespace Koogle.Application
|
|||
services.AddScoped<IClubRequestService, ClubRequestService>();
|
||||
services.AddScoped<IBookingCategoryService, BookingCategoryService>();
|
||||
services.AddScoped<ICashBookService, CashBookService>();
|
||||
services.AddScoped<ICashBookExportService, CashBookExportService>();
|
||||
|
||||
// Note: Game types are registered in Koogle.Web where Blazor components are defined
|
||||
// Use services.AddGameType<TDefinition, TLogicService>() to register game types
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
using Koogle.Application.DTOs;
|
||||
|
||||
namespace Koogle.Application.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Service for exporting cash book reports to various formats.
|
||||
/// </summary>
|
||||
public interface ICashBookExportService
|
||||
{
|
||||
/// <summary>
|
||||
/// Exports a cash book report to Excel format.
|
||||
/// </summary>
|
||||
/// <param name="report">The report data to export.</param>
|
||||
/// <param name="clubName">Name of the club for the header.</param>
|
||||
/// <returns>Excel file as byte array.</returns>
|
||||
Task<byte[]> ExportToExcelAsync(CashBookReportDto report, string clubName);
|
||||
|
||||
/// <summary>
|
||||
/// Exports a cash book report to PDF format.
|
||||
/// </summary>
|
||||
/// <param name="report">The report data to export.</param>
|
||||
/// <param name="clubName">Name of the club for the header.</param>
|
||||
/// <returns>PDF file as byte array.</returns>
|
||||
Task<byte[]> ExportToPdfAsync(CashBookReportDto report, string clubName);
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="15.1.0" />
|
||||
<PackageReference Include="ClosedXML" Version="0.105.0" />
|
||||
<PackageReference Include="FluentValidation" Version="12.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,216 @@
|
|||
using ClosedXML.Excel;
|
||||
using Koogle.Application.DTOs;
|
||||
using Koogle.Application.Interfaces;
|
||||
using Koogle.Domain.Enums;
|
||||
|
||||
namespace Koogle.Application.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for exporting cash book reports to Excel and PDF formats.
|
||||
/// </summary>
|
||||
public class CashBookExportService : ICashBookExportService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Task<byte[]> 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());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<byte[]> 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
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue