diff --git a/public/css/style.css b/public/css/style.css index 29bc200..0c259a4 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1289,3 +1289,15 @@ body:not(.admin-body) > * { height: 1px; background: var(--border); } + +/* Storno Button */ +.btn-storno { + background: #fff0f0; + color: var(--error); + border: 1.5px solid #fecaca; + font-weight: 700; +} +.btn-storno:hover { + background: #fee2e2; + border-color: var(--error); +} diff --git a/routes/billing.js b/routes/billing.js index f81ee8b..d750625 100644 --- a/routes/billing.js +++ b/routes/billing.js @@ -347,15 +347,111 @@ router.get('/export/pdf/:invoiceId', requireAdmin, async (req, res) => { router.post('/invoices/:id/cancel', requireAdmin, async (req, res) => { const period = req.body.period || currentPeriod(); try { - await db.query( - "UPDATE invoices SET status='cancelled' WHERE id=?", - [req.params.id] - ); + await db.query("UPDATE invoices SET status='cancelled' WHERE id=?", [req.params.id]); res.redirect(`/admin/billing?period=${period}&success=Rechnung+storniert`); } catch (err) { res.redirect(`/admin/billing?period=${period}&error=Fehler+beim+Stornieren`); } }); +// GET – Storno-PDF herunterladen +router.get('/export/storno-pdf/:invoiceId', requireAdmin, async (req, res) => { + try { + const [rows] = await db.query(` + SELECT i.*, + m.first_name, m.last_name, m.email, m.street, m.zip, m.city, + t.name as tariff_name + FROM invoices i + JOIN memberships m ON i.membership_id = m.id + LEFT JOIN tariffs t ON m.tariff_id = t.id + WHERE i.id = ? + `, [req.params.invoiceId]); + if (rows.length === 0) return res.status(404).send('Rechnung nicht gefunden'); + const inv = rows[0]; + + const PDFDocument = require('pdfkit'); + res.setHeader('Content-Type', 'application/pdf'); + res.setHeader('Content-Disposition', `attachment; filename="Storno_PF24-${String(inv.id).padStart(6,'0')}_${inv.period}.pdf"`); + + const doc = new PDFDocument({ margin: 60, size: 'A4' }); + doc.pipe(res); + + // Absender + doc.fontSize(10).fillColor('#666') + .text('PlusFit24 UG · Moosleiten 12 · 84089 Aiglsbach', 60, 60); + + // Empfänger + doc.fontSize(11).fillColor('#000') + .text(`${inv.first_name} ${inv.last_name}`, 60, 110) + .text(inv.street || '') + .text(`${inv.zip} ${inv.city}`); + + // Roter STORNO Stempel + doc.fontSize(28).fillColor('#dc2626') + .text('STORNORECHNUNG', 60, 215); + + doc.fontSize(10).fillColor('#333') + .text(`Storno-Nr.: STORNO-PF24-${String(inv.id).padStart(6,'0')}`, 60, 258) + .text(`Datum: ${new Date().toLocaleDateString('de-DE')}`) + .text(`Bezieht sich auf Rechnung: PF24-${String(inv.id).padStart(6,'0')} vom ${new Date(inv.created_at).toLocaleDateString('de-DE')}`) + .text(`Zeitraum: ${periodLabel(inv.period)}`); + + doc.moveTo(60, 318).lineTo(535, 318).strokeColor('#ddd').stroke(); + + doc.fontSize(10).fillColor('#999') + .text('Beschreibung', 60, 330) + .text('Betrag', 460, 330, { align: 'right', width: 75 }); + doc.moveTo(60, 345).lineTo(535, 345).strokeColor('#ddd').stroke(); + + doc.fontSize(11).fillColor('#dc2626') + .text(`Storno: ${inv.description || `Mitgliedsbeitrag ${periodLabel(inv.period)}`}`, 60, 355) + .text(`-${Number(inv.amount).toFixed(2).replace('.', ',')} €`, 460, 355, { align: 'right', width: 75 }); + + doc.moveTo(60, 378).lineTo(535, 378).strokeColor('#ddd').stroke(); + + doc.fontSize(12).fillColor('#dc2626') + .text('Stornobetrag:', 360, 390) + .text(`-${Number(inv.amount).toFixed(2).replace('.', ',')} €`, 460, 390, { align: 'right', width: 75 }); + + doc.fontSize(9).fillColor('#999') + .text('Alle Beträge inkl. MwSt. gem. § 19 UStG (Kleinunternehmerregelung)', 60, 415); + + doc.fontSize(9).fillColor('#999') + .text('PlusFit24 UG · Moosleiten 12 · 84089 Aiglsbach · Gläubiger-ID: DE1200100002549495', + 60, 730, { align: 'center', width: 475 }); + doc.end(); + } catch (err) { + console.error(err); + res.status(500).send('PDF Fehler: ' + err.message); + } +}); + +// POST – Neue Rechnung aus stornierter erstellen +router.post('/invoices/:id/reissue', requireAdmin, async (req, res) => { + try { + const [rows] = await db.query('SELECT * FROM invoices WHERE id = ?', [req.params.id]); + if (rows.length === 0) return res.redirect('/admin/billing?error=Rechnung+nicht+gefunden'); + const orig = rows[0]; + + // Neue Rechnung mit gleichen Daten aber neuer ID und Status open + const [result] = await db.query(` + INSERT INTO invoices (billing_run_id, membership_id, period, amount, description, iban, account_holder, bank_name, status) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'open') + `, [ + orig.billing_run_id, orig.membership_id, + orig.period, + orig.amount, + orig.description ? orig.description + ' (Neuausstellung)' : 'Neuausstellung', + orig.iban, orig.account_holder, orig.bank_name + ]); + + const newPeriod = orig.period; + res.redirect(`/admin/billing?period=${newPeriod}&success=Neue+Rechnung+PF24-${String(result.insertId).padStart(6,'0')}+erstellt`); + } catch (err) { + console.error(err); + res.redirect('/admin/billing?error=Fehler+bei+Neuausstellung'); + } +}); + module.exports = router; module.exports.currentPeriod = currentPeriod; diff --git a/views/admin/billing.ejs b/views/admin/billing.ejs index d69a05b..3cd6ebf 100644 --- a/views/admin/billing.ejs +++ b/views/admin/billing.ejs @@ -145,21 +145,35 @@
- - 📄 PDF - + <% if (inv.status !== 'cancelled') { %> + + + 📄 Rechnung + + <% } else { %> + + + 🚫 Storno-PDF + + +
+ +
+ <% } %> <% if (inv.status === 'open') { %>
- +
<% } %> <% if (inv.status !== 'cancelled') { %>
+ onsubmit="return confirm('Rechnung PF24-<%= String(inv.id).padStart(6,'0') %> wirklich stornieren?')"> - +
<% } %>