// PDF Export - Privat (René Täger) - EINE PDF pro Mieter 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]; // ALLE Positionen laden 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]); // Berechnungsgrundlagen const gesamtKosten = kostenResult.rows.reduce((sum, k) => sum + parseFloat(k.betrag), 0); const gesamtFlaeche = parseFloat(abrechnung.objekt_flaeche || 95); const kostenProQm = gesamtKosten / gesamtFlaeche; // PDF erstellen const pdfDoc = await PDFDocument.create(); const width = 595.28; const height = 841.89; const margin = 50; const font = await pdfDoc.embedFont(StandardFonts.Helvetica); const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold); const istEntwurf = req.query.entwurf === 'true'; const heute = new Date().toLocaleDateString('de-DE'); // Hilfsfunktion für Trennlinie function drawLine(page, yPos) { page.drawLine({ start: { x: margin, y: yPos }, end: { x: width - margin, y: yPos }, thickness: 0.5, color: rgb(0.8, 0.8, 0.8) }); } // ========== FÜR JEDEN MIETER EINE SEITE ========== for (const pos of positionenResult.rows) { const mieterName = pos.mieter_name; const mieterAdresse = pos.mieter_adresse || ''; const mieterFlaeche = parseFloat(pos.anteil_qm); const anteilKosten = parseFloat(pos.anteil_kosten); const vorauszahlungen = parseFloat(pos.summe_vorauszahlungen); const ergebnis = parseFloat(pos.ergebnis); // Neue Seite für diesen Mieter const page = pdfDoc.addPage([width, height]); let y = height - 50; // ========== ABSENDER (oben links) ========== page.drawText('René Täger', { x: margin, y, size: 10, font: fontBold }); y -= 14; page.drawText('Zingelstraße 7', { x: margin, y, size: 9, font }); y -= 14; page.drawText('25554 Wilster', { x: margin, y, size: 9, font }); y -= 30; // Trennlinie drawLine(page, y); y -= 30; // ========== EMPFÄNGER (nur dieser Mieter) ========== page.drawText(mieterName, { x: margin, y, size: 11, font: fontBold }); y -= 16; if (mieterAdresse) { const adressZeilen = mieterAdresse.split('\n'); for (const zeile of adressZeilen) { page.drawText(zeile, { x: margin, y, size: 10, font }); y -= 14; } } y -= 40; // ========== DATUM + BETREFF ========== page.drawText(`Wilster, den ${heute}`, { x: width - margin - 150, y: height - 120, size: 9, font }); page.drawText('Nebenkostenabrechnung', { x: margin, y, size: 16, font: fontBold, color: rgb(0.2, 0.4, 0.6) }); y -= 25; if (istEntwurf) { page.drawText('ENTWURF', { x: width - 140, y: y + 15, size: 20, font: fontBold, color: rgb(0.8, 0.3, 0.3) }); } page.drawText(`für das Jahr ${abrechnung.jahr}`, { x: margin, y, size: 12, font }); y -= 20; page.drawText(`Objekt: ${abrechnung.objekt_name}`, { x: margin, y, size: 11, font: fontBold }); y -= 16; page.drawText(`${abrechnung.adresse}, ${abrechnung.plz} ${abrechnung.ort}`, { x: margin, y, size: 10, font }); y -= 16; page.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 ========== page.drawText('Sehr geehrte(r) Mieter(in),', { x: margin, y, size: 10, font: fontBold }); y -= 18; page.drawText('im Folgenden erhalten Sie Ihre persönliche Nebenkostenabrechnung.', { x: margin, y, size: 9, font }); y -= 14; page.drawText('Die Kosten wurden nach Ihrem im Mietvertrag vereinbarten Anteil umgelegt.', { x: margin, y, size: 9, font }); y -= 30; // ========== 1. KOSTENÜBERSICHT (alle Kosten des Objekts) ========== page.drawText('1. Kostenübersicht des Objekts:', { x: margin, y, size: 12, font: fontBold, color: rgb(0.2, 0.4, 0.6) }); y -= 20; // Tabellenkopf page.drawText('Kategorie', { x: margin, y, size: 9, font: fontBold }); page.drawText('Bezeichnung', { x: margin + 100, y, size: 9, font: fontBold }); page.drawText('Betrag', { x: margin + 350, y, size: 9, font: fontBold }); y -= 12; drawLine(page, y + 8); y -= 5; for (const k of kostenResult.rows) { const betrag = parseFloat(k.betrag); page.drawText(k.kategorie, { x: margin, y, size: 9, font }); page.drawText(k.bezeichnung, { x: margin + 100, y, size: 9, font }); page.drawText(`${betrag.toFixed(2)} €`, { x: margin + 350, y, size: 9, font }); y -= 14; } y -= 5; drawLine(page, y + 8); y -= 5; page.drawText('Gesamtkosten:', { x: margin + 100, y, size: 10, font: fontBold }); page.drawText(`${gesamtKosten.toFixed(2)} €`, { x: margin + 350, y, size: 10, font: fontBold }); y -= 30; // ========== 2. BERECHNUNGSGRUNDLAGE ========== page.drawText('2. Berechnungsgrundlage:', { x: margin, y, size: 12, font: fontBold, color: rgb(0.2, 0.4, 0.6) }); y -= 20; page.drawText(`Gesamtkosten:`, { x: margin + 20, y, size: 9, font }); page.drawText(`${gesamtKosten.toFixed(2)} €`, { x: margin + 250, y, size: 9, font }); y -= 14; page.drawText(`Gesamtwohnfläche:`, { x: margin + 20, y, size: 9, font }); page.drawText(`${gesamtFlaeche.toFixed(2)} qm`, { x: margin + 250, y, size: 9, font }); y -= 14; page.drawText(`Kosten pro qm:`, { x: margin + 20, y, size: 9, font: fontBold }); page.drawText(`${gesamtKosten.toFixed(2)} € / ${gesamtFlaeche.toFixed(2)} qm = ${kostenProQm.toFixed(2)} €/qm`, { x: margin + 250, y, size: 9, font: fontBold }); y -= 30; // ========== 3. IHRE PERSÖNLICHE BERECHNUNG ========== page.drawText('3. Ihre persönliche Berechnung:', { x: margin, y, size: 12, font: fontBold, color: rgb(0.2, 0.4, 0.6) }); y -= 20; page.drawText(`Ihr Name:`, { x: margin + 20, y, size: 9, font }); page.drawText(mieterName, { x: margin + 250, y, size: 9, font: fontBold }); y -= 14; page.drawText(`Ihre Wohnfläche:`, { x: margin + 20, y, size: 9, font }); page.drawText(`${mieterFlaeche.toFixed(2)} qm`, { x: margin + 250, y, size: 9, font }); y -= 14; page.drawText(`Ihr Anteil an den Kosten:`, { x: margin + 20, y, size: 9, font }); page.drawText(`${kostenProQm.toFixed(2)} €/qm × ${mieterFlaeche.toFixed(2)} qm = ${anteilKosten.toFixed(2)} €`, { x: margin + 250, y, size: 9, font }); y -= 14; page.drawText(`Geleistete Vorauszahlungen:`, { x: margin + 20, y, size: 9, font }); page.drawText(`${vorauszahlungen.toFixed(2)} €`, { x: margin + 250, y, size: 9, font }); y -= 25; // Ergebnis hervorgehoben drawLine(page, y + 8); y -= 5; if (ergebnis > 0) { page.drawText(`Nachzahlung:`, { x: margin + 20, y, size: 11, font: fontBold }); page.drawText(`${anteilKosten.toFixed(2)} € - ${vorauszahlungen.toFixed(2)} € = ${ergebnis.toFixed(2)} €`, { x: margin + 250, y, size: 11, font: fontBold }); y -= 20; page.drawText(`Bitte überweisen Sie den Betrag innerhalb von 14 Tagen.`, { x: margin + 20, y, size: 9, font, color: rgb(0.8, 0.2, 0.2) }); } else { page.drawText(`Gutschrift:`, { x: margin + 20, y, size: 11, font: fontBold }); page.drawText(`${anteilKosten.toFixed(2)} € - ${vorauszahlungen.toFixed(2)} € = ${Math.abs(ergebnis).toFixed(2)} €`, { x: margin + 250, y, size: 11, font: fontBold }); y -= 20; page.drawText(`Der Betrag wird mit der nächsten Miete verrechnet.`, { x: margin + 20, y, size: 9, font, color: rgb(0.2, 0.6, 0.2) }); } y -= 25; // ========== FUSSZEILE ========== y -= 20; drawLine(page, y); y -= 20; page.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; page.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 }); } });