Email versnad
This commit is contained in:
parent
8c38fd2b5d
commit
5fd8f549bb
2
app.js
2
app.js
@ -37,6 +37,7 @@ const billingRouter = require('./routes/billing');
|
||||
const financeRouter = require('./routes/finance');
|
||||
const renewalRouter = require('./routes/renewal');
|
||||
const contractsRouter = require('./routes/contracts');
|
||||
const mailingRouter = require('./routes/mailing');
|
||||
const cron = require('node-cron');
|
||||
|
||||
app.use('/', indexRouter);
|
||||
@ -46,6 +47,7 @@ app.use('/admin/billing', billingRouter);
|
||||
app.use('/admin/finance', financeRouter);
|
||||
app.use('/', renewalRouter);
|
||||
app.use('/admin/contracts', contractsRouter);
|
||||
app.use('/admin/mailing', mailingRouter);
|
||||
|
||||
// 404 Handler
|
||||
app.use((req, res) => {
|
||||
|
||||
@ -1450,3 +1450,51 @@ body:not(.admin-body) > * {
|
||||
background: #f0f0ff;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
MAILING
|
||||
================================================ */
|
||||
.mailing-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 300px;
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
@media (max-width: 900px) { .mailing-layout { grid-template-columns: 1fr; } }
|
||||
|
||||
.mail-textarea {
|
||||
width: 100%;
|
||||
font-family: 'Outfit', sans-serif;
|
||||
font-size: 0.92rem;
|
||||
resize: vertical;
|
||||
min-height: 200px;
|
||||
}
|
||||
.mail-preview-hint {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 6px;
|
||||
}
|
||||
.mail-recipients { max-height: 500px; overflow-y: auto; }
|
||||
.recipients-list { margin-top: 12px; }
|
||||
.recipient-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.recipient-row:last-child { border-bottom: none; }
|
||||
.recipient-name { font-weight: 600; }
|
||||
.recipient-email { color: var(--text-muted); font-size: 0.8rem; }
|
||||
|
||||
.mail-type-badge {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
background: var(--bg);
|
||||
}
|
||||
.mail-type-bulk { background: #ede9fe; color: #5b21b6; }
|
||||
.mail-type-direct { background: #dbeafe; color: #1e40af; }
|
||||
.mail-type-renewal { background: #dcfce7; color: var(--success); }
|
||||
.mail-type-renewal_auto { background: #fef3c7; color: var(--warning); }
|
||||
|
||||
164
routes/mailing.js
Normal file
164
routes/mailing.js
Normal file
@ -0,0 +1,164 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../config/database');
|
||||
const mailer = require('../config/mailer');
|
||||
const { requireAdmin } = require('../middleware/auth');
|
||||
|
||||
// ============================================
|
||||
// GET /admin/mailing – Übersicht
|
||||
// ============================================
|
||||
router.get('/', requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const [members] = await db.query(`
|
||||
SELECT id, first_name, last_name, email, status,
|
||||
t.name as tariff_name
|
||||
FROM memberships m
|
||||
LEFT JOIN tariffs t ON m.tariff_id = t.id
|
||||
WHERE m.status = 'active'
|
||||
ORDER BY m.last_name ASC
|
||||
`);
|
||||
|
||||
const [log] = await db.query(`
|
||||
SELECT e.*, m.first_name, m.last_name
|
||||
FROM email_log e
|
||||
LEFT JOIN memberships m ON e.membership_id = m.id
|
||||
ORDER BY e.sent_at DESC
|
||||
LIMIT 50
|
||||
`);
|
||||
|
||||
res.render('admin/mailing', {
|
||||
admin: req.session.adminUser,
|
||||
members,
|
||||
log,
|
||||
success: req.query.success || null,
|
||||
error: req.query.error || null
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.redirect('/admin?error=' + encodeURIComponent('Fehler im Mailing: ' + err.message));
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// POST /admin/mailing/send-all – An alle aktiven
|
||||
// ============================================
|
||||
router.post('/send-all', requireAdmin, async (req, res) => {
|
||||
const { subject, body, include_name } = req.body;
|
||||
if (!subject || !body) return res.redirect('/admin/mailing?error=Betreff+und+Text+erforderlich');
|
||||
|
||||
try {
|
||||
const [members] = await db.query(
|
||||
"SELECT * FROM memberships WHERE status = 'active'"
|
||||
);
|
||||
|
||||
let sent = 0, failed = 0;
|
||||
|
||||
for (const member of members) {
|
||||
const personalBody = include_name
|
||||
? `Hallo ${member.first_name} ${member.last_name},\n\n${body}`
|
||||
: body;
|
||||
|
||||
const html = bodyToHtml(personalBody, subject);
|
||||
|
||||
try {
|
||||
await mailer.sendMail({
|
||||
from: process.env.MAIL_FROM,
|
||||
to: member.email,
|
||||
subject: subject,
|
||||
html: html
|
||||
});
|
||||
await db.query(
|
||||
'INSERT INTO email_log (membership_id, type, recipient, subject, status) VALUES (?,?,?,?,?)',
|
||||
[member.id, 'bulk', member.email, subject, 'sent']
|
||||
);
|
||||
sent++;
|
||||
} catch (err) {
|
||||
await db.query(
|
||||
'INSERT INTO email_log (membership_id, type, recipient, subject, status) VALUES (?,?,?,?,?)',
|
||||
[member.id, 'bulk', member.email, subject, 'failed']
|
||||
);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
res.redirect(`/admin/mailing?success=${sent}+E-Mails+gesendet${failed > 0 ? '+(' + failed + '+Fehler)' : ''}`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.redirect('/admin/mailing?error=' + encodeURIComponent(err.message));
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// POST /admin/mailing/send-one – An einzelnes Mitglied
|
||||
// ============================================
|
||||
router.post('/send-one', requireAdmin, async (req, res) => {
|
||||
const { membership_id, subject, body } = req.body;
|
||||
const backUrl = req.headers.referer || '/admin/mailing';
|
||||
|
||||
if (!membership_id || !subject || !body) {
|
||||
return res.redirect(backUrl + '?error=Alle+Felder+erforderlich');
|
||||
}
|
||||
|
||||
try {
|
||||
const [rows] = await db.query(
|
||||
'SELECT * FROM memberships WHERE id = ?', [membership_id]
|
||||
);
|
||||
if (rows.length === 0) return res.redirect(backUrl + '?error=Mitglied+nicht+gefunden');
|
||||
const member = rows[0];
|
||||
|
||||
const html = bodyToHtml(body, subject);
|
||||
|
||||
await mailer.sendMail({
|
||||
from: process.env.MAIL_FROM,
|
||||
to: member.email,
|
||||
subject: subject,
|
||||
html: html
|
||||
});
|
||||
|
||||
await db.query(
|
||||
'INSERT INTO email_log (membership_id, type, recipient, subject, status) VALUES (?,?,?,?,?)',
|
||||
[member.id, 'direct', member.email, subject, 'sent']
|
||||
);
|
||||
|
||||
res.redirect(backUrl + '?success=E-Mail+an+' + encodeURIComponent(member.email) + '+gesendet');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
await db.query(
|
||||
'INSERT INTO email_log (membership_id, type, recipient, subject, status) VALUES (?,?,?,?,?)',
|
||||
[membership_id, 'direct', '', subject, 'failed']
|
||||
).catch(() => {});
|
||||
res.redirect(backUrl + '?error=E-Mail+Fehler:+' + encodeURIComponent(err.message));
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// HTML Template
|
||||
// ============================================
|
||||
function bodyToHtml(text, subject) {
|
||||
const paragraphs = text.split('\n')
|
||||
.map(l => l.trim())
|
||||
.map(l => l ? `<p style="margin:0 0 12px;color:#374151;line-height:1.6">${l}</p>` : '<br>')
|
||||
.join('');
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head><meta charset="UTF-8"></head>
|
||||
<body style="font-family:Outfit,Arial,sans-serif;background:#f8f9ff;margin:0;padding:20px">
|
||||
<div style="max-width:600px;margin:0 auto;background:white;border-radius:16px;overflow:hidden;box-shadow:0 2px 16px rgba(0,0,0,0.08)">
|
||||
<div style="background:#2d2dcc;padding:28px 32px">
|
||||
<h1 style="color:white;margin:0;font-size:1.6rem">Plusfit<span style="color:#a5b4fc">24</span></h1>
|
||||
</div>
|
||||
<div style="padding:32px">
|
||||
<h2 style="margin:0 0 20px;color:#1a1a2e;font-size:1.2rem">${subject}</h2>
|
||||
${paragraphs}
|
||||
<hr style="border:none;border-top:1px solid #e2e4ed;margin:24px 0">
|
||||
<p style="font-size:0.82rem;color:#9ca3af;margin:0">
|
||||
PlusFit24 UG · Moosleiten 12 · 84089 Aiglsbach<br>
|
||||
Diese E-Mail wurde über das PlusFit24 Verwaltungssystem gesendet.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>`;
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
@ -19,7 +19,8 @@
|
||||
<a href="/admin/contracts" class="nav-link">📑 Verträge</a>
|
||||
<a href="/admin/billing" class="nav-link active">💶 Abrechnung</a>
|
||||
<a href="/admin/finance" class="nav-link">📊 Finanzen</a>
|
||||
<a href="/admin#einstellungen" class="nav-link">⚙️ Einstellungen</a>
|
||||
<a href="/admin/mailing" class="nav-link">📧 Mailing</a>
|
||||
<a href="/admin#einstellungen" class="nav-link">⚙️ Einstellungen</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<span>👤 <%= admin %></span>
|
||||
|
||||
@ -20,7 +20,8 @@
|
||||
<a href="/admin/contracts" class="nav-link active">📑 Verträge</a>
|
||||
<a href="/admin/billing" class="nav-link">💶 Abrechnung</a>
|
||||
<a href="/admin/finance" class="nav-link">📊 Finanzen</a>
|
||||
<a href="/admin#einstellungen" class="nav-link">⚙️ Einstellungen</a>
|
||||
<a href="/admin/mailing" class="nav-link">📧 Mailing</a>
|
||||
<a href="/admin#einstellungen" class="nav-link">⚙️ Einstellungen</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<span>👤 <%= admin %></span>
|
||||
|
||||
@ -20,7 +20,8 @@
|
||||
<a href="/admin/contracts" class="nav-link">📑 Verträge</a>
|
||||
<a href="/admin/billing" class="nav-link">💶 Abrechnung</a>
|
||||
<a href="/admin/finance" class="nav-link active">📊 Finanzen</a>
|
||||
<a href="/admin#einstellungen" class="nav-link">⚙️ Einstellungen</a>
|
||||
<a href="/admin/mailing" class="nav-link">📧 Mailing</a>
|
||||
<a href="/admin#einstellungen" class="nav-link">⚙️ Einstellungen</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<span>👤 <%= admin %></span>
|
||||
|
||||
187
views/admin/mailing.ejs
Normal file
187
views/admin/mailing.ejs
Normal file
@ -0,0 +1,187 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PlusFit24 – Mailing</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body class="admin-body">
|
||||
<div class="admin-layout">
|
||||
|
||||
<aside class="admin-sidebar">
|
||||
<div class="logo admin-logo">Plusfit<span>24</span></div>
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin" class="nav-link">📋 Tarife</a>
|
||||
<a href="/admin#kategorien" class="nav-link">🏷️ Kategorien</a>
|
||||
<a href="/admin#mitglieder" class="nav-link">👥 Mitglieder</a>
|
||||
<a href="/admin/contracts" class="nav-link">📑 Verträge</a>
|
||||
<a href="/admin/billing" class="nav-link">💶 Abrechnung</a>
|
||||
<a href="/admin/finance" class="nav-link">📊 Finanzen</a>
|
||||
<a href="/admin/mailing" class="nav-link active">📧 Mailing</a>
|
||||
<a href="/admin#einstellungen" class="nav-link">⚙️ Einstellungen</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<span>👤 <%= admin %></span>
|
||||
<a href="/admin/logout" class="logout-link">Abmelden</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="admin-main">
|
||||
<h1 class="finance-title">📧 Mailing</h1>
|
||||
|
||||
<% if (success) { %><div class="alert alert-success"><%= success %></div><% } %>
|
||||
<% if (error) { %><div class="alert alert-error"><%= error %></div><% } %>
|
||||
|
||||
<div class="finance-tabs">
|
||||
<button class="ftab active" onclick="showTab('bulk', this)">📢 An alle aktiven Mitglieder</button>
|
||||
<button class="ftab" onclick="showTab('single', this)">👤 An einzelnes Mitglied</button>
|
||||
<button class="ftab" onclick="showTab('log', this)">
|
||||
📋 Versandprotokoll
|
||||
<% if (log.length > 0) { %><span style="margin-left:6px;background:#e0e7ff;color:var(--primary);padding:1px 7px;border-radius:10px;font-size:0.75rem"><%= log.length %></span><% } %>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ===== TAB: AN ALLE ===== -->
|
||||
<div class="ftab-content active" id="tab-bulk">
|
||||
<div class="mailing-layout">
|
||||
|
||||
<!-- Formular -->
|
||||
<div class="finance-card" style="flex:1">
|
||||
<h3>E-Mail an alle aktiven Mitglieder (<%= members.length %>)</h3>
|
||||
<form method="POST" action="/admin/mailing/send-all"
|
||||
onsubmit="return confirm('E-Mail an <%= members.length %> aktive Mitglieder senden?')">
|
||||
<div class="form-group" style="margin-top:16px">
|
||||
<label>Betreff *</label>
|
||||
<input type="text" name="subject" class="form-control"
|
||||
placeholder="z.B. Wichtige Information zu deiner Mitgliedschaft" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" name="include_name" value="1" checked style="margin-right:6px">
|
||||
Persönliche Anrede ("Hallo Max Mustermann,") einfügen
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Nachricht *</label>
|
||||
<textarea name="body" class="form-control mail-textarea" rows="10"
|
||||
placeholder="Schreibe hier deine Nachricht..." required></textarea>
|
||||
</div>
|
||||
<div class="mail-preview-hint">
|
||||
💡 Zeilenumbrüche werden automatisch als Absätze formatiert.
|
||||
</div>
|
||||
<div style="display:flex;gap:10px;margin-top:16px">
|
||||
<button type="submit" class="btn btn-primary">📢 Jetzt an alle senden</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Empfängerliste -->
|
||||
<div class="finance-card mail-recipients">
|
||||
<h3>Empfänger (<%= members.length %>)</h3>
|
||||
<div class="recipients-list">
|
||||
<% members.forEach(m => { %>
|
||||
<div class="recipient-row">
|
||||
<span class="recipient-name"><%= m.last_name %>, <%= m.first_name %></span>
|
||||
<span class="recipient-email"><%= m.email %></span>
|
||||
</div>
|
||||
<% }) %>
|
||||
<% if (members.length === 0) { %>
|
||||
<p class="karte-empty">Keine aktiven Mitglieder.</p>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== TAB: EINZELN ===== -->
|
||||
<div class="ftab-content" id="tab-single">
|
||||
<div class="finance-card" style="max-width:680px">
|
||||
<h3>E-Mail an einzelnes Mitglied</h3>
|
||||
<form method="POST" action="/admin/mailing/send-one" style="margin-top:16px">
|
||||
<div class="form-group">
|
||||
<label>Mitglied *</label>
|
||||
<select name="membership_id" class="form-control" required>
|
||||
<option value="">– Mitglied wählen –</option>
|
||||
<% members.forEach(m => { %>
|
||||
<option value="<%= m.id %>"><%= m.last_name %>, <%= m.first_name %> · <%= m.email %></option>
|
||||
<% }) %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Betreff *</label>
|
||||
<input type="text" name="subject" class="form-control"
|
||||
placeholder="Betreff der E-Mail" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Nachricht *</label>
|
||||
<textarea name="body" class="form-control mail-textarea" rows="10"
|
||||
placeholder="Schreibe hier deine Nachricht..." required></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">📧 E-Mail senden</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== TAB: LOG ===== -->
|
||||
<div class="ftab-content" id="tab-log">
|
||||
<div class="finance-card">
|
||||
<h3>Versandprotokoll (letzte 50)</h3>
|
||||
<% if (log.length === 0) { %>
|
||||
<p class="karte-empty">Noch keine E-Mails gesendet.</p>
|
||||
<% } else { %>
|
||||
<div class="table-wrap" style="margin-top:12px">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Mitglied</th>
|
||||
<th>Empfänger</th>
|
||||
<th>Betreff</th>
|
||||
<th>Typ</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% log.forEach(entry => { %>
|
||||
<tr>
|
||||
<td><small><%= new Date(entry.sent_at).toLocaleString('de-DE') %></small></td>
|
||||
<td><%= entry.first_name ? entry.last_name + ', ' + entry.first_name : '–' %></td>
|
||||
<td><small class="text-muted"><%= entry.recipient %></small></td>
|
||||
<td><%= entry.subject %></td>
|
||||
<td>
|
||||
<span class="mail-type-badge mail-type-<%= entry.type %>">
|
||||
<%= entry.type === 'bulk' ? '📢 Rundmail' : entry.type === 'direct' ? '👤 Direkt' : entry.type === 'renewal' ? '🔄 Verlängerung' : entry.type === 'renewal_auto' ? '⏰ Auto' : '📧 ' + entry.type %>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="invoice-status <%= entry.status === 'sent' ? 'paid' : 'open' %>">
|
||||
<%= entry.status === 'sent' ? '✅ Gesendet' : '❌ Fehler' %>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showTab(name, el) {
|
||||
document.querySelectorAll('.ftab-content').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.ftab').forEach(t => t.classList.remove('active'));
|
||||
document.getElementById('tab-' + name).classList.add('active');
|
||||
if (el) el.classList.add('active');
|
||||
}
|
||||
const hash = window.location.hash.replace('#','');
|
||||
if (hash && document.getElementById('tab-' + hash)) showTab(hash);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -19,7 +19,8 @@
|
||||
<a href="/admin/contracts" class="nav-link">📑 Verträge</a>
|
||||
<a href="/admin/billing" class="nav-link">💶 Abrechnung</a>
|
||||
<a href="/admin/finance" class="nav-link">📊 Finanzen</a>
|
||||
<a href="/admin#einstellungen" class="nav-link">⚙️ Einstellungen</a>
|
||||
<a href="/admin/mailing" class="nav-link">📧 Mailing</a>
|
||||
<a href="/admin#einstellungen" class="nav-link">⚙️ Einstellungen</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<span>👤 <%= admin %></span>
|
||||
@ -47,6 +48,7 @@
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="detail-header-actions">
|
||||
<button class="btn btn-outline btn-sm" onclick="toggleModal('directMailModal')">📧 E-Mail senden</button>
|
||||
<button class="btn btn-primary" id="editBtn" onclick="enableEdit()">✏️ Bearbeiten</button>
|
||||
<button class="btn btn-success hidden" id="saveBtn" form="memberForm">💾 Speichern</button>
|
||||
<button class="btn btn-outline hidden" id="cancelBtn" onclick="cancelEdit()">✕ Abbrechen</button>
|
||||
@ -485,6 +487,36 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Direkte E-Mail -->
|
||||
<div class="modal-overlay hidden" id="directMailModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3>E-Mail an <%= member.first_name %> <%= member.last_name %></h3>
|
||||
<button onclick="toggleModal('directMailModal')" class="modal-close">✕</button>
|
||||
</div>
|
||||
<form method="POST" action="/admin/mailing/send-one">
|
||||
<input type="hidden" name="membership_id" value="<%= member.id %>">
|
||||
<div class="form-group">
|
||||
<label>Empfänger</label>
|
||||
<input type="text" value="<%= member.email %>" class="form-control" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Betreff *</label>
|
||||
<input type="text" name="subject" class="form-control" required placeholder="Betreff">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Nachricht *</label>
|
||||
<textarea name="body" class="form-control" rows="8" required
|
||||
placeholder="Schreibe hier deine Nachricht..."></textarea>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" onclick="toggleModal('directMailModal')" class="btn btn-outline">Abbrechen</button>
|
||||
<button type="submit" class="btn btn-primary">📧 Senden</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/iban.js"></script>
|
||||
<script>
|
||||
const editableFields = document.querySelectorAll('.karte-input:not(.karte-readonly)');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user