Add infrastructure layer with persistence and services
- EF Core DbContext and entity configurations - Design-time factory for migrations - Initial and soft-delete migrations - Service implementations: Account, AccountYear, Entry, BalanceQuery, FileSave, PdfStatement - Dependency injection registration
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
using System.Globalization;
|
||||
using Duempelkas.App.Services;
|
||||
using Duempelkas.App.Services.Models;
|
||||
using Duempelkas.Domain.Enums;
|
||||
using Duempelkas.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace Duempelkas.Infrastructure.Services;
|
||||
|
||||
public class PdfStatementService : IPdfStatementService
|
||||
{
|
||||
private readonly FinanceDbContext _db;
|
||||
private readonly IEntryService _entryService;
|
||||
private readonly IBalanceQueryService _balanceQueryService;
|
||||
private static readonly CultureInfo DeLocale = new("de-DE");
|
||||
|
||||
public PdfStatementService(FinanceDbContext db, IEntryService entryService, IBalanceQueryService balanceQueryService)
|
||||
{
|
||||
_db = db;
|
||||
_entryService = entryService;
|
||||
_balanceQueryService = balanceQueryService;
|
||||
}
|
||||
|
||||
public async Task<byte[]> GenerateStatementAsync(int accountId, bool currentYearOnly)
|
||||
{
|
||||
var account = await _db.Accounts.FindAsync(accountId)
|
||||
?? throw new InvalidOperationException($"Konto {accountId} nicht gefunden.");
|
||||
|
||||
var entries = await _entryService.GetEntriesAsync(accountId, currentYearOnly);
|
||||
var balance = await _balanceQueryService.GetAccountBalanceAsync(accountId);
|
||||
|
||||
var title = currentYearOnly
|
||||
? $"{account.Name} – Auszug {DateTime.Now.Year}"
|
||||
: $"{account.Name} – Gesamtauszug";
|
||||
|
||||
var document = Document.Create(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Size(PageSizes.A4);
|
||||
page.MarginHorizontal(40);
|
||||
page.MarginVertical(30);
|
||||
page.DefaultTextStyle(x => x.FontSize(10));
|
||||
|
||||
page.Header().Column(col =>
|
||||
{
|
||||
col.Item().Text(account.Name).Bold().FontSize(20);
|
||||
col.Item().Text(title).FontSize(14).FontColor(Colors.Grey.Darken1);
|
||||
col.Item().PaddingTop(5).Text($"Erstellt am: {DateTime.Now:dd.MM.yyyy}").FontSize(8).FontColor(Colors.Grey.Medium);
|
||||
col.Item().PaddingTop(10).LineHorizontal(1).LineColor(Colors.Grey.Lighten2);
|
||||
});
|
||||
|
||||
page.Content().PaddingTop(15).Column(col =>
|
||||
{
|
||||
col.Item().PaddingBottom(10).Row(row =>
|
||||
{
|
||||
row.RelativeItem().Text("Übertrag:").SemiBold();
|
||||
row.AutoItem().Text(FormatCurrency(balance.CarryoverBalance)).SemiBold();
|
||||
});
|
||||
|
||||
col.Item().Table(table =>
|
||||
{
|
||||
table.ColumnsDefinition(columns =>
|
||||
{
|
||||
columns.ConstantColumn(80);
|
||||
columns.ConstantColumn(80);
|
||||
columns.RelativeColumn();
|
||||
columns.ConstantColumn(100);
|
||||
});
|
||||
|
||||
table.Header(header =>
|
||||
{
|
||||
header.Cell().BorderBottom(1).BorderColor(Colors.Grey.Darken1)
|
||||
.PaddingBottom(5).Text("Datum").SemiBold();
|
||||
header.Cell().BorderBottom(1).BorderColor(Colors.Grey.Darken1)
|
||||
.PaddingBottom(5).Text("Nr.").SemiBold();
|
||||
header.Cell().BorderBottom(1).BorderColor(Colors.Grey.Darken1)
|
||||
.PaddingBottom(5).Text("Bezeichnung").SemiBold();
|
||||
header.Cell().BorderBottom(1).BorderColor(Colors.Grey.Darken1)
|
||||
.PaddingBottom(5).AlignRight().Text("Betrag").SemiBold();
|
||||
});
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
var titleText = entry.Title;
|
||||
if (entry.IsTransfer && !string.IsNullOrEmpty(entry.LinkedAccountName))
|
||||
{
|
||||
var prefix = entry.Type == EntryType.Expense ? "an" : "von";
|
||||
titleText += $" ({prefix} {entry.LinkedAccountName})";
|
||||
}
|
||||
|
||||
table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2)
|
||||
.PaddingVertical(3).Text(entry.Date.ToString("dd.MM.yyyy"));
|
||||
table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2)
|
||||
.PaddingVertical(3).Text(entry.DisplayId);
|
||||
table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2)
|
||||
.PaddingVertical(3).Text(titleText);
|
||||
table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2)
|
||||
.PaddingVertical(3).AlignRight()
|
||||
.Text($"{(entry.Type == EntryType.Income ? "+" : "−")}{FormatCurrency(entry.Amount)}")
|
||||
.FontColor(entry.Type == EntryType.Income ? Colors.Green.Darken1 : Colors.Red.Darken1);
|
||||
}
|
||||
});
|
||||
|
||||
col.Item().PaddingTop(20).LineHorizontal(1).LineColor(Colors.Grey.Lighten2);
|
||||
col.Item().PaddingTop(10).Column(summaryCol =>
|
||||
{
|
||||
SummaryRow(summaryCol, "Übertrag:", balance.CarryoverBalance);
|
||||
SummaryRow(summaryCol, "Einnahmen gesamt:", balance.TotalIncome);
|
||||
SummaryRow(summaryCol, "Ausgaben gesamt:", -balance.TotalExpense);
|
||||
summaryCol.Item().PaddingTop(5).Row(row =>
|
||||
{
|
||||
row.RelativeItem().Text("Saldo:").Bold().FontSize(12);
|
||||
row.AutoItem().Text(FormatCurrency(balance.TotalBalance)).Bold().FontSize(12)
|
||||
.FontColor(balance.TotalBalance >= 0 ? Colors.Green.Darken1 : Colors.Red.Darken1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
page.Footer().AlignCenter().Text(text =>
|
||||
{
|
||||
text.Span("Seite ");
|
||||
text.CurrentPageNumber();
|
||||
text.Span(" von ");
|
||||
text.TotalPages();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return document.GeneratePdf();
|
||||
}
|
||||
|
||||
private static void SummaryRow(ColumnDescriptor col, string label, decimal value)
|
||||
{
|
||||
col.Item().PaddingVertical(2).Row(row =>
|
||||
{
|
||||
row.RelativeItem().Text(label);
|
||||
row.AutoItem().Text(FormatCurrency(value))
|
||||
.FontColor(value >= 0 ? Colors.Green.Darken1 : Colors.Red.Darken1);
|
||||
});
|
||||
}
|
||||
|
||||
private static string FormatCurrency(decimal amount)
|
||||
{
|
||||
return amount.ToString("N2", DeLocale) + " €";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user