diff --git a/migrations/040_invoice_status_finalisiert.sql b/migrations/040_invoice_status_finalisiert.sql new file mode 100644 index 0000000..67693f8 --- /dev/null +++ b/migrations/040_invoice_status_finalisiert.sql @@ -0,0 +1,3 @@ +ALTER TABLE invoices + MODIFY COLUMN status ENUM('Entwurf','Finalisiert','Gesendet','Bezahlt','Storniert') + NOT NULL DEFAULT 'Entwurf'; diff --git a/migrations/041_mail_templates.sql b/migrations/041_mail_templates.sql new file mode 100644 index 0000000..addafa2 --- /dev/null +++ b/migrations/041_mail_templates.sql @@ -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; diff --git a/public/index.php b/public/index.php index a8558ae..dc3b910 100644 --- a/public/index.php +++ b/public/index.php @@ -131,6 +131,7 @@ $router->get('/tax/euer/{year}/konto/{konto_id}', fn($p) => (new EuerController( // ── Einstellungen ──────────────────────────────────────────── $router->get('/settings', fn($p) => (new SettingsController())->show($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/logo', fn($p) => (new SettingsController())->uploadLogo($p)); $router->post('/settings/logo/delete', fn($p) => (new SettingsController())->deleteLogo($p)); diff --git a/src/Controllers/SettingsController.php b/src/Controllers/SettingsController.php index 2ad8598..56aaaf2 100644 --- a/src/Controllers/SettingsController.php +++ b/src/Controllers/SettingsController.php @@ -90,6 +90,25 @@ class SettingsController 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 { header('Content-Type: application/json'); diff --git a/src/Models/Invoice.php b/src/Models/Invoice.php index 8fbbc20..821fc6a 100644 --- a/src/Models/Invoice.php +++ b/src/Models/Invoice.php @@ -150,7 +150,7 @@ class Invoice $fy = FiscalYear::findOrCreateByYear($year); 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] ); diff --git a/src/Models/Setting.php b/src/Models/Setting.php index 3c3a964..289b763 100644 --- a/src/Models/Setting.php +++ b/src/Models/Setting.php @@ -39,6 +39,7 @@ class Setting mail_smtp_host=?, mail_smtp_port=?, mail_smtp_encryption=?, mail_smtp_user=?, mail_smtp_pass=?, mail_from_name=?, mail_from_email=?, + mail_subject_template=?, mail_body_template=?, logo_max_width=?, logo_max_height=?, logo_position=?, logo_margin_side=?, logo_margin_top=? WHERE id=1', @@ -78,6 +79,8 @@ class Setting $data['mail_smtp_pass'] ?: '', $data['mail_from_name'] ?: '', $data['mail_from_email'] ?: '', + $data['mail_subject_template'] ?: 'Rechnung %RNR%', + $data['mail_body_template'] ?: null, max(50, (int)($data['logo_max_width'] ?? 600)), max(20, (int)($data['logo_max_height'] ?? 100)), $logoPosition, diff --git a/src/Services/EmailService.php b/src/Services/EmailService.php index c7ae4fb..7941950 100644 --- a/src/Services/EmailService.php +++ b/src/Services/EmailService.php @@ -20,15 +20,19 @@ class EmailService $pdfContent = $pdfService->renderToString($invoice, $lines, $settings); $nr = $invoice['rechnungs_nr'] ?? 'Entwurf'; - $firma = $settings['firma'] ?? ''; - $subject = 'Rechnung ' . $nr; $filename = 'Rechnung-' . $nr . '.pdf'; - $body = "Guten Tag,\n\n" - . "anbei erhalten Sie die Rechnung {$nr} von {$firma}.\n\n" + $defaultBody = "Guten Tag,\n\n" + . "anbei erhalten Sie die Rechnung %RNR% von %FNAME%.\n\n" . "Bei Fragen stehen wir Ihnen gerne zur Verfügung.\n\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); } @@ -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 { $host = $settings['mail_smtp_host'] ?? ''; diff --git a/src/Views/invoices/show.php b/src/Views/invoices/show.php index 002fabb..f064f0c 100644 --- a/src/Views/invoices/show.php +++ b/src/Views/invoices/show.php @@ -10,10 +10,11 @@ $customerName = !empty($invoice['firma']) $lzHtml = $h($invoice['leistungszeitraum'] ?? ''); $badgeClass = match($invoice['status']) { - 'Bezahlt' => 'badge-green', - 'Storniert' => 'badge-red', - 'Gesendet' => 'badge-yellow', - default => 'badge-gray', + 'Bezahlt' => 'badge-green', + 'Storniert' => 'badge-red', + 'Gesendet' => 'badge-yellow', + 'Finalisiert' => 'badge-blue', + default => 'badge-gray', }; ?> @@ -40,7 +41,7 @@ $badgeClass = match($invoice['status']) { Zahlungseingang erfassen - +
- - - -