feat(sprint8): Phase 2 — Treasury frontend + PDF receipts
Backend: - ReceiptPdfService: Generates Quittung PDF per payment (OpenPDF, A4) - FinancialReportService: Annual financial report PDF (Jahresabschluss) - FinanceController: Added receipt download, annual report, CSV export endpoints - Portal receipt download with member ownership verification Frontend: - src/services/finance.ts: Complete React Query service (types, hooks, mutations) - /finance: Dashboard with KPI cards, recent transactions, outstanding members - /finance/payments: Payment list with filtering, void, receipt download - /finance/kassenbuch: Kassenbuch ledger with date range, CSV export - /finance/fee-schedules: Fee schedule CRUD with interval management - /finance/reports: Annual report PDF download - /portal/finance: Member self-service balance + payment history + receipts Navigation & i18n: - Added Finanzen (Wallet icon) to admin sidebar - Portal finance page for member payments - Comprehensive de.json + en.json finance keys (~100 translations)
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
package de.cannamanage.service;
|
||||
|
||||
import com.lowagie.text.*;
|
||||
import com.lowagie.text.pdf.PdfPCell;
|
||||
import com.lowagie.text.pdf.PdfPTable;
|
||||
import com.lowagie.text.pdf.PdfWriter;
|
||||
import de.cannamanage.domain.entity.Club;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.Year;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.TextStyle;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Generates the annual financial report (Jahresabschluss) as PDF.
|
||||
* Content: Income/expense totals, monthly breakdown, category breakdown, member payment summary.
|
||||
* Legal basis: §259 BGB (Rechenschaftspflicht), §147 AO (10-year retention).
|
||||
*/
|
||||
@Service
|
||||
public class FinancialReportService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(FinancialReportService.class);
|
||||
|
||||
private static final Font HEADER_FONT = new Font(Font.HELVETICA, 16, Font.BOLD);
|
||||
private static final Font TITLE_FONT = new Font(Font.HELVETICA, 12, Font.BOLD);
|
||||
private static final Font SUBTITLE_FONT = new Font(Font.HELVETICA, 10, Font.BOLD);
|
||||
private static final Font NORMAL_FONT = new Font(Font.HELVETICA, 10, Font.NORMAL);
|
||||
private static final Font TABLE_HEADER_FONT = new Font(Font.HELVETICA, 9, Font.BOLD, Color.WHITE);
|
||||
private static final Font TABLE_CELL_FONT = new Font(Font.HELVETICA, 9, Font.NORMAL);
|
||||
private static final Font TABLE_CELL_BOLD = new Font(Font.HELVETICA, 9, Font.BOLD);
|
||||
private static final Font FOOTER_FONT = new Font(Font.HELVETICA, 8, Font.ITALIC, Color.GRAY);
|
||||
|
||||
private static final Color PRIMARY_COLOR = new Color(34, 87, 58); // Dark green
|
||||
private static final Color HEADER_BG = new Color(34, 87, 58);
|
||||
private static final Color LIGHT_BG = new Color(245, 248, 245);
|
||||
private static final Color INCOME_COLOR = new Color(22, 163, 74);
|
||||
private static final Color EXPENSE_COLOR = new Color(220, 38, 38);
|
||||
|
||||
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("dd.MM.yyyy");
|
||||
|
||||
/**
|
||||
* DTO for annual report data.
|
||||
*/
|
||||
public record AnnualReportData(
|
||||
int year,
|
||||
BigDecimal totalIncome,
|
||||
BigDecimal totalExpenses,
|
||||
BigDecimal netBalance,
|
||||
List<MonthlyBreakdown> monthlyBreakdown,
|
||||
List<CategoryBreakdown> expensesByCategory,
|
||||
int totalMembers,
|
||||
int paidMembers,
|
||||
BigDecimal totalOutstanding
|
||||
) {}
|
||||
|
||||
public record MonthlyBreakdown(
|
||||
int month,
|
||||
String monthName,
|
||||
BigDecimal income,
|
||||
BigDecimal expenses,
|
||||
BigDecimal net
|
||||
) {}
|
||||
|
||||
public record CategoryBreakdown(
|
||||
String category,
|
||||
BigDecimal amount,
|
||||
double percentage
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Generate the annual financial report PDF.
|
||||
*/
|
||||
public byte[] generateAnnualReport(AnnualReportData data, Club club) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
Document document = new Document(PageSize.A4, 50, 50, 50, 50);
|
||||
|
||||
try {
|
||||
PdfWriter writer = PdfWriter.getInstance(document, baos);
|
||||
writer.setPageEvent(new PdfFooterHandler());
|
||||
document.open();
|
||||
|
||||
// === Page 1: Cover & Summary ===
|
||||
|
||||
// Club header
|
||||
Paragraph clubHeader = new Paragraph(club.getName(), HEADER_FONT);
|
||||
clubHeader.setSpacingAfter(5);
|
||||
document.add(clubHeader);
|
||||
|
||||
// Title
|
||||
Paragraph title = new Paragraph(
|
||||
"Jahresabschluss " + data.year(), TITLE_FONT);
|
||||
title.setSpacingAfter(5);
|
||||
document.add(title);
|
||||
|
||||
// Subtitle
|
||||
Paragraph subtitle = new Paragraph(
|
||||
"Finanzübersicht für das Geschäftsjahr " + data.year(),
|
||||
NORMAL_FONT);
|
||||
subtitle.setSpacingAfter(20);
|
||||
document.add(subtitle);
|
||||
|
||||
// --- Summary KPIs ---
|
||||
addSectionTitle(document, "Zusammenfassung");
|
||||
|
||||
PdfPTable summaryTable = new PdfPTable(2);
|
||||
summaryTable.setWidthPercentage(60);
|
||||
summaryTable.setHorizontalAlignment(Element.ALIGN_LEFT);
|
||||
summaryTable.setSpacingAfter(20);
|
||||
|
||||
addSummaryRow(summaryTable, "Gesamteinnahmen",
|
||||
formatEuro(data.totalIncome()), INCOME_COLOR);
|
||||
addSummaryRow(summaryTable, "Gesamtausgaben",
|
||||
formatEuro(data.totalExpenses()), EXPENSE_COLOR);
|
||||
addSummaryRow(summaryTable, "Saldo",
|
||||
formatEuro(data.netBalance()),
|
||||
data.netBalance().signum() >= 0 ? INCOME_COLOR : EXPENSE_COLOR);
|
||||
|
||||
document.add(summaryTable);
|
||||
|
||||
// --- Member payment summary ---
|
||||
addSectionTitle(document, "Mitgliederbeiträge");
|
||||
|
||||
PdfPTable memberTable = new PdfPTable(2);
|
||||
memberTable.setWidthPercentage(60);
|
||||
memberTable.setHorizontalAlignment(Element.ALIGN_LEFT);
|
||||
memberTable.setSpacingAfter(20);
|
||||
|
||||
addSummaryRow(memberTable, "Mitglieder gesamt",
|
||||
String.valueOf(data.totalMembers()), Color.BLACK);
|
||||
addSummaryRow(memberTable, "Beiträge bezahlt",
|
||||
String.valueOf(data.paidMembers()), INCOME_COLOR);
|
||||
addSummaryRow(memberTable, "Offene Beiträge",
|
||||
formatEuro(data.totalOutstanding()), EXPENSE_COLOR);
|
||||
|
||||
document.add(memberTable);
|
||||
|
||||
// --- Monthly Breakdown Table ---
|
||||
addSectionTitle(document, "Monatliche Übersicht");
|
||||
|
||||
PdfPTable monthlyTable = new PdfPTable(4);
|
||||
monthlyTable.setWidthPercentage(100);
|
||||
monthlyTable.setWidths(new float[]{30f, 23f, 23f, 24f});
|
||||
monthlyTable.setSpacingAfter(20);
|
||||
|
||||
addColoredHeader(monthlyTable, "Monat");
|
||||
addColoredHeader(monthlyTable, "Einnahmen");
|
||||
addColoredHeader(monthlyTable, "Ausgaben");
|
||||
addColoredHeader(monthlyTable, "Saldo");
|
||||
|
||||
BigDecimal runningTotal = BigDecimal.ZERO;
|
||||
for (MonthlyBreakdown month : data.monthlyBreakdown()) {
|
||||
runningTotal = runningTotal.add(month.net());
|
||||
|
||||
boolean isEven = month.month() % 2 == 0;
|
||||
Color rowBg = isEven ? LIGHT_BG : Color.WHITE;
|
||||
|
||||
addCellWithBg(monthlyTable, month.monthName(), TABLE_CELL_FONT, rowBg);
|
||||
addCellWithBg(monthlyTable, formatEuro(month.income()), TABLE_CELL_FONT, rowBg);
|
||||
addCellWithBg(monthlyTable, formatEuro(month.expenses()), TABLE_CELL_FONT, rowBg);
|
||||
addCellWithBg(monthlyTable, formatEuro(month.net()), TABLE_CELL_FONT, rowBg);
|
||||
}
|
||||
|
||||
// Total row
|
||||
addCellWithBg(monthlyTable, "GESAMT", TABLE_CELL_BOLD, LIGHT_BG);
|
||||
addCellWithBg(monthlyTable, formatEuro(data.totalIncome()), TABLE_CELL_BOLD, LIGHT_BG);
|
||||
addCellWithBg(monthlyTable, formatEuro(data.totalExpenses()), TABLE_CELL_BOLD, LIGHT_BG);
|
||||
addCellWithBg(monthlyTable, formatEuro(data.netBalance()), TABLE_CELL_BOLD, LIGHT_BG);
|
||||
|
||||
document.add(monthlyTable);
|
||||
|
||||
// --- Expense Breakdown by Category ---
|
||||
if (data.expensesByCategory() != null && !data.expensesByCategory().isEmpty()) {
|
||||
addSectionTitle(document, "Ausgaben nach Kategorie");
|
||||
|
||||
PdfPTable categoryTable = new PdfPTable(3);
|
||||
categoryTable.setWidthPercentage(80);
|
||||
categoryTable.setWidths(new float[]{45f, 30f, 25f});
|
||||
categoryTable.setSpacingAfter(20);
|
||||
|
||||
addColoredHeader(categoryTable, "Kategorie");
|
||||
addColoredHeader(categoryTable, "Betrag");
|
||||
addColoredHeader(categoryTable, "Anteil");
|
||||
|
||||
for (CategoryBreakdown cat : data.expensesByCategory()) {
|
||||
addCell(categoryTable, translateCategory(cat.category()));
|
||||
addCell(categoryTable, formatEuro(cat.amount()));
|
||||
addCell(categoryTable, String.format("%.1f%%", cat.percentage()));
|
||||
}
|
||||
|
||||
document.add(categoryTable);
|
||||
}
|
||||
|
||||
// --- Footer note ---
|
||||
Paragraph footerNote = new Paragraph(
|
||||
"Erstellt am " + LocalDate.now().format(DATE_FMT)
|
||||
+ " — Anbauvereinigung gemäß §2 KCanG",
|
||||
FOOTER_FONT);
|
||||
footerNote.setAlignment(Element.ALIGN_CENTER);
|
||||
footerNote.setSpacingBefore(30);
|
||||
document.add(footerNote);
|
||||
|
||||
document.close();
|
||||
|
||||
} catch (DocumentException e) {
|
||||
log.error("Failed to generate annual report PDF for year {}", data.year(), e);
|
||||
throw new RuntimeException("PDF generation failed", e);
|
||||
}
|
||||
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
// --- Helper methods ---
|
||||
|
||||
private void addSectionTitle(Document document, String text) throws DocumentException {
|
||||
Paragraph section = new Paragraph(text, SUBTITLE_FONT);
|
||||
section.setSpacingBefore(10);
|
||||
section.setSpacingAfter(8);
|
||||
document.add(section);
|
||||
}
|
||||
|
||||
private void addSummaryRow(PdfPTable table, String label, String value, Color valueColor) {
|
||||
PdfPCell labelCell = new PdfPCell(new Phrase(label, NORMAL_FONT));
|
||||
labelCell.setBorder(Rectangle.NO_BORDER);
|
||||
labelCell.setPaddingBottom(5);
|
||||
table.addCell(labelCell);
|
||||
|
||||
Font valueFont = new Font(Font.HELVETICA, 10, Font.BOLD, valueColor);
|
||||
PdfPCell valueCell = new PdfPCell(new Phrase(value, valueFont));
|
||||
valueCell.setBorder(Rectangle.NO_BORDER);
|
||||
valueCell.setPaddingBottom(5);
|
||||
valueCell.setHorizontalAlignment(Element.ALIGN_RIGHT);
|
||||
table.addCell(valueCell);
|
||||
}
|
||||
|
||||
private void addColoredHeader(PdfPTable table, String text) {
|
||||
PdfPCell cell = new PdfPCell(new Phrase(text, TABLE_HEADER_FONT));
|
||||
cell.setBackgroundColor(HEADER_BG);
|
||||
cell.setPadding(6);
|
||||
cell.setBorderWidth(0);
|
||||
table.addCell(cell);
|
||||
}
|
||||
|
||||
private void addCell(PdfPTable table, String text) {
|
||||
PdfPCell cell = new PdfPCell(new Phrase(text, TABLE_CELL_FONT));
|
||||
cell.setPadding(5);
|
||||
cell.setBorderWidth(0.5f);
|
||||
cell.setBorderColor(Color.LIGHT_GRAY);
|
||||
table.addCell(cell);
|
||||
}
|
||||
|
||||
private void addCellWithBg(PdfPTable table, String text, Font font, Color bg) {
|
||||
PdfPCell cell = new PdfPCell(new Phrase(text, font));
|
||||
cell.setBackgroundColor(bg);
|
||||
cell.setPadding(5);
|
||||
cell.setBorderWidth(0.5f);
|
||||
cell.setBorderColor(Color.LIGHT_GRAY);
|
||||
table.addCell(cell);
|
||||
}
|
||||
|
||||
private String formatEuro(BigDecimal amount) {
|
||||
return String.format(Locale.GERMAN, "%,.2f €", amount);
|
||||
}
|
||||
|
||||
private String translateCategory(String category) {
|
||||
return switch (category) {
|
||||
case "RENT" -> "Miete";
|
||||
case "UTILITIES" -> "Nebenkosten";
|
||||
case "EQUIPMENT" -> "Ausstattung";
|
||||
case "SEEDS" -> "Saatgut";
|
||||
case "SUPPLIES" -> "Verbrauchsmaterial";
|
||||
case "INSURANCE" -> "Versicherung";
|
||||
case "LEGAL" -> "Rechtsberatung";
|
||||
case "OTHER" -> "Sonstiges";
|
||||
default -> category;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package de.cannamanage.service;
|
||||
|
||||
import com.lowagie.text.*;
|
||||
import com.lowagie.text.pdf.PdfPCell;
|
||||
import com.lowagie.text.pdf.PdfPTable;
|
||||
import com.lowagie.text.pdf.PdfWriter;
|
||||
import de.cannamanage.domain.entity.Club;
|
||||
import de.cannamanage.domain.entity.Member;
|
||||
import de.cannamanage.domain.entity.Payment;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Generates receipt (Quittung) PDFs for member payments.
|
||||
* Layout: A4 portrait, single page, professional receipt format.
|
||||
* Legal basis: §147 AO (10-year retention for Buchungsbelege).
|
||||
*/
|
||||
@Service
|
||||
public class ReceiptPdfService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ReceiptPdfService.class);
|
||||
|
||||
private static final Font CLUB_NAME_FONT = new Font(Font.HELVETICA, 14, Font.BOLD);
|
||||
private static final Font CLUB_ADDRESS_FONT = new Font(Font.HELVETICA, 9, Font.NORMAL, Color.DARK_GRAY);
|
||||
private static final Font TITLE_FONT = new Font(Font.HELVETICA, 18, Font.BOLD);
|
||||
private static final Font RECEIPT_NR_FONT = new Font(Font.HELVETICA, 10, Font.NORMAL, Color.GRAY);
|
||||
private static final Font LABEL_FONT = new Font(Font.HELVETICA, 10, Font.BOLD);
|
||||
private static final Font VALUE_FONT = new Font(Font.HELVETICA, 10, Font.NORMAL);
|
||||
private static final Font AMOUNT_FONT = new Font(Font.HELVETICA, 14, Font.BOLD);
|
||||
private static final Font FOOTER_FONT = new Font(Font.HELVETICA, 8, Font.ITALIC, Color.GRAY);
|
||||
private static final Font SIGNATURE_FONT = new Font(Font.HELVETICA, 9, Font.NORMAL);
|
||||
|
||||
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("dd.MM.yyyy");
|
||||
private static final DateTimeFormatter PERIOD_FMT = DateTimeFormatter.ofPattern("MMMM yyyy", Locale.GERMAN);
|
||||
|
||||
/**
|
||||
* Generate a receipt PDF for a payment.
|
||||
*
|
||||
* @param payment the payment record
|
||||
* @param member the paying member
|
||||
* @param club the club entity (for header)
|
||||
* @return PDF bytes
|
||||
*/
|
||||
public byte[] generateReceipt(Payment payment, Member member, Club club) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
// A4 with generous margins
|
||||
Document document = new Document(PageSize.A4, 60, 60, 60, 60);
|
||||
|
||||
try {
|
||||
PdfWriter.getInstance(document, baos);
|
||||
document.open();
|
||||
|
||||
// --- Club Header ---
|
||||
Paragraph clubName = new Paragraph(club.getName(), CLUB_NAME_FONT);
|
||||
clubName.setAlignment(Element.ALIGN_LEFT);
|
||||
document.add(clubName);
|
||||
|
||||
if (club.getAddress() != null && !club.getAddress().isBlank()) {
|
||||
Paragraph address = new Paragraph(club.getAddress(), CLUB_ADDRESS_FONT);
|
||||
address.setSpacingAfter(5);
|
||||
document.add(address);
|
||||
}
|
||||
|
||||
// Horizontal rule
|
||||
Paragraph hr = new Paragraph(" ");
|
||||
hr.setSpacingBefore(5);
|
||||
hr.setSpacingAfter(15);
|
||||
document.add(hr);
|
||||
|
||||
// --- Title ---
|
||||
Paragraph title = new Paragraph("QUITTUNG", TITLE_FONT);
|
||||
title.setAlignment(Element.ALIGN_CENTER);
|
||||
title.setSpacingAfter(5);
|
||||
document.add(title);
|
||||
|
||||
// Receipt number
|
||||
String receiptNr = payment.getReceiptNumber() != null
|
||||
? payment.getReceiptNumber()
|
||||
: "CM-" + payment.getId().toString().substring(0, 8).toUpperCase();
|
||||
Paragraph nrPara = new Paragraph("Nr. " + receiptNr, RECEIPT_NR_FONT);
|
||||
nrPara.setAlignment(Element.ALIGN_CENTER);
|
||||
nrPara.setSpacingAfter(30);
|
||||
document.add(nrPara);
|
||||
|
||||
// --- Receipt Details Table ---
|
||||
PdfPTable detailsTable = new PdfPTable(2);
|
||||
detailsTable.setWidthPercentage(80);
|
||||
detailsTable.setHorizontalAlignment(Element.ALIGN_LEFT);
|
||||
detailsTable.setWidths(new float[]{35f, 65f});
|
||||
detailsTable.setSpacingAfter(20);
|
||||
|
||||
addDetailRow(detailsTable, "Erhalten von:", getMemberDisplayName(member));
|
||||
addDetailRow(detailsTable, "Mitgliedsnr.:", member.getMemberNumber() != null
|
||||
? member.getMemberNumber() : "—");
|
||||
|
||||
// Amount - formatted as Euro
|
||||
BigDecimal amount = BigDecimal.valueOf(payment.getAmountCents()).divide(BigDecimal.valueOf(100));
|
||||
String amountStr = String.format(Locale.GERMAN, "%,.2f €", amount);
|
||||
addDetailRow(detailsTable, "Betrag:", amountStr, AMOUNT_FONT);
|
||||
|
||||
addDetailRow(detailsTable, "Datum:", payment.getPaymentDate().format(DATE_FMT));
|
||||
addDetailRow(detailsTable, "Zahlungsart:", translatePaymentMethod(payment.getPaymentMethod().name()));
|
||||
|
||||
// Period covered
|
||||
if (payment.getPeriodFrom() != null && payment.getPeriodTo() != null) {
|
||||
String period = formatPeriod(payment.getPeriodFrom(), payment.getPeriodTo());
|
||||
addDetailRow(detailsTable, "Zeitraum:", period);
|
||||
}
|
||||
|
||||
addDetailRow(detailsTable, "Verwendung:", "Mitgliedsbeitrag");
|
||||
|
||||
if (payment.getReference() != null && !payment.getReference().isBlank()) {
|
||||
addDetailRow(detailsTable, "Referenz:", payment.getReference());
|
||||
}
|
||||
|
||||
document.add(detailsTable);
|
||||
|
||||
// --- Spacer ---
|
||||
document.add(new Paragraph(" "));
|
||||
document.add(new Paragraph(" "));
|
||||
document.add(new Paragraph(" "));
|
||||
|
||||
// --- Signature Section ---
|
||||
PdfPTable sigTable = new PdfPTable(2);
|
||||
sigTable.setWidthPercentage(80);
|
||||
sigTable.setHorizontalAlignment(Element.ALIGN_LEFT);
|
||||
sigTable.setWidths(new float[]{50f, 50f});
|
||||
|
||||
// Date line
|
||||
PdfPCell dateCell = new PdfPCell();
|
||||
dateCell.setBorder(Rectangle.TOP);
|
||||
dateCell.setBorderWidth(0.5f);
|
||||
dateCell.setPaddingTop(5);
|
||||
dateCell.addElement(new Phrase("Datum", SIGNATURE_FONT));
|
||||
sigTable.addCell(dateCell);
|
||||
|
||||
// Signature line
|
||||
PdfPCell sigCell = new PdfPCell();
|
||||
sigCell.setBorder(Rectangle.TOP);
|
||||
sigCell.setBorderWidth(0.5f);
|
||||
sigCell.setPaddingTop(5);
|
||||
sigCell.addElement(new Phrase("Unterschrift Kassenwart", SIGNATURE_FONT));
|
||||
sigTable.addCell(sigCell);
|
||||
|
||||
document.add(sigTable);
|
||||
|
||||
// --- Footer ---
|
||||
Paragraph footer = new Paragraph(
|
||||
"Anbauvereinigung gemäß §2 KCanG — " + club.getName(),
|
||||
FOOTER_FONT
|
||||
);
|
||||
footer.setAlignment(Element.ALIGN_CENTER);
|
||||
footer.setSpacingBefore(40);
|
||||
document.add(footer);
|
||||
|
||||
document.close();
|
||||
|
||||
} catch (DocumentException e) {
|
||||
log.error("Failed to generate receipt PDF for payment {}", payment.getId(), e);
|
||||
throw new RuntimeException("PDF generation failed", e);
|
||||
}
|
||||
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
private void addDetailRow(PdfPTable table, String label, String value) {
|
||||
addDetailRow(table, label, value, VALUE_FONT);
|
||||
}
|
||||
|
||||
private void addDetailRow(PdfPTable table, String label, String value, Font valueFont) {
|
||||
PdfPCell labelCell = new PdfPCell(new Phrase(label, LABEL_FONT));
|
||||
labelCell.setBorder(Rectangle.NO_BORDER);
|
||||
labelCell.setPaddingBottom(8);
|
||||
table.addCell(labelCell);
|
||||
|
||||
PdfPCell valueCell = new PdfPCell(new Phrase(value, valueFont));
|
||||
valueCell.setBorder(Rectangle.NO_BORDER);
|
||||
valueCell.setPaddingBottom(8);
|
||||
table.addCell(valueCell);
|
||||
}
|
||||
|
||||
private String getMemberDisplayName(Member member) {
|
||||
String firstName = member.getFirstName() != null ? member.getFirstName() : "";
|
||||
String lastName = member.getLastName() != null ? member.getLastName() : "";
|
||||
return (firstName + " " + lastName).trim();
|
||||
}
|
||||
|
||||
private String translatePaymentMethod(String method) {
|
||||
return switch (method) {
|
||||
case "CASH" -> "Bar";
|
||||
case "BANK_TRANSFER" -> "Überweisung";
|
||||
case "SEPA" -> "SEPA-Lastschrift";
|
||||
case "CARD" -> "Kartenzahlung";
|
||||
default -> method;
|
||||
};
|
||||
}
|
||||
|
||||
private String formatPeriod(LocalDate from, LocalDate to) {
|
||||
if (from.getMonth() == to.getMonth() && from.getYear() == to.getYear()) {
|
||||
return from.format(PERIOD_FMT);
|
||||
}
|
||||
return from.format(PERIOD_FMT) + " – " + to.format(PERIOD_FMT);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user