Initial commit - Stand 26.04.2026
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
// 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 });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user