Files
buchhaltung/backend/routes/nebenkosten_pdf_v3.js.unix
T
2026-04-26 07:51:39 +02:00

219 lines
10 KiB
Plaintext

// PDF Export - Privat (René Täger) mit Rechenweg
app.post('/api/nebenkostenabrechnung/:id/pdf', async (req, res) => {
try {
const { PDFDocument, rgb, StandardFonts } = require('pdf-lib');
const abrechnungResult = await pool.query(`
SELECT na.*, o.name as objekt_name, o.adresse, o.plz, o.ort, o.wohnflaeche_qm as objekt_flaeche
FROM nebenkostenabrechnungen na
JOIN objekte o ON o.id = na.objekt_id
WHERE na.id = $1
`, [req.params.id]);
if (abrechnungResult.rows.length === 0) {
return res.status(404).json({ error: 'Abrechnung nicht gefunden' });
}
const abrechnung = abrechnungResult.rows[0];
const positionenResult = await pool.query(`
SELECT ap.*, m.name as mieter_name, m.email as mieter_email, m.adresse as mieter_adresse
FROM abrechnungspositionen ap
JOIN mieter m ON m.id = ap.mieter_id
WHERE ap.abrechnung_id = $1
`, [req.params.id]);
const kostenResult = await pool.query(`
SELECT * FROM objektkosten
WHERE objekt_id = $1 AND jahr = $2 ORDER BY kategorie
`, [abrechnung.objekt_id, abrechnung.jahr]);
// PDF erstellen
const pdfDoc = await PDFDocument.create();
const width = 595.28;
const height = 841.89;
let currentPage = pdfDoc.addPage([width, height]);
let y = height - 50;
const margin = 50;
const contentWidth = width - 2 * margin;
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
const istEntwurf = req.query.entwurf === 'true';
// Hilfsfunktion für Trennlinie
function drawLine(yPos) {
currentPage.drawLine({
start: { x: margin, y: yPos },
end: { x: width - margin, y: yPos },
thickness: 0.5,
color: rgb(0.8, 0.8, 0.8)
});
}
// ========== SEITE 1: ABSENDER + EMPFÄNGER + BETREFF ==========
// Absender (klein, oben links)
currentPage.drawText('René Täger', { x: margin, y, size: 10, font: fontBold });
y -= 14;
currentPage.drawText('Zingelstraße 7', { x: margin, y, size: 9, font });
y -= 14;
currentPage.drawText('25554 Wilster', { x: margin, y, size: 9, font });
y -= 30;
// Trennlinie
drawLine(y);
y -= 30;
// Empfänger (Mieter)
const empfaenger = positionenResult.rows[0];
if (empfaenger) {
currentPage.drawText(empfaenger.mieter_name || '', { x: margin, y, size: 11, font: fontBold });
y -= 16;
if (empfaenger.mieter_adresse) {
const adressZeilen = empfaenger.mieter_adresse.split('\n');
for (const zeile of adressZeilen) {
currentPage.drawText(zeile, { x: margin, y, size: 10, font });
y -= 14;
}
}
}
y -= 40;
// Datum + Betreff
const heute = new Date().toLocaleDateString('de-DE');
currentPage.drawText(`Wilster, den ${heute}`, { x: width - margin - 150, y: height - 120, size: 9, font });
currentPage.drawText('Nebenkostenabrechnung', { x: margin, y, size: 16, font: fontBold, color: rgb(0.2, 0.4, 0.6) });
y -= 25;
if (istEntwurf) {
currentPage.drawText('ENTWURF', { x: width - 140, y: y + 15, size: 20, font: fontBold, color: rgb(0.8, 0.3, 0.3) });
}
currentPage.drawText(`für das Jahr ${abrechnung.jahr}`, { x: margin, y, size: 12, font });
y -= 20;
currentPage.drawText(`Objekt: ${abrechnung.objekt_name}`, { x: margin, y, size: 11, font: fontBold });
y -= 16;
currentPage.drawText(`${abrechnung.adresse}, ${abrechnung.plz} ${abrechnung.ort}`, { x: margin, y, size: 10, font });
y -= 16;
currentPage.drawText(`Abrechnungszeitraum: ${new Date(abrechnung.zeitraum_von).toLocaleDateString('de-DE')} - ${new Date(abrechnung.zeitraum_bis).toLocaleDateString('de-DE')}`, { x: margin, y, size: 10, font });
y -= 35;
// ========== BEGLEITTEXT ==========
currentPage.drawText('Sehr geehrte(r) Mieter(in),', { x: margin, y, size: 10, font: fontBold });
y -= 18;
currentPage.drawText('im Folgenden erhalten Sie die Nebenkostenabrechnung für das abgelaufene Jahr.', { x: margin, y, size: 9, font });
y -= 14;
currentPage.drawText('Die Kosten wurden nach den im Mietvertrag vereinbarten Anteilen umgelegt.', { x: margin, y, size: 9, font });
y -= 30;
// ========== KOSTENÜBERSICHT ==========
currentPage.drawText('1. Kostenübersicht:', { x: margin, y, size: 12, font: fontBold, color: rgb(0.2, 0.4, 0.6) });
y -= 20;
// Tabellenkopf
currentPage.drawText('Kategorie', { x: margin, y, size: 9, font: fontBold });
currentPage.drawText('Bezeichnung', { x: margin + 100, y, size: 9, font: fontBold });
currentPage.drawText('Betrag', { x: margin + 350, y, size: 9, font: fontBold });
y -= 12;
drawLine(y + 8);
y -= 5;
let gesamtKosten = 0;
for (const k of kostenResult.rows) {
const betrag = parseFloat(k.betrag);
gesamtKosten += betrag;
currentPage.drawText(k.kategorie, { x: margin, y, size: 9, font });
currentPage.drawText(k.bezeichnung, { x: margin + 100, y, size: 9, font });
currentPage.drawText(`${betrag.toFixed(2)} €`, { x: margin + 350, y, size: 9, font });
y -= 14;
}
y -= 5;
drawLine(y + 8);
y -= 5;
currentPage.drawText('Gesamtkosten:', { x: margin + 100, y, size: 10, font: fontBold });
currentPage.drawText(`${gesamtKosten.toFixed(2)} €`, { x: margin + 350, y, size: 10, font: fontBold });
y -= 30;
// ========== RECHENWEG (Der Weg ist das Ziel!) ==========
currentPage.drawText('2. Berechnungsgrundlage:', { x: margin, y, size: 12, font: fontBold, color: rgb(0.2, 0.4, 0.6) });
y -= 20;
// Gesamtfläche berechnen
const gesamtFlaeche = parseFloat(abrechnung.objekt_flaeche || 95);
const kostenProQm = gesamtKosten / gesamtFlaeche;
currentPage.drawText(`Gesamtkosten:`, { x: margin + 20, y, size: 9, font });
currentPage.drawText(`${gesamtKosten.toFixed(2)} €`, { x: margin + 250, y, size: 9, font });
y -= 14;
currentPage.drawText(`Gesamtwohnfläche:`, { x: margin + 20, y, size: 9, font });
currentPage.drawText(`${gesamtFlaeche.toFixed(2)} qm`, { x: margin + 250, y, size: 9, font });
y -= 14;
currentPage.drawText(`Kosten pro qm:`, { x: margin + 20, y, size: 9, font: fontBold });
currentPage.drawText(`${gesamtKosten.toFixed(2)} € / ${gesamtFlaeche.toFixed(2)} qm = ${kostenProQm.toFixed(2)} €/qm`, { x: margin + 250, y, size: 9, font: fontBold });
y -= 30;
// ========== VERTEILUNG AUF MIETER ==========
currentPage.drawText('3. Verteilung auf Mieter:', { x: margin, y, size: 12, font: fontBold, color: rgb(0.2, 0.4, 0.6) });
y -= 20;
for (const pos of positionenResult.rows) {
const anteilKosten = parseFloat(pos.anteil_kosten);
const vorauszahlungen = parseFloat(pos.summe_vorauszahlungen);
const ergebnis = parseFloat(pos.ergebnis);
const mieterFlaeche = parseFloat(pos.anteil_qm);
currentPage.drawText(pos.mieter_name, { x: margin, y, size: 11, font: fontBold });
y -= 16;
// Rechenweg
currentPage.drawText(` Wohnfläche: ${mieterFlaeche.toFixed(2)} qm`, { x: margin + 15, y, size: 9, font });
y -= 14;
currentPage.drawText(` Berechnung: ${kostenProQm.toFixed(2)} €/qm × ${mieterFlaeche.toFixed(2)} qm = ${anteilKosten.toFixed(2)} €`, { x: margin + 15, y, size: 9, font });
y -= 14;
currentPage.drawText(` Geleistete Vorauszahlungen:`, { x: margin + 15, y, size: 9, font });
currentPage.drawText(`${vorauszahlungen.toFixed(2)} €`, { x: margin + 250, y, size: 9, font });
y -= 16;
// Ergebnis
if (ergebnis > 0) {
currentPage.drawText(` Ergebnis: ${anteilKosten.toFixed(2)} € - ${vorauszahlungen.toFixed(2)} € =`, { x: margin + 15, y, size: 9, font });
currentPage.drawText(`${ergebnis.toFixed(2)} €`, { x: margin + 250, y, size: 10, font: fontBold, color: rgb(0.8, 0.2, 0.2) });
y -= 14;
currentPage.drawText(` Nachzahlung erforderlich:`, { x: margin + 15, y, size: 10, font: fontBold });
currentPage.drawText(`${ergebnis.toFixed(2)} €`, { x: margin + 250, y, size: 10, font: fontBold, color: rgb(0.8, 0.2, 0.2) });
} else {
currentPage.drawText(` Ergebnis: ${anteilKosten.toFixed(2)} € - ${vorauszahlungen.toFixed(2)} € =`, { x: margin + 15, y, size: 9, font });
currentPage.drawText(`${Math.abs(ergebnis).toFixed(2)} €`, { x: margin + 250, y, size: 10, font: fontBold, color: rgb(0.2, 0.6, 0.2) });
y -= 14;
currentPage.drawText(` Gutschrift:`, { x: margin + 15, y, size: 10, font: fontBold });
currentPage.drawText(`${Math.abs(ergebnis).toFixed(2)} €`, { x: margin + 250, y, size: 10, font: fontBold, color: rgb(0.2, 0.6, 0.2) });
}
y -= 25;
}
// ========== FUSSZEILE ==========
y -= 20;
drawLine(y);
y -= 20;
currentPage.drawText('Diese Abrechnung wurde maschinell erstellt und ist ohne Unterschrift gültig.', { x: margin, y, size: 8, font, color: rgb(0.5, 0.5, 0.5) });
y -= 12;
currentPage.drawText('Bei Rückfragen: René Täger | Tel: +49 15563 717612 | E-Mail: info@taeger-it.de', { x: margin, y, size: 8, font, color: rgb(0.5, 0.5, 0.5) });
// PDF speichern
const pdfBytes = await pdfDoc.save();
const pdfBuffer = Buffer.from(pdfBytes.buffer || pdfBytes);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Length', pdfBuffer.length);
res.setHeader('Content-Disposition', `attachment; filename="nebenkostenabrechnung-${abrechnung.jahr}-${abrechnung.objekt_name.replace(/\s+/g, '_')}.pdf"`);
res.setHeader('Access-Control-Expose-Headers', 'Content-Disposition');
res.end(pdfBuffer);
} catch (error) {
console.error('PDF Error:', error);
res.status(500).json({ error: error.message });
}
});