srtzj
This commit is contained in:
parent
39228bcee5
commit
b59f256e7b
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user