ung der rückläufer
This commit is contained in:
parent
6927d0be3e
commit
ff7e7441f3
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user