Files
2026-04-26 07:51:39 +02:00

219 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) 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 });
}
});