This commit is contained in:
cay 2026-03-27 13:57:17 +00:00
parent 39228bcee5
commit b59f256e7b
3 changed files with 133 additions and 11 deletions

View File

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

View File

@ -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;

View File

@ -145,21 +145,35 @@
</td>
<td>
<div class="invoice-actions">
<a href="/admin/billing/export/pdf/<%= inv.id %>"
class="btn btn-sm btn-outline" target="_blank" title="PDF herunterladen">
📄 PDF
</a>
<% if (inv.status !== 'cancelled') { %>
<!-- Normale Rechnung PDF -->
<a href="/admin/billing/export/pdf/<%= inv.id %>"
class="btn btn-sm btn-outline" target="_blank">
📄 Rechnung
</a>
<% } else { %>
<!-- Storno PDF -->
<a href="/admin/billing/export/storno-pdf/<%= inv.id %>"
class="btn btn-sm btn-storno" target="_blank">
🚫 Storno-PDF
</a>
<!-- Neue Rechnung ausstellen -->
<form method="POST" action="/admin/billing/invoices/<%= inv.id %>/reissue" style="display:inline"
onsubmit="return confirm('Neue Rechnung für diesen Posten ausstellen?')">
<button type="submit" class="btn btn-sm btn-primary">🔄 Neue Rechnung</button>
</form>
<% } %>
<% if (inv.status === 'open') { %>
<form method="POST" action="/admin/billing/invoices/<%= inv.id %>/paid" style="display:inline">
<input type="hidden" name="period" value="<%= period %>">
<button type="submit" class="btn btn-sm btn-success" title="Als bezahlt markieren">✅</button>
<button type="submit" class="btn btn-sm btn-success">✅ Bezahlt</button>
</form>
<% } %>
<% if (inv.status !== 'cancelled') { %>
<form method="POST" action="/admin/billing/invoices/<%= inv.id %>/cancel" style="display:inline"
onsubmit="return confirm('Rechnung PF24-' + String(<%= inv.id %>).padStart(6,'0') + ' wirklich stornieren?')">
onsubmit="return confirm('Rechnung PF24-<%= String(inv.id).padStart(6,'0') %> wirklich stornieren?')">
<input type="hidden" name="period" value="<%= period %>">
<button type="submit" class="btn btn-sm btn-danger" title="Stornieren">🚫</button>
<button type="submit" class="btn btn-sm btn-danger">🚫 Stornieren</button>
</form>
<% } %>
</div>