System-Mails im pressekonto-CI: zentrales E-Mail-Layout fuer alle Transaktionsmails

- Neues Layout components/emails/layout (Wortmarke, 'Publisher - Hub'-
  Tagline, Brand-Familien-Fusszeile) + Button-Komponente; alle System-
  Mails (Magic-Link, Go-Live-Reset, Kontakt, PM published/rejected,
  Kuendigung, Widerruf) darauf umgestellt
- Framework-Auth-Mails (Passwort-Reset, E-Mail-Verifizierung) via
  toMailUsing auf eigene Views im gleichen CI statt Laravel-Default
- Anmeldelink-Betreff vereinheitlicht; EmailBrandingTest sichert das
  Branding ueber alle Mails ab

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-03 16:26:30 +00:00
parent f0cd11aa57
commit 9832869596
16 changed files with 351 additions and 168 deletions

View file

@ -22,7 +22,7 @@ class MagicLoginLink extends Mailable
public function envelope(): Envelope public function envelope(): Envelope
{ {
return new Envelope( return new Envelope(
subject: 'Ihr Login-Link fuer pressekonto' subject: 'Ihr Anmeldelink für Ihr pressekonto'
); );
} }

View file

@ -27,7 +27,10 @@ use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Registered; use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Events\Verified; use Illuminate\Auth\Events\Verified;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification; use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Auth\Notifications\VerifyEmail;
use Illuminate\Database\Events\QueryExecuted; use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
@ -92,6 +95,33 @@ class AppServiceProvider extends ServiceProvider
Event::listen(Registered::class, SendEmailVerificationNotification::class); Event::listen(Registered::class, SendEmailVerificationNotification::class);
Event::listen(Verified::class, ActivateUserAfterVerification::class); Event::listen(Verified::class, ActivateUserAfterVerification::class);
// Framework-Auth-Mails (Passwort-Reset, E-Mail-Verifizierung) im
// pressekonto-CI rendern statt im Laravel-Standard-Markdown-Theme —
// gleiches Layout wie alle übrigen System-Mails (emails/layout).
ResetPassword::toMailUsing(function (User $user, string $token): MailMessage {
$resetUrl = url(route('password.reset', [
'token' => $token,
'email' => $user->getEmailForPasswordReset(),
], false));
return (new MailMessage)
->subject('Passwort zurücksetzen für Ihr pressekonto')
->view('emails.auth.reset-password', [
'user' => $user,
'resetUrl' => $resetUrl,
'expiresInMinutes' => (int) config('auth.passwords.'.config('auth.defaults.passwords').'.expire'),
]);
});
VerifyEmail::toMailUsing(function (User $user, string $verificationUrl): MailMessage {
return (new MailMessage)
->subject('E-Mail-Adresse bestätigen für Ihr pressekonto')
->view('emails.auth.verify-email', [
'user' => $user,
'verificationUrl' => $verificationUrl,
]);
});
// Bei jedem Login Firmen-/Kontakt-Zuordnungen (gleiche E-Mail) auffrischen. // Bei jedem Login Firmen-/Kontakt-Zuordnungen (gleiche E-Mail) auffrischen.
Event::listen(Login::class, SyncCompanyMembershipsOnLogin::class); Event::listen(Login::class, SyncCompanyMembershipsOnLogin::class);

View file

@ -0,0 +1,12 @@
@props([
'url',
])
{{-- E-Mail-tauglicher Button im Hub-CI (Tabellen-Konstrukt für Outlook). --}}
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="margin: 22px 0;">
<tr>
<td style="border-radius: 4px; background-color: #1A2540;">
<a href="{{ $url }}" style="display: inline-block; padding: 13px 30px; font-family: 'Inter Tight', 'Segoe UI', Arial, Helvetica, sans-serif; font-size: 14px; font-weight: bold; color: #ffffff; text-decoration: none; border-radius: 4px;">{{ $slot }}</a>
</td>
</tr>
</table>

View file

@ -0,0 +1,106 @@
@props([
'title' => null,
'preheader' => null,
])
@php
/**
* Zentrales CI-Layout für alle System-E-Mails (Auth, Pressemitteilungen,
* Kündigung/Widerruf, Kontakt). Absender-Marke ist immer pressekonto —
* der gemeinsame Publisher · Hub der Portale presseecho und
* businessportal24. Header und Footer erklären diese Zugehörigkeit,
* damit Empfänger die Mail sofort einordnen können.
*
* E-Mail-tauglich gebaut: Tabellen-Layout, Inline-Styles für alles
* Layout-Kritische, ergänzende Klassen im <style>-Block (werden von
* Gmail/Outlook.com/Apple Mail unterstützt; ohne sie bleibt die Mail
* trotzdem lesbar).
*/
$brand = config('domains.domains.pressekonto.brand', []);
$taglineShort = $brand['tagline_short'] ?? 'Publisher · Hub';
$footerLegal = str_replace(':year', (string) now()->year, $brand['footer_legal'] ?? '© '.now()->year.' pressekonto · Alle Rechte vorbehalten');
$presseechoUrl = config('domains.domain_presseecho_url');
$businessportalUrl = config('domains.domain_businessportal_url');
$fontStack = "'Inter Tight', 'Segoe UI', Arial, Helvetica, sans-serif";
@endphp
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light">
<title>{{ $title }}</title>
<style>
.email-body p { margin: 0 0 14px; }
.email-body .title { font-size: 17px; font-weight: bold; color: #1A2540; margin: 18px 0; padding-left: 14px; border-left: 3px solid #B07A3A; }
.email-body .meta { background: #f6f4ef; border-left: 3px solid #1A2540; padding: 14px 18px; margin: 18px 0; }
.email-body .meta-danger { background: #fdf2f0; border-left: 3px solid #b3401f; padding: 14px 18px; margin: 18px 0; }
.email-body .meta dt, .email-body .meta-danger dt { font-weight: bold; color: #1A2540; font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; }
.email-body .meta dd, .email-body .meta-danger dd { margin: 2px 0 10px; }
.email-body .message { white-space: pre-line; background: #f6f4ef; border: 1px solid #e8e4da; padding: 16px 18px; margin: 18px 0; }
.email-body .note { font-size: 12.5px; color: #6b7280; border-top: 1px solid #e8e4da; padding-top: 14px; margin-top: 26px; }
.email-body .note p { margin: 0 0 8px; }
.email-body a { color: #1A2540; }
</style>
</head>
<body style="margin: 0; padding: 0; background-color: #e8e4da;">
@if ($preheader)
{{-- Vorschautext im Posteingang; im Mail-Body unsichtbar. --}}
<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">{{ $preheader }}</div>
@endif
<table role="presentation" width="100%" border="0" cellpadding="0" cellspacing="0" style="background-color: #e8e4da;">
<tr>
<td align="center" style="padding: 32px 16px;">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%; max-width: 600px;">
{{-- Bernstein-Akzentstreifen (CI: 3px-Streifen des Hubs) --}}
<tr>
<td style="height: 3px; background-color: #B07A3A; font-size: 0; line-height: 0;">&nbsp;</td>
</tr>
{{-- Header: Wortmarke + Publisher · Hub in Hub-Blau --}}
<tr>
<td style="background-color: #1A2540; padding: 26px 36px;">
<span style="font-family: {{ $fontStack }}; font-size: 22px; font-weight: bold; letter-spacing: -0.4px; line-height: 1;"><span style="color: #ffffff;">{{ $brand['name'] ?? 'presse' }}</span><span style="color: #B07A3A;">{{ $brand['accent'] ?? 'konto' }}</span></span>
<div style="font-family: {{ $fontStack }}; font-size: 10px; font-weight: bold; letter-spacing: 0.22em; text-transform: uppercase; color: rgba(255, 255, 255, 0.65); margin-top: 8px;">
{{ $taglineShort }}
</div>
</td>
</tr>
{{-- Inhalt --}}
<tr>
<td class="email-body" style="background-color: #ffffff; padding: 32px 36px 28px; font-family: {{ $fontStack }}; font-size: 14.5px; line-height: 1.65; color: #2a3038;">
{{ $slot }}
</td>
</tr>
{{-- Brand-Familie: Zugehörigkeit von presseecho + businessportal24 --}}
<tr>
<td style="background-color: #ffffff; border-top: 1px solid #e8e4da; padding: 20px 36px 24px; font-family: {{ $fontStack }}; font-size: 12px; line-height: 1.7; color: #6b7280;">
<span style="font-weight: bold; color: #1A2540;">presse<span style="color: #B07A3A;">konto</span></span>
ist der gemeinsame Publisher&nbsp;·&nbsp;Hub für
<a href="{{ $presseechoUrl }}" style="font-weight: bold; text-decoration: none;"><span style="color: #1B2417;">presse</span><span style="color: #345636;">echo</span></a>
und
<a href="{{ $businessportalUrl }}" style="font-weight: bold; text-decoration: none;"><span style="color: #232A33;">businessportal</span><span style="color: #C84A1E;">24</span></a>
— ein Konto, redaktionell geprüft, auf beiden Portalen veröffentlichen.
</td>
</tr>
</table>
{{-- Rechtliches unterhalb der Karte --}}
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%; max-width: 600px;">
<tr>
<td align="center" style="padding: 18px 8px 6px; font-family: {{ $fontStack }}; font-size: 11.5px; color: #8a8f96;">
{{ $footerLegal }}
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View file

@ -1,15 +1,4 @@
<!DOCTYPE html> <x-emails.layout title="Bitte Passwort setzen – {{ config('app.name') }}" preheader="Ihr Account wurde übertragen – bitte einmalig ein neues Passwort setzen.">
<html lang="de">
<head>
<meta charset="utf-8">
<title>Bitte Passwort setzen – {{ config('app.name') }}</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
.btn { display: inline-block; background: #2563eb; color: #fff; padding: 12px 28px; border-radius: 6px; text-decoration: none; font-weight: bold; margin: 16px 0; }
.note { font-size: 13px; color: #666; border-top: 1px solid #eee; padding-top: 12px; margin-top: 24px; }
</style>
</head>
<body>
<p>Hallo {{ $user->name }},</p> <p>Hallo {{ $user->name }},</p>
<p> <p>
@ -18,9 +7,9 @@
um sich künftig einzuloggen. um sich künftig einzuloggen.
</p> </p>
<p>Klicken Sie auf den folgenden Link, um Ihr Passwort festzulegen:</p> <p>Klicken Sie auf den folgenden Button, um Ihr Passwort festzulegen:</p>
<p><a class="btn" href="{{ $resetUrl }}">Passwort jetzt setzen</a></p> <x-emails.button :url="$resetUrl">Passwort jetzt setzen</x-emails.button>
<p> <p>
Alternativ können Sie diesen Link in Ihren Browser kopieren:<br> Alternativ können Sie diesen Link in Ihren Browser kopieren:<br>
@ -35,5 +24,4 @@
</p> </p>
<p>Bei Fragen erreichen Sie uns unter {{ config('mail.from.address') }}.</p> <p>Bei Fragen erreichen Sie uns unter {{ config('mail.from.address') }}.</p>
</div> </div>
</body> </x-emails.layout>
</html>

View file

@ -1,20 +1,13 @@
<!DOCTYPE html> <x-emails.layout title="Ihr Anmeldelink" preheader="Ihr Einmal-Link für den Login in Ihr pressekonto.">
<html lang="de">
<head>
<meta charset="utf-8">
<title>Ihr Login-Link</title>
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.5;">
<p>Hallo {{ $user->name }},</p> <p>Hallo {{ $user->name }},</p>
<p>hier ist Ihr Einmal-Link fuer den Login in Ihr Kundenkonto.</p> <p>hier ist Ihr Einmal-Link für den Login in Ihr Kundenkonto.</p>
<p> <x-emails.button :url="$loginUrl">Jetzt einloggen</x-emails.button>
<a href="{{ $loginUrl }}">Jetzt einloggen</a>
</p>
<p>Der Link ist gueltig bis {{ $expiresAt }} Uhr und kann nur einmal verwendet werden.</p> <p>Der Link ist gültig bis {{ $expiresAt }} Uhr und kann nur einmal verwendet werden.</p>
<p>Falls Sie den Login nicht angefordert haben, ignorieren Sie diese E-Mail bitte.</p> <div class="note">
</body> <p>Falls Sie den Login nicht angefordert haben, ignorieren Sie diese E-Mail bitte.</p>
</html> </div>
</x-emails.layout>

View file

@ -0,0 +1,23 @@
<x-emails.layout title="Passwort zurücksetzen" preheader="Mit diesem Link können Sie ein neues Passwort für Ihr pressekonto vergeben.">
<p>Hallo {{ $user->name }},</p>
<p>
Sie erhalten diese E-Mail, weil für Ihr Konto ein neues Passwort angefordert wurde.
Klicken Sie auf den folgenden Button, um ein neues Passwort zu vergeben:
</p>
<x-emails.button :url="$resetUrl">Neues Passwort vergeben</x-emails.button>
<p>
Alternativ können Sie diesen Link in Ihren Browser kopieren:<br>
<small>{{ $resetUrl }}</small>
</p>
<div class="note">
<p>Der Link ist <strong>{{ $expiresInMinutes }} Minuten</strong> gültig.</p>
<p>
Falls Sie kein neues Passwort angefordert haben, ignorieren Sie diese E-Mail bitte —
Ihr Passwort bleibt unverändert.
</p>
</div>
</x-emails.layout>

View file

@ -0,0 +1,19 @@
<x-emails.layout title="E-Mail-Adresse bestätigen" preheader="Bitte bestätigen Sie Ihre E-Mail-Adresse, um Ihr pressekonto zu aktivieren.">
<p>Hallo {{ $user->name }},</p>
<p>
vielen Dank für Ihre Registrierung. Bitte bestätigen Sie Ihre E-Mail-Adresse,
um Ihr Konto zu aktivieren:
</p>
<x-emails.button :url="$verificationUrl">E-Mail-Adresse bestätigen</x-emails.button>
<p>
Alternativ können Sie diesen Link in Ihren Browser kopieren:<br>
<small>{{ $verificationUrl }}</small>
</p>
<div class="note">
<p>Falls Sie kein Konto erstellt haben, ignorieren Sie diese E-Mail bitte.</p>
</div>
</x-emails.layout>

View file

@ -1,18 +1,4 @@
<!DOCTYPE html> <x-emails.layout title="Neue Kontaktanfrage" preheader="Über das Kontaktformular ist eine neue Anfrage eingegangen.">
<html lang="de">
<head>
<meta charset="utf-8">
<title>Neue Kontaktanfrage</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
.meta { background: #f8fafc; border-left: 4px solid #2563eb; padding: 12px 16px; margin: 16px 0; }
.meta dt { font-weight: bold; color: #111; }
.meta dd { margin: 0 0 8px; }
.message { white-space: pre-line; border: 1px solid #eee; padding: 16px; }
.note { font-size: 13px; color: #666; border-top: 1px solid #eee; padding-top: 12px; margin-top: 24px; }
</style>
</head>
<body>
<p>Über das Kontaktformular auf <strong>{{ $host }}</strong> ist eine neue Anfrage eingegangen:</p> <p>Über das Kontaktformular auf <strong>{{ $host }}</strong> ist eine neue Anfrage eingegangen:</p>
<dl class="meta"> <dl class="meta">
@ -24,6 +10,7 @@
<div class="message">{{ $messageText }}</div> <div class="message">{{ $messageText }}</div>
<p class="note">Antworten gehen direkt an die Absenderadresse (Reply-To).</p> <div class="note">
</body> <p>Antworten gehen direkt an die Absenderadresse (Reply-To).</p>
</html> </div>
</x-emails.layout>

View file

@ -1,23 +1,11 @@
<!DOCTYPE html> <x-emails.layout title="Ihre Pressemitteilung wurde veröffentlicht" preheader="Ihre Pressemitteilung wurde geprüft und veröffentlicht.">
<html lang="de">
<head>
<meta charset="utf-8">
<title>Ihre Pressemitteilung wurde veröffentlicht</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
.btn { display: inline-block; background: #16a34a; color: #fff; padding: 12px 28px; border-radius: 6px; text-decoration: none; font-weight: bold; margin: 16px 0; }
.title { font-size: 18px; font-weight: bold; margin: 12px 0; color: #111; }
.note { font-size: 13px; color: #666; border-top: 1px solid #eee; padding-top: 12px; margin-top: 24px; }
</style>
</head>
<body>
<p>Hallo {{ $user->name }},</p> <p>Hallo {{ $user->name }},</p>
<p>Ihre Pressemitteilung wurde geprüft und erfolgreich veröffentlicht.</p> <p>Ihre Pressemitteilung wurde geprüft und erfolgreich veröffentlicht.</p>
<p class="title">{{ $pressRelease->title }}</p> <p class="title">{{ $pressRelease->title }}</p>
<p><a class="btn" href="{{ $showUrl }}">Pressemitteilung ansehen</a></p> <x-emails.button :url="$showUrl">Pressemitteilung ansehen</x-emails.button>
@if($pressRelease->published_at) @if($pressRelease->published_at)
<p>Veröffentlicht am: {{ $pressRelease->published_at->format('d.m.Y H:i') }} Uhr</p> <p>Veröffentlicht am: {{ $pressRelease->published_at->format('d.m.Y H:i') }} Uhr</p>
@ -26,5 +14,4 @@
<div class="note"> <div class="note">
<p>Bei Fragen erreichen Sie uns unter {{ config('mail.from.address') }}.</p> <p>Bei Fragen erreichen Sie uns unter {{ config('mail.from.address') }}.</p>
</div> </div>
</body> </x-emails.layout>
</html>

View file

@ -1,17 +1,4 @@
<!DOCTYPE html> <x-emails.layout title="Ihre Pressemitteilung wurde abgelehnt" preheader="Ihre eingereichte Pressemitteilung konnte nicht veröffentlicht werden.">
<html lang="de">
<head>
<meta charset="utf-8">
<title>Ihre Pressemitteilung wurde abgelehnt</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
.btn { display: inline-block; background: #2563eb; color: #fff; padding: 12px 28px; border-radius: 6px; text-decoration: none; font-weight: bold; margin: 16px 0; }
.title { font-size: 18px; font-weight: bold; margin: 12px 0; color: #111; }
.reason { background: #fef2f2; border-left: 4px solid #dc2626; padding: 12px 16px; margin: 16px 0; color: #7f1d1d; }
.note { font-size: 13px; color: #666; border-top: 1px solid #eee; padding-top: 12px; margin-top: 24px; }
</style>
</head>
<body>
<p>Hallo {{ $user->name }},</p> <p>Hallo {{ $user->name }},</p>
<p>Ihre eingereichte Pressemitteilung konnte leider nicht veröffentlicht werden.</p> <p>Ihre eingereichte Pressemitteilung konnte leider nicht veröffentlicht werden.</p>
@ -19,7 +6,7 @@
<p class="title">{{ $pressRelease->title }}</p> <p class="title">{{ $pressRelease->title }}</p>
@if($reason) @if($reason)
<div class="reason"> <div class="meta-danger">
<strong>Ablehnungsgrund:</strong><br> <strong>Ablehnungsgrund:</strong><br>
{{ $reason }} {{ $reason }}
</div> </div>
@ -27,10 +14,9 @@
<p>Bitte überarbeiten Sie den Inhalt und reichen Sie die Pressemitteilung erneut ein.</p> <p>Bitte überarbeiten Sie den Inhalt und reichen Sie die Pressemitteilung erneut ein.</p>
@endif @endif
<p><a class="btn" href="{{ $editUrl }}">Pressemitteilung bearbeiten</a></p> <x-emails.button :url="$editUrl">Pressemitteilung bearbeiten</x-emails.button>
<div class="note"> <div class="note">
<p>Bei Fragen erreichen Sie uns unter {{ config('mail.from.address') }}.</p> <p>Bei Fragen erreichen Sie uns unter {{ config('mail.from.address') }}.</p>
</div> </div>
</body> </x-emails.layout>
</html>

View file

@ -1,17 +1,4 @@
<!DOCTYPE html> <x-emails.layout title="Eingangsbestätigung Ihrer Kündigung" preheader="Wir bestätigen den Eingang Ihrer Kündigungserklärung.">
<html lang="de">
<head>
<meta charset="utf-8">
<title>Eingangsbestätigung Ihrer Kündigung</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
.meta { background: #f8fafc; border-left: 4px solid #2563eb; padding: 12px 16px; margin: 16px 0; }
.meta dt { font-weight: bold; color: #111; }
.meta dd { margin: 0 0 8px; }
.note { font-size: 13px; color: #666; border-top: 1px solid #eee; padding-top: 12px; margin-top: 24px; }
</style>
</head>
<body>
<p>Guten Tag {{ $terminationRequest->name }},</p> <p>Guten Tag {{ $terminationRequest->name }},</p>
<p> <p>
@ -46,10 +33,11 @@
schnellstmöglich den Zeitpunkt, zu dem das Vertragsverhältnis endet. schnellstmöglich den Zeitpunkt, zu dem das Vertragsverhältnis endet.
</p> </p>
<p class="note"> <div class="note">
Diese Bestätigung wurde automatisch erstellt und dokumentiert Inhalt sowie Datum <p>
und Uhrzeit des Zugangs Ihrer Kündigungserklärung (§ 312k BGB). Diese Bestätigung wurde automatisch erstellt und dokumentiert Inhalt sowie Datum
Bitte bewahren Sie sie auf. und Uhrzeit des Zugangs Ihrer Kündigungserklärung (§ 312k BGB).
</p> Bitte bewahren Sie sie auf.
</body> </p>
</html> </div>
</x-emails.layout>

View file

@ -1,20 +1,7 @@
<!DOCTYPE html> <x-emails.layout title="Neue Online-Kündigung" preheader="Über den Kündigungsbutton ist eine neue Kündigung eingegangen.">
<html lang="de">
<head>
<meta charset="utf-8">
<title>Neue Online-Kündigung</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
.meta { background: #fef2f2; border-left: 4px solid #dc2626; padding: 12px 16px; margin: 16px 0; }
.meta dt { font-weight: bold; color: #111; }
.meta dd { margin: 0 0 8px; }
.note { font-size: 13px; color: #666; border-top: 1px solid #eee; padding-top: 12px; margin-top: 24px; }
</style>
</head>
<body>
<p>Über den Kündigungsbutton auf <strong>{{ $terminationRequest->host }}</strong> ist eine neue Kündigung eingegangen:</p> <p>Über den Kündigungsbutton auf <strong>{{ $terminationRequest->host }}</strong> ist eine neue Kündigung eingegangen:</p>
<dl class="meta"> <dl class="meta-danger">
<dt>Vorgangsnummer</dt> <dt>Vorgangsnummer</dt>
<dd>K-{{ $terminationRequest->id }}</dd> <dd>K-{{ $terminationRequest->id }}</dd>
<dt>Art der Kündigung</dt> <dt>Art der Kündigung</dt>
@ -35,9 +22,10 @@
<dd>{{ $terminationRequest->created_at->format('d.m.Y H:i') }} Uhr</dd> <dd>{{ $terminationRequest->created_at->format('d.m.Y H:i') }} Uhr</dd>
</dl> </dl>
<p class="note"> <div class="note">
Die Zugangsbestätigung an den Verbraucher wurde automatisch versendet (§ 312k Abs. 4 BGB). <p>
Bitte den Vorgang dem Vertrag zuordnen und dem Kunden den Beendigungszeitpunkt bestätigen. Die Zugangsbestätigung an den Verbraucher wurde automatisch versendet (§ 312k Abs. 4 BGB).
</p> Bitte den Vorgang dem Vertrag zuordnen und dem Kunden den Beendigungszeitpunkt bestätigen.
</body> </p>
</html> </div>
</x-emails.layout>

View file

@ -1,17 +1,4 @@
<!DOCTYPE html> <x-emails.layout title="Eingangsbestätigung Ihres Widerrufs" preheader="Wir bestätigen den Eingang Ihres Widerrufs.">
<html lang="de">
<head>
<meta charset="utf-8">
<title>Eingangsbestätigung Ihres Widerrufs</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
.meta { background: #f8fafc; border-left: 4px solid #2563eb; padding: 12px 16px; margin: 16px 0; }
.meta dt { font-weight: bold; color: #111; }
.meta dd { margin: 0 0 8px; }
.note { font-size: 13px; color: #666; border-top: 1px solid #eee; padding-top: 12px; margin-top: 24px; }
</style>
</head>
<body>
<p>Guten Tag {{ $withdrawalRequest->name }},</p> <p>Guten Tag {{ $withdrawalRequest->name }},</p>
<p> <p>
@ -38,9 +25,10 @@
schnellstmöglich mit den weiteren Schritten (z.&nbsp;B. zur Rückabwicklung) bei Ihnen. schnellstmöglich mit den weiteren Schritten (z.&nbsp;B. zur Rückabwicklung) bei Ihnen.
</p> </p>
<p class="note"> <div class="note">
Diese Bestätigung wurde automatisch erstellt und dokumentiert den Zeitpunkt <p>
des Eingangs Ihrer Widerrufserklärung. Bitte bewahren Sie sie auf. Diese Bestätigung wurde automatisch erstellt und dokumentiert den Zeitpunkt
</p> des Eingangs Ihrer Widerrufserklärung. Bitte bewahren Sie sie auf.
</body> </p>
</html> </div>
</x-emails.layout>

View file

@ -1,20 +1,7 @@
<!DOCTYPE html> <x-emails.layout title="Neuer Online-Widerruf" preheader="Über den Widerrufsbutton ist ein neuer Widerruf eingegangen.">
<html lang="de">
<head>
<meta charset="utf-8">
<title>Neuer Online-Widerruf</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
.meta { background: #fef2f2; border-left: 4px solid #dc2626; padding: 12px 16px; margin: 16px 0; }
.meta dt { font-weight: bold; color: #111; }
.meta dd { margin: 0 0 8px; }
.note { font-size: 13px; color: #666; border-top: 1px solid #eee; padding-top: 12px; margin-top: 24px; }
</style>
</head>
<body>
<p>Über den Widerrufsbutton auf <strong>{{ $withdrawalRequest->host }}</strong> ist ein neuer Widerruf eingegangen:</p> <p>Über den Widerrufsbutton auf <strong>{{ $withdrawalRequest->host }}</strong> ist ein neuer Widerruf eingegangen:</p>
<dl class="meta"> <dl class="meta-danger">
<dt>Vorgangsnummer</dt> <dt>Vorgangsnummer</dt>
<dd>W-{{ $withdrawalRequest->id }}</dd> <dd>W-{{ $withdrawalRequest->id }}</dd>
<dt>Name</dt> <dt>Name</dt>
@ -27,10 +14,11 @@
<dd>{{ $withdrawalRequest->created_at->format('d.m.Y H:i') }} Uhr</dd> <dd>{{ $withdrawalRequest->created_at->format('d.m.Y H:i') }} Uhr</dd>
</dl> </dl>
<p class="note"> <div class="note">
Die Eingangsbestätigung an den Verbraucher wurde automatisch versendet. <p>
Bitte den Vorgang dem Vertrag zuordnen und die Rückabwicklung anstoßen Die Eingangsbestätigung an den Verbraucher wurde automatisch versendet.
(gesetzliche Frist beachten). Bitte den Vorgang dem Vertrag zuordnen und die Rückabwicklung anstoßen
</p> (gesetzliche Frist beachten).
</body> </p>
</html> </div>
</x-emails.layout>

View file

@ -0,0 +1,100 @@
<?php
use App\Mail\ContactRequestSubmitted;
use App\Mail\GoLivePasswordReset;
use App\Mail\MagicLoginLink;
use App\Mail\PressReleasePublished;
use App\Mail\PressReleaseRejected;
use App\Mail\TerminationRequestReceived;
use App\Mail\TerminationRequestSubmitted;
use App\Mail\WithdrawalRequestReceived;
use App\Mail\WithdrawalRequestSubmitted;
use App\Models\PressRelease;
use App\Models\TerminationRequest;
use App\Models\User;
use App\Models\WithdrawalRequest;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Auth\Notifications\VerifyEmail;
/**
* Alle System-Mails müssen im pressekonto-CI erscheinen: Wortmarke,
* „Publisher · Hub"-Tagline und die Zugehörigkeits-Erklärung zur
* Brand-Familie (presseecho + businessportal24) aus dem zentralen
* E-Mail-Layout (components/emails/layout).
*/
function assertHubEmailBranding(string $html): void
{
expect($html)
->toContain('Publisher · Hub')
->toContain('presseecho')
->toContain('businessportal24')
->toContain('ist der gemeinsame Publisher')
->toContain('#1A2540')
->toContain('#B07A3A');
}
test('the magic login link mail uses the hub ci layout', function () {
$user = User::factory()->make();
assertHubEmailBranding((new MagicLoginLink($user, 'https://pressekonto.test/magic-login/abc', '12.07.2026 10:00'))->render());
});
test('the go-live password reset mail uses the hub ci layout', function () {
$user = User::factory()->make();
assertHubEmailBranding((new GoLivePasswordReset($user, 'https://pressekonto.test/reset-password/abc'))->render());
});
test('the press release published and rejected mails use the hub ci layout', function () {
$pressRelease = PressRelease::factory()->published()->create();
$user = $pressRelease->user;
assertHubEmailBranding((new PressReleasePublished($user, $pressRelease))->render());
assertHubEmailBranding((new PressReleaseRejected($user, $pressRelease, 'Bitte Quellen ergänzen.'))->render());
});
test('the termination mails use the hub ci layout', function () {
$terminationRequest = TerminationRequest::factory()->create();
assertHubEmailBranding((new TerminationRequestReceived($terminationRequest))->render());
assertHubEmailBranding((new TerminationRequestSubmitted($terminationRequest))->render());
});
test('the withdrawal mails use the hub ci layout', function () {
$withdrawalRequest = WithdrawalRequest::factory()->create();
assertHubEmailBranding((new WithdrawalRequestReceived($withdrawalRequest))->render());
assertHubEmailBranding((new WithdrawalRequestSubmitted($withdrawalRequest))->render());
});
test('the contact request mail uses the hub ci layout', function () {
assertHubEmailBranding(
(new ContactRequestSubmitted('Paula Presse', 'paula@example.test', 'Eine Frage zur Plattform.', 'pressekonto.test'))->render()
);
});
test('the password reset notification renders in the hub ci layout', function () {
$user = User::factory()->create();
$mailMessage = (new ResetPassword('test-token'))->toMail($user);
expect($mailMessage->subject)->toBe('Passwort zurücksetzen für Ihr pressekonto');
$html = (string) $mailMessage->render();
assertHubEmailBranding($html);
expect($html)->toContain('/reset-password/test-token');
});
test('the email verification notification renders in the hub ci layout', function () {
$user = User::factory()->unverified()->create();
$mailMessage = (new VerifyEmail)->toMail($user);
expect($mailMessage->subject)->toBe('E-Mail-Adresse bestätigen für Ihr pressekonto');
$html = (string) $mailMessage->render();
assertHubEmailBranding($html);
expect($html)->toContain('/verify-email/');
});