202 lines
8.8 KiB
Plaintext
202 lines
8.8 KiB
Plaintext
// 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 });
|
|
}
|
|
}); |