// PDF Export - Privat (René Täger) 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 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'; // ========== ABSENDER (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 currentPage.drawLine({ start: { x: margin, y }, end: { x: width - margin, y }, thickness: 1, color: rgb(0.7, 0.7, 0.7) }); 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('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; // Trennlinie currentPage.drawLine({ start: { x: margin, y: y + 8 }, end: { x: width - margin, y: y + 8 }, thickness: 0.5, color: rgb(0.8, 0.8, 0.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; currentPage.drawLine({ start: { x: margin, y: y + 8 }, end: { x: width - margin, y: y + 8 }, thickness: 0.5, color: rgb(0.8, 0.8, 0.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; // ========== VERTEILUNG AUF MIETER ========== currentPage.drawText('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); currentPage.drawText(pos.mieter_name, { x: margin, y, size: 11, font: fontBold }); y -= 16; currentPage.drawText(` Wohnfläche: ${pos.anteil_qm} qm | Zeitraum: ${pos.anteil_tage} Tage`, { x: margin + 15, y, size: 9, font }); y -= 14; currentPage.drawText(` Ihr Anteil an den Kosten:`, { x: margin + 15, y, size: 9, font }); currentPage.drawText(`${anteilKosten.toFixed(2)} €`, { x: margin + 250, 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; if (ergebnis > 0) { currentPage.drawText(` Nachzahlung:`, { 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(` 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; currentPage.drawLine({ start: { x: margin, y }, end: { x: width - margin, y }, thickness: 1, color: rgb(0.7, 0.7, 0.7) }); 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 }); } });