From cab04c58692c2d447f9bbe51635a53d836a73708 Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Mon, 3 Aug 2026 21:08:03 +0200 Subject: [PATCH] feat: add buildCsv for cashbox export --- .../cashbox-export.utils.spec.ts | 35 ++++++++++++++++- .../cashbox-export/cashbox-export.utils.ts | 38 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.spec.ts b/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.spec.ts index 858f6a2..8e08973 100644 --- a/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.spec.ts +++ b/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.spec.ts @@ -1,4 +1,4 @@ -import { buildRows } from './cashbox-export.utils'; +import { buildCsv, buildRows } from './cashbox-export.utils'; describe('buildRows', () => { const team = (overrides: Partial<{ transactions: any[]; players: any[] }> = {}) => ({ @@ -101,3 +101,36 @@ describe('buildRows', () => { ]); }); }); + +describe('buildCsv', () => { + it('renders the header and formatted rows with German decimals', () => { + const csv = buildCsv([ + { date: '2026-08-05T00:00:00.000Z', type: 'payment', who: 'Alex Muster', note: 'Bar bezahlt', amount: 10, runningTotal: 10 }, + { date: '2026-08-10T00:00:00.000Z', type: 'expense', who: 'Teamkasse', note: 'Bälle', amount: -20.5, runningTotal: -10.5 }, + ]); + + expect(csv).toBe( + 'Datum;Typ;Wer;Notiz;Betrag;Periodensaldo\r\n' + + '2026-08-05;Zahlung;Alex Muster;Bar bezahlt;10,00;10,00\r\n' + + '2026-08-10;Ausgabe;Teamkasse;Bälle;-20,50;-10,50', + ); + }); + + it('quotes notes containing a semicolon and escapes embedded quotes', () => { + const csv = buildCsv([ + { date: '2026-08-05T00:00:00.000Z', type: 'credit', who: 'Teamkasse', note: 'Spende; "danke"', amount: 5, runningTotal: 5 }, + ]); + + expect(csv).toBe( + 'Datum;Typ;Wer;Notiz;Betrag;Periodensaldo\r\n' + + '2026-08-05;Guthaben;Teamkasse;"Spende; ""danke""";5,00;5,00', + ); + }); + + it('shows a placeholder row when there are no bookings', () => { + const csv = buildCsv([]); + expect(csv).toBe( + 'Datum;Typ;Wer;Notiz;Betrag;Periodensaldo\r\nKeine Buchungen im gewählten Zeitraum', + ); + }); +}); diff --git a/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.ts b/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.ts index 2ada4bb..54e90b9 100644 --- a/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.ts +++ b/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.ts @@ -60,3 +60,41 @@ export function buildRows(team: Team, from: string, to: string): CashboxExportRo return { ...row, runningTotal }; }); } + +const TYPE_LABELS: Record = { + payment: 'Zahlung', + credit: 'Guthaben', + expense: 'Ausgabe', +}; + +function formatGermanAmount(value: number): string { + return value.toFixed(2).replace('.', ','); +} + +function escapeCsvField(value: string): string { + if (/[;"\n\r]/.test(value)) { + return `"${value.replace(/"/g, '""')}"`; + } + return value; +} + +export function buildCsv(rows: CashboxExportRow[]): string { + const lines = ['Datum;Typ;Wer;Notiz;Betrag;Periodensaldo']; + if (rows.length === 0) { + lines.push('Keine Buchungen im gewählten Zeitraum'); + } else { + for (const row of rows) { + lines.push( + [ + row.date.slice(0, 10), + TYPE_LABELS[row.type] ?? row.type, + escapeCsvField(row.who), + escapeCsvField(row.note), + formatGermanAmount(row.amount), + formatGermanAmount(row.runningTotal), + ].join(';'), + ); + } + } + return lines.join('\r\n'); +}