Feature: E-Mail-Versand Rechnungen – Texte, Status Finalisiert, SMTP-Einstellungen
This commit is contained in:
parent
4485acbe1f
commit
c50e41e57b
12 changed files with 212 additions and 97 deletions
3
migrations/040_invoice_status_finalisiert.sql
Normal file
3
migrations/040_invoice_status_finalisiert.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
ALTER TABLE invoices
|
||||||
|
MODIFY COLUMN status ENUM('Entwurf','Finalisiert','Gesendet','Bezahlt','Storniert')
|
||||||
|
NOT NULL DEFAULT 'Entwurf';
|
||||||
3
migrations/041_mail_templates.sql
Normal file
3
migrations/041_mail_templates.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
ALTER TABLE company_settings
|
||||||
|
ADD COLUMN IF NOT EXISTS mail_subject_template VARCHAR(500) NOT NULL DEFAULT 'Rechnung %RNR%' AFTER mail_from_email,
|
||||||
|
ADD COLUMN IF NOT EXISTS mail_body_template TEXT AFTER mail_subject_template;
|
||||||
|
|
@ -131,6 +131,7 @@ $router->get('/tax/euer/{year}/konto/{konto_id}', fn($p) => (new EuerController(
|
||||||
// ── Einstellungen ────────────────────────────────────────────
|
// ── Einstellungen ────────────────────────────────────────────
|
||||||
$router->get('/settings', fn($p) => (new SettingsController())->show($p));
|
$router->get('/settings', fn($p) => (new SettingsController())->show($p));
|
||||||
$router->post('/settings', fn($p) => (new SettingsController())->update($p));
|
$router->post('/settings', fn($p) => (new SettingsController())->update($p));
|
||||||
|
$router->post('/settings/mail-template', fn($p) => (new SettingsController())->mailTemplate($p));
|
||||||
$router->post('/settings/mail-test', fn($p) => (new SettingsController())->mailTest($p));
|
$router->post('/settings/mail-test', fn($p) => (new SettingsController())->mailTest($p));
|
||||||
$router->post('/settings/logo', fn($p) => (new SettingsController())->uploadLogo($p));
|
$router->post('/settings/logo', fn($p) => (new SettingsController())->uploadLogo($p));
|
||||||
$router->post('/settings/logo/delete', fn($p) => (new SettingsController())->deleteLogo($p));
|
$router->post('/settings/logo/delete', fn($p) => (new SettingsController())->deleteLogo($p));
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,25 @@ class SettingsController
|
||||||
View::redirect('/settings', 'Logo hochgeladen.');
|
View::redirect('/settings', 'Logo hochgeladen.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function mailTemplate(array $params): void
|
||||||
|
{
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
$subject = trim($_POST['mail_subject_template'] ?? '');
|
||||||
|
$body = trim($_POST['mail_body_template'] ?? '');
|
||||||
|
|
||||||
|
if ($subject === '') {
|
||||||
|
echo json_encode(['error' => 'Betreff darf nicht leer sein.']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
\Steuerkram\Database::execute(
|
||||||
|
'UPDATE company_settings SET mail_subject_template=?, mail_body_template=? WHERE id=1',
|
||||||
|
[$subject, $body ?: null]
|
||||||
|
);
|
||||||
|
|
||||||
|
echo json_encode(['ok' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
public function mailTest(array $params): void
|
public function mailTest(array $params): void
|
||||||
{
|
{
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ class Invoice
|
||||||
$fy = FiscalYear::findOrCreateByYear($year);
|
$fy = FiscalYear::findOrCreateByYear($year);
|
||||||
|
|
||||||
Database::execute(
|
Database::execute(
|
||||||
'UPDATE invoices SET rechnungs_nr=?, status=\'Gesendet\', fiscal_year_id=?, updated_at=NOW() WHERE id=?',
|
'UPDATE invoices SET rechnungs_nr=?, status=\'Finalisiert\', fiscal_year_id=?, updated_at=NOW() WHERE id=?',
|
||||||
[$nr, $fy['id'], $id]
|
[$nr, $fy['id'], $id]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ class Setting
|
||||||
mail_smtp_host=?, mail_smtp_port=?, mail_smtp_encryption=?,
|
mail_smtp_host=?, mail_smtp_port=?, mail_smtp_encryption=?,
|
||||||
mail_smtp_user=?, mail_smtp_pass=?,
|
mail_smtp_user=?, mail_smtp_pass=?,
|
||||||
mail_from_name=?, mail_from_email=?,
|
mail_from_name=?, mail_from_email=?,
|
||||||
|
mail_subject_template=?, mail_body_template=?,
|
||||||
logo_max_width=?, logo_max_height=?,
|
logo_max_width=?, logo_max_height=?,
|
||||||
logo_position=?, logo_margin_side=?, logo_margin_top=?
|
logo_position=?, logo_margin_side=?, logo_margin_top=?
|
||||||
WHERE id=1',
|
WHERE id=1',
|
||||||
|
|
@ -78,6 +79,8 @@ class Setting
|
||||||
$data['mail_smtp_pass'] ?: '',
|
$data['mail_smtp_pass'] ?: '',
|
||||||
$data['mail_from_name'] ?: '',
|
$data['mail_from_name'] ?: '',
|
||||||
$data['mail_from_email'] ?: '',
|
$data['mail_from_email'] ?: '',
|
||||||
|
$data['mail_subject_template'] ?: 'Rechnung %RNR%',
|
||||||
|
$data['mail_body_template'] ?: null,
|
||||||
max(50, (int)($data['logo_max_width'] ?? 600)),
|
max(50, (int)($data['logo_max_width'] ?? 600)),
|
||||||
max(20, (int)($data['logo_max_height'] ?? 100)),
|
max(20, (int)($data['logo_max_height'] ?? 100)),
|
||||||
$logoPosition,
|
$logoPosition,
|
||||||
|
|
|
||||||
|
|
@ -20,15 +20,19 @@ class EmailService
|
||||||
$pdfContent = $pdfService->renderToString($invoice, $lines, $settings);
|
$pdfContent = $pdfService->renderToString($invoice, $lines, $settings);
|
||||||
|
|
||||||
$nr = $invoice['rechnungs_nr'] ?? 'Entwurf';
|
$nr = $invoice['rechnungs_nr'] ?? 'Entwurf';
|
||||||
$firma = $settings['firma'] ?? '';
|
|
||||||
$subject = 'Rechnung ' . $nr;
|
|
||||||
$filename = 'Rechnung-' . $nr . '.pdf';
|
$filename = 'Rechnung-' . $nr . '.pdf';
|
||||||
|
|
||||||
$body = "Guten Tag,\n\n"
|
$defaultBody = "Guten Tag,\n\n"
|
||||||
. "anbei erhalten Sie die Rechnung {$nr} von {$firma}.\n\n"
|
. "anbei erhalten Sie die Rechnung %RNR% von %FNAME%.\n\n"
|
||||||
. "Bei Fragen stehen wir Ihnen gerne zur Verfügung.\n\n"
|
. "Bei Fragen stehen wir Ihnen gerne zur Verfügung.\n\n"
|
||||||
. "Mit freundlichen Grüßen\n"
|
. "Mit freundlichen Grüßen\n"
|
||||||
. $firma;
|
. "%INHABER%";
|
||||||
|
|
||||||
|
$subjectTpl = ($settings['mail_subject_template'] ?? '') ?: 'Rechnung %RNR%';
|
||||||
|
$bodyTpl = ($settings['mail_body_template'] ?? '') ?: $defaultBody;
|
||||||
|
|
||||||
|
$subject = $this->replacePlaceholders($subjectTpl, $invoice, $settings);
|
||||||
|
$body = $this->replacePlaceholders($bodyTpl, $invoice, $settings);
|
||||||
|
|
||||||
$mailer->send($recipients, $subject, $body, $pdfContent, $filename);
|
$mailer->send($recipients, $subject, $body, $pdfContent, $filename);
|
||||||
}
|
}
|
||||||
|
|
@ -48,6 +52,15 @@ class EmailService
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function replacePlaceholders(string $text, array $invoice, array $settings): string
|
||||||
|
{
|
||||||
|
return str_replace(
|
||||||
|
['%RNR%', '%FNAME%', '%INHABER%'],
|
||||||
|
[$invoice['rechnungs_nr'] ?? 'Entwurf', $settings['firma'] ?? '', $settings['inhaber'] ?? ''],
|
||||||
|
$text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private function buildMailer(array $settings): SmtpMailer
|
private function buildMailer(array $settings): SmtpMailer
|
||||||
{
|
{
|
||||||
$host = $settings['mail_smtp_host'] ?? '';
|
$host = $settings['mail_smtp_host'] ?? '';
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ $badgeClass = match($invoice['status']) {
|
||||||
'Bezahlt' => 'badge-green',
|
'Bezahlt' => 'badge-green',
|
||||||
'Storniert' => 'badge-red',
|
'Storniert' => 'badge-red',
|
||||||
'Gesendet' => 'badge-yellow',
|
'Gesendet' => 'badge-yellow',
|
||||||
|
'Finalisiert' => 'badge-blue',
|
||||||
default => 'badge-gray',
|
default => 'badge-gray',
|
||||||
};
|
};
|
||||||
?>
|
?>
|
||||||
|
|
@ -40,7 +41,7 @@ $badgeClass = match($invoice['status']) {
|
||||||
<?php if ($invoice['status'] === 'Gesendet'): ?>
|
<?php if ($invoice['status'] === 'Gesendet'): ?>
|
||||||
<a href="/invoices/<?= $h($invoice['id']) ?>/book" class="btn btn-success">Zahlungseingang erfassen</a>
|
<a href="/invoices/<?= $h($invoice['id']) ?>/book" class="btn btn-success">Zahlungseingang erfassen</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if (in_array($invoice['status'], ['Entwurf','Gesendet'])): ?>
|
<?php if (in_array($invoice['status'], ['Entwurf','Finalisiert','Gesendet'])): ?>
|
||||||
<form method="post" action="/invoices/<?= $h($invoice['id']) ?>/cancel"
|
<form method="post" action="/invoices/<?= $h($invoice['id']) ?>/cancel"
|
||||||
data-confirm="Rechnung stornieren?">
|
data-confirm="Rechnung stornieren?">
|
||||||
<button type="submit" class="btn btn-danger">Stornieren</button>
|
<button type="submit" class="btn btn-danger">Stornieren</button>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
<?php use Steuerkram\View; $h = fn($v) => View::h($v); ?>
|
<?php use Steuerkram\View; $h = fn($v) => View::h($v); ?>
|
||||||
|
|
||||||
<form id="settings-form" method="post" action="/settings">
|
<form id="settings-form" method="post" action="/settings">
|
||||||
|
<input type="hidden" id="hf-mail-subject" name="mail_subject_template"
|
||||||
|
value="<?= $h($settings['mail_subject_template'] ?? 'Rechnung %RNR%') ?>">
|
||||||
|
<input type="hidden" id="hf-mail-body" name="mail_body_template"
|
||||||
|
value="<?= $h($settings['mail_body_template'] ?? '') ?>">
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;align-items:start">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;align-items:start">
|
||||||
|
|
||||||
<!-- Firmendaten -->
|
<!-- Firmendaten -->
|
||||||
|
|
@ -81,9 +85,55 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-2">
|
||||||
|
<div class="card-header"><div class="card-title">Bankdaten</div></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Bankname</label>
|
||||||
|
<input type="text" name="bank_name" value="<?= $h($settings['bank_name'] ?? '') ?>">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>IBAN</label>
|
||||||
|
<input type="text" name="iban" value="<?= $h($settings['iban'] ?? '') ?>" placeholder="DE89 3704 0044 0532 0130 00">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>BIC</label>
|
||||||
|
<input type="text" name="bic" value="<?= $h($settings['bic'] ?? '') ?>">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bankdaten + Logo + Nummernkreise -->
|
<div class="card mb-2">
|
||||||
|
<div class="card-header"><div class="card-title">Nummernkreise</div></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-grid form-grid-2">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Rechnungs-Präfix</label>
|
||||||
|
<input type="text" name="invoice_nr_prefix" value="<?= $h($settings['invoice_nr_prefix'] ?? 'AR') ?>" maxlength="10">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Beleg-Präfix</label>
|
||||||
|
<input type="text" name="beleg_nr_prefix" value="<?= $h($settings['beleg_nr_prefix'] ?? 'BLG') ?>" maxlength="10">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Anlagen-Präfix</label>
|
||||||
|
<input type="text" name="anlage_nr_prefix" value="<?= $h($settings['anlage_nr_prefix'] ?? 'ANL') ?>" maxlength="10">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Kunden-Präfix</label>
|
||||||
|
<input type="text" name="customer_nr_prefix" value="<?= $h($settings['customer_nr_prefix'] ?? 'K-') ?>" maxlength="10">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Standard-Zahlungsziel (Tage)</label>
|
||||||
|
<input type="number" name="payment_days_default" value="<?= $h($settings['payment_days_default'] ?? 14) ?>" min="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- rechte Spalte -->
|
||||||
<div>
|
<div>
|
||||||
<!-- Betriebsmodus -->
|
<!-- Betriebsmodus -->
|
||||||
<div class="card mb-2" style="border:2px solid <?= ($settings['test_mode'] ?? 1) ? 'var(--color-warning,#f59e0b)' : 'var(--color-success,#22c55e)' ?>">
|
<div class="card mb-2" style="border:2px solid <?= ($settings['test_mode'] ?? 1) ? 'var(--color-warning,#f59e0b)' : 'var(--color-success,#22c55e)' ?>">
|
||||||
|
|
@ -210,52 +260,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card mb-2">
|
|
||||||
<div class="card-header"><div class="card-title">Bankdaten</div></div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Bankname</label>
|
|
||||||
<input type="text" name="bank_name" value="<?= $h($settings['bank_name'] ?? '') ?>">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>IBAN</label>
|
|
||||||
<input type="text" name="iban" value="<?= $h($settings['iban'] ?? '') ?>" placeholder="DE89 3704 0044 0532 0130 00">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>BIC</label>
|
|
||||||
<input type="text" name="bic" value="<?= $h($settings['bic'] ?? '') ?>">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card mb-2">
|
|
||||||
<div class="card-header"><div class="card-title">Nummernkreise</div></div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="form-grid form-grid-2">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Rechnungs-Präfix</label>
|
|
||||||
<input type="text" name="invoice_nr_prefix" value="<?= $h($settings['invoice_nr_prefix'] ?? 'AR') ?>" maxlength="10">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Beleg-Präfix</label>
|
|
||||||
<input type="text" name="beleg_nr_prefix" value="<?= $h($settings['beleg_nr_prefix'] ?? 'BLG') ?>" maxlength="10">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Anlagen-Präfix</label>
|
|
||||||
<input type="text" name="anlage_nr_prefix" value="<?= $h($settings['anlage_nr_prefix'] ?? 'ANL') ?>" maxlength="10">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Kunden-Präfix</label>
|
|
||||||
<input type="text" name="customer_nr_prefix" value="<?= $h($settings['customer_nr_prefix'] ?? 'K-') ?>" maxlength="10">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Standard-Zahlungsziel (Tage)</label>
|
|
||||||
<input type="number" name="payment_days_default" value="<?= $h($settings['payment_days_default'] ?? 14) ?>" min="0">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- E-Mail-Versand -->
|
<!-- E-Mail-Versand -->
|
||||||
<div class="card mb-2">
|
<div class="card mb-2">
|
||||||
<div class="card-header"><div class="card-title">E-Mail-Versand (SMTP)</div></div>
|
<div class="card-header"><div class="card-title">E-Mail-Versand (SMTP)</div></div>
|
||||||
|
|
@ -298,45 +302,15 @@
|
||||||
<input type="email" name="mail_from_email" value="<?= $h($settings['mail_from_email'] ?? '') ?>">
|
<input type="email" name="mail_from_email" value="<?= $h($settings['mail_from_email'] ?? '') ?>">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="display:flex;align-items:center;gap:.5rem;flex-wrap:wrap">
|
||||||
|
<button type="button" class="btn btn-secondary" id="btn-mail-text">Text bearbeiten</button>
|
||||||
<button type="button" class="btn btn-secondary" id="btn-mail-test">Test-Mail senden</button>
|
<button type="button" class="btn btn-secondary" id="btn-mail-test">Test-Mail senden</button>
|
||||||
<span id="mail-test-result" style="margin-left:.75rem;font-size:.8rem"></span>
|
<span id="mail-test-result" style="font-size:.8rem"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
<!-- Logo -->
|
||||||
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.getElementById('btn-mail-test')?.addEventListener('click', function () {
|
|
||||||
var btn = this;
|
|
||||||
var result = document.getElementById('mail-test-result');
|
|
||||||
btn.disabled = true;
|
|
||||||
result.textContent = 'Sende…';
|
|
||||||
result.style.color = '';
|
|
||||||
fetch('/settings/mail-test', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
|
||||||
.then(function (r) { return r.json(); })
|
|
||||||
.then(function (data) {
|
|
||||||
if (data.ok) {
|
|
||||||
result.textContent = '✓ Test-Mail erfolgreich gesendet.';
|
|
||||||
result.style.color = 'green';
|
|
||||||
} else {
|
|
||||||
result.textContent = '✗ ' + (data.error || 'Unbekannter Fehler');
|
|
||||||
result.style.color = 'red';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function () {
|
|
||||||
result.textContent = '✗ Netzwerkfehler';
|
|
||||||
result.style.color = 'red';
|
|
||||||
})
|
|
||||||
.finally(function () { btn.disabled = false; });
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;margin-top:0">
|
|
||||||
<div></div>
|
|
||||||
<div>
|
|
||||||
<div class="card mb-2">
|
<div class="card mb-2">
|
||||||
<div class="card-header"><div class="card-title">Logo</div></div>
|
<div class="card-header"><div class="card-title">Logo</div></div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
@ -407,8 +381,60 @@ document.getElementById('btn-mail-test')?.addEventListener('click', function ()
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Modal: E-Mail-Text bearbeiten -->
|
||||||
|
<div id="mail-text-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:1000;align-items:center;justify-content:center">
|
||||||
|
<div style="background:#fff;border-radius:8px;padding:1.5rem;width:520px;max-width:95vw;box-shadow:0 8px 32px rgba(0,0,0,.2)">
|
||||||
|
<h3 style="margin:0 0 1rem">E-Mail-Text bearbeiten</h3>
|
||||||
|
<p style="font-size:.78rem;color:#666;margin:0 0 1rem">
|
||||||
|
Platzhalter: <code>%RNR%</code> = Rechnungsnummer · <code>%FNAME%</code> = Firmenname · <code>%INHABER%</code> = Inhaber
|
||||||
|
</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Betreff</label>
|
||||||
|
<input type="text" id="mail-text-subject" style="width:100%"
|
||||||
|
value="<?= $h($settings['mail_subject_template'] ?? 'Rechnung %RNR%') ?>">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Text</label>
|
||||||
|
<textarea id="mail-text-body" rows="8" style="width:100%;resize:vertical;font-family:inherit;font-size:.85rem"><?= $h($settings['mail_body_template'] ?? '') ?></textarea>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:.75rem;justify-content:flex-end;margin-top:1.25rem">
|
||||||
|
<button type="button" class="btn btn-secondary" id="btn-mail-text-cancel">Abbrechen</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="btn-mail-text-save">Speichern</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('btn-mail-test')?.addEventListener('click', function () {
|
||||||
|
var btn = this;
|
||||||
|
var result = document.getElementById('mail-test-result');
|
||||||
|
btn.disabled = true;
|
||||||
|
result.textContent = 'Sende…';
|
||||||
|
result.style.color = '';
|
||||||
|
fetch('/settings/mail-test', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (data) {
|
||||||
|
if (data.ok) {
|
||||||
|
result.textContent = '✓ Test-Mail erfolgreich gesendet.';
|
||||||
|
result.style.color = 'green';
|
||||||
|
} else {
|
||||||
|
result.textContent = '✗ ' + (data.error || 'Unbekannter Fehler');
|
||||||
|
result.style.color = 'red';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
result.textContent = '✗ Netzwerkfehler';
|
||||||
|
result.style.color = 'red';
|
||||||
|
})
|
||||||
|
.finally(function () { btn.disabled = false; });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function updateModeLabel(cb) {
|
function updateModeLabel(cb) {
|
||||||
|
|
@ -442,4 +468,50 @@ function logoPositionChanged() {
|
||||||
document.getElementById('logo-margin-fields').style.display =
|
document.getElementById('logo-margin-fields').style.display =
|
||||||
(pos === 'left' || pos === 'right') ? 'block' : 'none';
|
(pos === 'left' || pos === 'right') ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// E-Mail-Text-Modal
|
||||||
|
document.getElementById('btn-mail-text')?.addEventListener('click', function () {
|
||||||
|
document.getElementById('mail-text-modal').style.display = 'flex';
|
||||||
|
});
|
||||||
|
document.getElementById('btn-mail-text-cancel')?.addEventListener('click', function () {
|
||||||
|
document.getElementById('mail-text-modal').style.display = 'none';
|
||||||
|
});
|
||||||
|
document.getElementById('btn-mail-text-save')?.addEventListener('click', function () {
|
||||||
|
var btn = this;
|
||||||
|
var subject = document.getElementById('mail-text-subject').value.trim();
|
||||||
|
var body = document.getElementById('mail-text-body').value;
|
||||||
|
var result = document.getElementById('mail-test-result');
|
||||||
|
|
||||||
|
if (!subject) {
|
||||||
|
document.getElementById('mail-text-subject').focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
var fd = new FormData();
|
||||||
|
fd.append('mail_subject_template', subject);
|
||||||
|
fd.append('mail_body_template', body);
|
||||||
|
|
||||||
|
fetch('/settings/mail-template', { method: 'POST', body: fd })
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (data) {
|
||||||
|
if (data.ok) {
|
||||||
|
// Versteckte Felder im Hauptformular synchron halten
|
||||||
|
document.getElementById('hf-mail-subject').value = subject;
|
||||||
|
document.getElementById('hf-mail-body').value = body;
|
||||||
|
document.getElementById('mail-text-modal').style.display = 'none';
|
||||||
|
result.textContent = '✓ E-Mail-Text gespeichert.';
|
||||||
|
result.style.color = 'green';
|
||||||
|
setTimeout(function () { result.textContent = ''; }, 3000);
|
||||||
|
} else {
|
||||||
|
result.textContent = '✗ ' + (data.error || 'Fehler beim Speichern.');
|
||||||
|
result.style.color = 'red';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
result.textContent = '✗ Netzwerkfehler.';
|
||||||
|
result.style.color = 'red';
|
||||||
|
})
|
||||||
|
.finally(function () { btn.disabled = false; });
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ class InvoiceBuchungTest extends TestCase
|
||||||
$this->assertNotEmpty($nr, 'Rechnung wurde finalisiert (Rechnungsnummer erhalten)');
|
$this->assertNotEmpty($nr, 'Rechnung wurde finalisiert (Rechnungsnummer erhalten)');
|
||||||
|
|
||||||
$invoice = \Steuerkram\Models\Invoice::find($invoiceId);
|
$invoice = \Steuerkram\Models\Invoice::find($invoiceId);
|
||||||
$this->assertEquals('Gesendet', $invoice['status'] ?? null, 'Rechnungsstatus ist Gesendet');
|
$this->assertEquals('Finalisiert', $invoice['status'] ?? null, 'Rechnungsstatus ist Finalisiert');
|
||||||
|
|
||||||
// Buchungen erzeugen (EÜR: Einnahme)
|
// Buchungen erzeugen (EÜR: Einnahme)
|
||||||
$belegNr = \Steuerkram\Models\Beleg::generateBelegNr();
|
$belegNr = \Steuerkram\Models\Beleg::generateBelegNr();
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ class InvoiceToReportsTest extends TestCase
|
||||||
$this->assertNotEmpty($rechnungsNr, 'Rechnungsnummer nach Finalisierung vorhanden');
|
$this->assertNotEmpty($rechnungsNr, 'Rechnungsnummer nach Finalisierung vorhanden');
|
||||||
|
|
||||||
$invoice = \Steuerkram\Models\Invoice::find($invoiceId);
|
$invoice = \Steuerkram\Models\Invoice::find($invoiceId);
|
||||||
$this->assertEquals('Gesendet', $invoice['status'] ?? null, 'Status nach Finalisierung: Gesendet');
|
$this->assertEquals('Finalisiert', $invoice['status'] ?? null, 'Status nach Finalisierung: Finalisiert');
|
||||||
|
|
||||||
// ── Beleg und Buchungen anlegen (wie InvoiceController::createBelegAndBookings) ──
|
// ── Beleg und Buchungen anlegen (wie InvoiceController::createBelegAndBookings) ──
|
||||||
$belegNr = \Steuerkram\Models\Beleg::generateBelegNr();
|
$belegNr = \Steuerkram\Models\Beleg::generateBelegNr();
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ class IstVersteuerungTest extends TestCase
|
||||||
$this->assertNotEmpty($nr, 'Rechnungsnummer vergeben');
|
$this->assertNotEmpty($nr, 'Rechnungsnummer vergeben');
|
||||||
|
|
||||||
$invoice = \Steuerkram\Models\Invoice::find($invoiceId);
|
$invoice = \Steuerkram\Models\Invoice::find($invoiceId);
|
||||||
$this->assertEquals('Gesendet', $invoice['status'] ?? null, 'Status nach Finalisierung: Gesendet');
|
$this->assertEquals('Finalisiert', $invoice['status'] ?? null, 'Status nach Finalisierung: Finalisiert');
|
||||||
|
|
||||||
// Beleg anlegen (wie in InvoiceController::createBeleg)
|
// Beleg anlegen (wie in InvoiceController::createBeleg)
|
||||||
$belegNr = \Steuerkram\Models\Beleg::generateBelegNr();
|
$belegNr = \Steuerkram\Models\Beleg::generateBelegNr();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue