import { Team } from 'src/teams/entities/team.entity'; import PDFDocument = require('pdfkit'); export interface CashboxExportRow { date: string; type: string; who: string; note: string; amount: number; runningTotal: number; } interface RawRow { date: string; type: string; who: string; note: string; amount: number; } export function buildRows(team: Team, from: string, to: string): CashboxExportRow[] { const fromTime = new Date(`${from}T00:00:00.000Z`).getTime(); const toTime = new Date(`${to}T23:59:59.999Z`).getTime(); const raw: RawRow[] = []; for (const transaction of team.transactions ?? []) { if (!transaction.type) continue; raw.push({ date: transaction.date, type: transaction.type.name, who: 'Teamkasse', note: transaction.note, amount: Number(transaction.amount), }); } for (const player of team.players ?? []) { for (const transaction of player.transactions ?? []) { if (!transaction.type || transaction.type.name !== 'payment') continue; raw.push({ date: transaction.date, type: transaction.type.name, who: `${player.firstName} ${player.lastName}`, note: transaction.note, amount: Number(transaction.amount), }); } } const filtered = raw .filter((row) => { const time = new Date(row.date).getTime(); return time >= fromTime && time <= toTime; }) .sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0)); let runningTotal = 0; return filtered.map((row) => { runningTotal += row.amount; return { ...row, runningTotal }; }); } const TYPE_LABELS: Record = { payment: 'Zahlung', credit: 'Guthaben', expense: 'Ausgabe', }; function formatGermanAmount(value: number): string { const rounded = Math.sign(value) * Math.round((Math.abs(value) + Number.EPSILON) * 100) / 100; const normalized = rounded === 0 ? 0 : rounded; return normalized.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'); } export function buildPdf( team: Pick, rows: CashboxExportRow[], from: string, to: string, ): Promise { return new Promise((resolve, reject) => { const doc = new PDFDocument({ margin: 40 }); const chunks: Buffer[] = []; doc.on('data', (chunk) => chunks.push(chunk)); doc.on('end', () => resolve(Buffer.concat(chunks))); doc.on('error', reject); doc.fontSize(16).text(`Kassenbuch ${team.name}`); doc.fontSize(10).text(`Zeitraum: ${from} bis ${to}`); doc.moveDown(); if (rows.length === 0) { doc.text('Keine Buchungen im gewählten Zeitraum.'); } else { for (const row of rows) { doc.text( `${row.date.slice(0, 10)} ${TYPE_LABELS[row.type] ?? row.type} ${row.who} ${row.note} ` + `${formatGermanAmount(row.amount)} € Saldo: ${formatGermanAmount(row.runningTotal)} €`, ); } } doc.end(); }); }