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

220 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 });
}
});