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
{
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\Verified;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Auth\Notifications\VerifyEmail;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Gate;
@ -92,6 +95,33 @@ class AppServiceProvider extends ServiceProvider
Event::listen(Registered::class, SendEmailVerificationNotification::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.
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>
<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>
<x-emails.layout title="Bitte Passwort setzen – {{ config('app.name') }}" preheader="Ihr Account wurde übertragen – bitte einmalig ein neues Passwort setzen.">
<p>Hallo {{ $user->name }},</p>
<p>
@ -18,9 +7,9 @@
um sich künftig einzuloggen.
</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>
Alternativ können Sie diesen Link in Ihren Browser kopieren:<br>
@ -35,5 +24,4 @@
</p>
<p>Bei Fragen erreichen Sie uns unter {{ config('mail.from.address') }}.</p>
</div>
</body>
</html>
</x-emails.layout>

View file

@ -1,20 +1,13 @@
<!DOCTYPE html>
<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;">
<x-emails.layout title="Ihr Anmeldelink" preheader="Ihr Einmal-Link für den Login in Ihr pressekonto.">
<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>
<a href="{{ $loginUrl }}">Jetzt einloggen</a>
</p>
<x-emails.button :url="$loginUrl">Jetzt einloggen</x-emails.button>
<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>
</body>
</html>
<div class="note">
<p>Falls Sie den Login nicht angefordert haben, ignorieren Sie diese E-Mail bitte.</p>
</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>
<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>
<x-emails.layout title="Neue Kontaktanfrage" preheader="Über das Kontaktformular ist eine neue Anfrage eingegangen.">
<p>Über das Kontaktformular auf <strong>{{ $host }}</strong> ist eine neue Anfrage eingegangen:</p>
<dl class="meta">
@ -24,6 +10,7 @@
<div class="message">{{ $messageText }}</div>
<p class="note">Antworten gehen direkt an die Absenderadresse (Reply-To).</p>
</body>
</html>
<div class="note">
<p>Antworten gehen direkt an die Absenderadresse (Reply-To).</p>
</div>
</x-emails.layout>

View file

@ -1,23 +1,11 @@
<!DOCTYPE html>
<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>
<x-emails.layout title="Ihre Pressemitteilung wurde veröffentlicht" preheader="Ihre Pressemitteilung wurde geprüft und veröffentlicht.">
<p>Hallo {{ $user->name }},</p>
<p>Ihre Pressemitteilung wurde geprüft und erfolgreich veröffentlicht.</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)
<p>Veröffentlicht am: {{ $pressRelease->published_at->format('d.m.Y H:i') }} Uhr</p>
@ -26,5 +14,4 @@
<div class="note">
<p>Bei Fragen erreichen Sie uns unter {{ config('mail.from.address') }}.</p>
</div>
</body>
</html>
</x-emails.layout>

View file

@ -1,17 +1,4 @@
<!DOCTYPE html>
<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>
<x-emails.layout title="Ihre Pressemitteilung wurde abgelehnt" preheader="Ihre eingereichte Pressemitteilung konnte nicht veröffentlicht werden.">
<p>Hallo {{ $user->name }},</p>
<p>Ihre eingereichte Pressemitteilung konnte leider nicht veröffentlicht werden.</p>
@ -19,7 +6,7 @@
<p class="title">{{ $pressRelease->title }}</p>
@if($reason)
<div class="reason">
<div class="meta-danger">
<strong>Ablehnungsgrund:</strong><br>
{{ $reason }}
</div>
@ -27,10 +14,9 @@
<p>Bitte überarbeiten Sie den Inhalt und reichen Sie die Pressemitteilung erneut ein.</p>
@endif
<p><a class="btn" href="{{ $editUrl }}">Pressemitteilung bearbeiten</a></p>
<x-emails.button :url="$editUrl">Pressemitteilung bearbeiten</x-emails.button>
<div class="note">
<p>Bei Fragen erreichen Sie uns unter {{ config('mail.from.address') }}.</p>
</div>
</body>
</html>
</x-emails.layout>

View file

@ -1,17 +1,4 @@
<!DOCTYPE html>
<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>
<x-emails.layout title="Eingangsbestätigung Ihrer Kündigung" preheader="Wir bestätigen den Eingang Ihrer Kündigungserklärung.">
<p>Guten Tag {{ $terminationRequest->name }},</p>
<p>
@ -46,10 +33,11 @@
schnellstmöglich den Zeitpunkt, zu dem das Vertragsverhältnis endet.
</p>
<p class="note">
Diese Bestätigung wurde automatisch erstellt und dokumentiert Inhalt sowie Datum
und Uhrzeit des Zugangs Ihrer Kündigungserklärung (§ 312k BGB).
Bitte bewahren Sie sie auf.
</p>
</body>
</html>
<div class="note">
<p>
Diese Bestätigung wurde automatisch erstellt und dokumentiert Inhalt sowie Datum
und Uhrzeit des Zugangs Ihrer Kündigungserklärung (§ 312k BGB).
Bitte bewahren Sie sie auf.
</p>
</div>
</x-emails.layout>

View file

@ -1,20 +1,7 @@
<!DOCTYPE html>
<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>
<x-emails.layout title="Neue Online-Kündigung" preheader="Über den Kündigungsbutton ist eine neue Kündigung eingegangen.">
<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>
<dd>K-{{ $terminationRequest->id }}</dd>
<dt>Art der Kündigung</dt>
@ -35,9 +22,10 @@
<dd>{{ $terminationRequest->created_at->format('d.m.Y H:i') }} Uhr</dd>
</dl>
<p class="note">
Die Zugangsbestätigung an den Verbraucher wurde automatisch versendet (§ 312k Abs. 4 BGB).
Bitte den Vorgang dem Vertrag zuordnen und dem Kunden den Beendigungszeitpunkt bestätigen.
</p>
</body>
</html>
<div class="note">
<p>
Die Zugangsbestätigung an den Verbraucher wurde automatisch versendet (§ 312k Abs. 4 BGB).
Bitte den Vorgang dem Vertrag zuordnen und dem Kunden den Beendigungszeitpunkt bestätigen.
</p>
</div>
</x-emails.layout>

View file

@ -1,17 +1,4 @@
<!DOCTYPE html>
<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>
<x-emails.layout title="Eingangsbestätigung Ihres Widerrufs" preheader="Wir bestätigen den Eingang Ihres Widerrufs.">
<p>Guten Tag {{ $withdrawalRequest->name }},</p>
<p>
@ -38,9 +25,10 @@
schnellstmöglich mit den weiteren Schritten (z.&nbsp;B. zur Rückabwicklung) bei Ihnen.
</p>
<p class="note">
Diese Bestätigung wurde automatisch erstellt und dokumentiert den Zeitpunkt
des Eingangs Ihrer Widerrufserklärung. Bitte bewahren Sie sie auf.
</p>
</body>
</html>
<div class="note">
<p>
Diese Bestätigung wurde automatisch erstellt und dokumentiert den Zeitpunkt
des Eingangs Ihrer Widerrufserklärung. Bitte bewahren Sie sie auf.
</p>
</div>
</x-emails.layout>

View file

@ -1,20 +1,7 @@
<!DOCTYPE html>
<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>
<x-emails.layout title="Neuer Online-Widerruf" preheader="Über den Widerrufsbutton ist ein neuer Widerruf eingegangen.">
<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>
<dd>W-{{ $withdrawalRequest->id }}</dd>
<dt>Name</dt>
@ -27,10 +14,11 @@
<dd>{{ $withdrawalRequest->created_at->format('d.m.Y H:i') }} Uhr</dd>
</dl>
<p class="note">
Die Eingangsbestätigung an den Verbraucher wurde automatisch versendet.
Bitte den Vorgang dem Vertrag zuordnen und die Rückabwicklung anstoßen
(gesetzliche Frist beachten).
</p>
</body>
</html>
<div class="note">
<p>
Die Eingangsbestätigung an den Verbraucher wurde automatisch versendet.
Bitte den Vorgang dem Vertrag zuordnen und die Rückabwicklung anstoßen
(gesetzliche Frist beachten).
</p>
</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/');
});