ung der rückläufer

This commit is contained in:
cay 2026-03-27 13:36:30 +00:00
parent 6927d0be3e
commit ff7e7441f3
2 changed files with 199 additions and 1 deletions

View File

@ -248,4 +248,103 @@ router.post('/dunning/:id/cancel', requireAdmin, async (req, res) => {
}
});
// ============================================
// Alle offenen Rückläufer mahnen
// ============================================
router.post('/chargebacks/dunning-all', requireAdmin, async (req, res) => {
const { amount, issued_date, reason } = req.body;
try {
const [openCBs] = await db.query(
"SELECT * FROM chargebacks WHERE status = 'open'", []
);
if (openCBs.length === 0) {
return res.redirect('/admin/finance?error=Keine+offenen+Rückläufer+vorhanden');
}
let count = 0;
for (const cb of openCBs) {
await db.query(
'INSERT INTO dunning_fees (membership_id, amount, reason, issued_date, notes) VALUES (?,?,?,?,?)',
[cb.membership_id, amount, reason || 'Mahngebühr Rücklastschrift', issued_date,
`Automatisch aus Rückläufer vom ${new Date(cb.chargeback_date).toLocaleDateString('de-DE')}`]
);
count++;
}
res.redirect(`/admin/finance?success=${count}+Mahngebühren+eingetragen#chargebacks`);
} catch (err) {
console.error(err);
res.redirect('/admin/finance?error=Fehler+beim+Eintragen');
}
});
// Einzelne Mahngebühr über Rückläufer-ID (membership_id wird aus chargeback geholt)
router.post('/dunning/add-from-chargeback', requireAdmin, async (req, res) => {
const { chargeback_id, amount, issued_date, reason, notes } = req.body;
try {
const [cbs] = await db.query('SELECT * FROM chargebacks WHERE id = ?', [chargeback_id]);
if (cbs.length === 0) return res.redirect('/admin/finance?error=Rückläufer+nicht+gefunden');
await db.query(
'INSERT INTO dunning_fees (membership_id, amount, reason, issued_date, notes) VALUES (?,?,?,?,?)',
[cbs[0].membership_id, amount, reason || 'Mahngebühr Rücklastschrift', issued_date, notes || null]
);
res.redirect('/admin/finance?success=Mahngebühr+eingetragen');
} catch (err) {
res.redirect('/admin/finance?error=Fehler');
}
});
// ============================================
// SEPA Nachforderung: offene Rückläufer + Mahngebühren
// ============================================
router.get('/chargebacks/sepa-export', requireAdmin, async (req, res) => {
try {
// Offene Rückläufer
const [chargebacks] = await db.query(`
SELECT c.*, m.first_name, m.last_name, m.iban, m.account_holder
FROM chargebacks c
JOIN memberships m ON c.membership_id = m.id
WHERE c.status = 'open'
`);
// Offene Mahngebühren
const [dunnings] = await db.query(`
SELECT d.*, m.first_name, m.last_name, m.iban, m.account_holder
FROM dunning_fees d
JOIN memberships m ON d.membership_id = m.id
WHERE d.status = 'open'
`);
if (chargebacks.length === 0 && dunnings.length === 0) {
return res.redirect('/admin/finance?error=Keine+offenen+Rückläufer+oder+Mahngebühren');
}
const today = new Date().toISOString().split('T')[0];
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename="SEPA_Nachforderung_${today}.csv"`);
res.write('\uFEFF');
res.write('Name;IBAN;BIC;Betrag;Verwendungszweck;Mandatsreferenz;Mandatsdatum\n');
for (const c of chargebacks) {
const name = `${c.last_name} ${c.first_name}`.replace(/;/g, ' ');
const iban = (c.iban || '').replace(/\s/g, '');
const amount = Number(c.amount).toFixed(2).replace('.', ',');
const ref = `PF24-RL-${String(c.id).padStart(5,'0')}`;
res.write(`${name};${iban};;${amount};Nachforderung Rücklastschrift ${c.period};${ref};${today}\n`);
}
for (const d of dunnings) {
const name = `${d.last_name} ${d.first_name}`.replace(/;/g, ' ');
const iban = (d.iban || '').replace(/\s/g, '');
const amount = Number(d.amount).toFixed(2).replace('.', ',');
const ref = `PF24-MG-${String(d.id).padStart(5,'0')}`;
res.write(`${name};${iban};;${amount};${d.reason};${ref};${today}\n`);
}
res.end();
} catch (err) {
console.error(err);
res.redirect('/admin/finance?error=SEPA+Export+Fehler');
}
});
module.exports = router;

View File

@ -146,9 +146,11 @@
<div class="finance-card">
<div class="section-header">
<h3>Rückläufer</h3>
<div style="display:flex;gap:8px">
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="btn btn-warning btn-sm" onclick="toggleModal('allDunningModal')">📬 Alle mahnen</button>
<button class="btn btn-outline btn-sm" onclick="toggleModal('addChargebackModal')">+ Manuell eintragen</button>
<button class="btn btn-outline btn-sm" onclick="toggleModal('importChargebackModal')">📥 CSV Import</button>
<a href="/admin/finance/chargebacks/sepa-export" class="btn btn-primary btn-sm">📥 SEPA Nachforderung</a>
</div>
</div>
<% if (chargebacks.length === 0) { %>
@ -171,11 +173,17 @@
</span>
</td>
<td>
<div style="display:flex;gap:6px;flex-wrap:wrap">
<% if (c.status === 'open') { %>
<button class="btn btn-sm btn-warning"
onclick="openDunningModal(<%= c.id %>, '<%= c.last_name %>, <%= c.first_name %>', <%= c.amount %>, '<%= c.period %>')">
📬 Mahnen
</button>
<form method="POST" action="/admin/finance/chargebacks/<%= c.id %>/resolve" style="display:inline">
<button type="submit" class="btn btn-sm btn-success">✅ Erledigt</button>
</form>
<% } %>
</div>
</td>
</tr>
<% }) %>
@ -457,7 +465,89 @@
</div>
</div>
<!-- Modal: Einzelne Mahngebühr für Rückläufer -->
<div class="modal-overlay hidden" id="singleDunningModal">
<div class="modal">
<div class="modal-header">
<h3>Mahngebühr zuweisen</h3>
<button onclick="toggleModal('singleDunningModal')" class="modal-close">✕</button>
</div>
<form method="POST" action="/admin/finance/dunning/add-from-chargeback">
<input type="hidden" name="chargeback_id" id="dunning_membership_id">
<input type="hidden" name="invoice_id" value="">
<div class="form-group">
<label>Mitglied</label>
<input type="text" id="dunning_member_name" class="form-control" disabled>
</div>
<div class="form-row">
<div class="form-group">
<label>Mahngebühr (€) *</label>
<input type="number" name="amount" id="dunning_amount" step="0.01" min="0"
class="form-control" required>
</div>
<div class="form-group">
<label>Datum *</label>
<input type="date" name="issued_date" class="form-control" required
value="<%= new Date().toISOString().split('T')[0] %>">
</div>
</div>
<div class="form-group">
<label>Grund</label>
<input type="text" name="reason" class="form-control" value="Mahngebühr Rücklastschrift">
</div>
<div class="form-group">
<label>Notizen</label>
<textarea name="notes" class="form-control" rows="2"></textarea>
</div>
<div class="modal-footer">
<button type="button" onclick="toggleModal('singleDunningModal')" class="btn btn-outline">Abbrechen</button>
<button type="submit" class="btn btn-primary">📬 Mahngebühr eintragen</button>
</div>
</form>
</div>
</div>
<!-- Modal: Alle offenen Rückläufer mahnen -->
<div class="modal-overlay hidden" id="allDunningModal">
<div class="modal">
<div class="modal-header">
<h3>Alle offenen Rückläufer mahnen</h3>
<button onclick="toggleModal('allDunningModal')" class="modal-close">✕</button>
</div>
<form method="POST" action="/admin/finance/chargebacks/dunning-all">
<p style="font-size:0.9rem;margin-bottom:16px;color:var(--text-muted)">
Trägt für alle offenen Rückläufer automatisch eine Mahngebühr ein.
</p>
<div class="form-row">
<div class="form-group">
<label>Mahngebühr pro Rückläufer (€) *</label>
<input type="number" name="amount" step="0.01" min="0"
value="<%= Number(dunningFee).toFixed(2) %>" class="form-control" required>
</div>
<div class="form-group">
<label>Datum *</label>
<input type="date" name="issued_date" class="form-control" required
value="<%= new Date().toISOString().split('T')[0] %>">
</div>
</div>
<div class="form-group">
<label>Grund</label>
<input type="text" name="reason" class="form-control" value="Mahngebühr Rücklastschrift">
</div>
<div class="modal-footer">
<button type="button" onclick="toggleModal('allDunningModal')" class="btn btn-outline">Abbrechen</button>
<button type="submit" class="btn btn-warning"
onclick="return confirm('Für alle offenen Rückläufer eine Mahngebühr eintragen?')">
📬 Alle mahnen
</button>
</div>
</form>
</div>
</div>
<script>
const dunningDefaultFee = <%= Number(dunningFee).toFixed(2) %>;
// Chart
const ctx = document.getElementById('revenueChart').getContext('2d');
const chartData = <%- JSON.stringify(monthlyRevenue) %>;
@ -503,6 +593,15 @@ function showFileName(input) {
const display = document.getElementById('fileNameDisplay');
display.textContent = input.files.length > 0 ? input.files[0].name : 'Datei auswählen...';
}
function openDunningModal(chargebackId, memberName, amount, period) {
document.getElementById('dunning_member_name').value = memberName;
document.getElementById('dunning_amount').value = dunningDefaultFee;
// Set membership_id via chargeback - we pass it through a hidden field approach
// Store chargeback_id to look up membership on server
document.querySelector('#singleDunningModal input[name="membership_id"]').value = chargebackId;
toggleModal('singleDunningModal');
}
</script>
</body>
</html>