diff --git a/app/Enums/ActivityType.php b/app/Enums/ActivityType.php index 3cbee23d..7565d7d5 100644 --- a/app/Enums/ActivityType.php +++ b/app/Enums/ActivityType.php @@ -20,6 +20,7 @@ enum ActivityType: string case TerminationRequest = 'termination_request'; case NewsletterSubscription = 'newsletter_subscription'; case SubscriptionChange = 'subscription_change'; + case MailSent = 'mail_sent'; public function label(): string { @@ -35,6 +36,7 @@ enum ActivityType: string self::TerminationRequest => 'Kündigung', self::NewsletterSubscription => 'Newsletter', self::SubscriptionChange => 'Abo', + self::MailSent => 'E-Mail', }; } @@ -50,6 +52,7 @@ enum ActivityType: string self::BookingPaid, self::CreditTopup => 'amber', self::ContactRequest, self::NewsletterSubscription => 'cyan', self::LegalRequest, self::WithdrawalRequest, self::TerminationRequest, self::SubscriptionChange => 'red', + self::MailSent => 'purple', }; } } diff --git a/app/Listeners/RecordMailActivity.php b/app/Listeners/RecordMailActivity.php new file mode 100644 index 00000000..66d09bae --- /dev/null +++ b/app/Listeners/RecordMailActivity.php @@ -0,0 +1,57 @@ + $address->getAddress(), + $event->message->getTo() ?? [], + )); + + if ($recipients === []) { + return; + } + + $subject = (string) $event->message->getSubject(); + $user = User::query()->whereIn('email', $recipients)->first(); + + $this->activityLog->record( + ActivityType::MailSent, + sprintf( + 'E-Mail „%s" an %s versendet.', + $subject !== '' ? $subject : __('(ohne Betreff)'), + implode(', ', $recipients), + ), + user: $user, + actorLabel: $user?->name ?? $recipients[0], + meta: array_filter([ + 'to' => $recipients, + 'subject' => $subject, + 'mailable' => $event->data['__laravel_mailable'] ?? null, + 'notification' => $event->data['__laravel_notification'] ?? null, + ], fn ($value): bool => $value !== null && $value !== ''), + ); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index f2e29799..65508399 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -11,6 +11,7 @@ use App\Http\Middleware\LogSlowAdminRequests; use App\Http\Middleware\SetEdition; use App\Listeners\ActivateUserAfterVerification; use App\Listeners\RecordAuthActivity; +use App\Listeners\RecordMailActivity; use App\Listeners\SyncCompanyMembershipsOnLogin; use App\Models\AdminPreset; use App\Models\Category; @@ -38,6 +39,7 @@ use Illuminate\Auth\Notifications\ResetPassword; use Illuminate\Auth\Notifications\VerifyEmail; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Events\QueryExecuted; +use Illuminate\Mail\Events\MessageSent; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Queue\Events\JobFailed; use Illuminate\Support\Facades\DB; @@ -129,6 +131,9 @@ class AppServiceProvider extends ServiceProvider Event::listen(Registered::class, [RecordAuthActivity::class, 'recordRegistered']); Event::listen(Login::class, [RecordAuthActivity::class, 'recordLogin']); + // Jede tatsächlich versendete E-Mail landet ebenfalls im Verlauf. + Event::listen(MessageSent::class, [RecordMailActivity::class, 'recordSent']); + // 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). diff --git a/resources/views/components/layouts/app/sidebar.blade.php b/resources/views/components/layouts/app/sidebar.blade.php index 1240ae53..d5c31815 100644 --- a/resources/views/components/layouts/app/sidebar.blade.php +++ b/resources/views/components/layouts/app/sidebar.blade.php @@ -96,8 +96,8 @@ - + {{ __('presseecho') }} + {{ __('Pressemitteilungen') }} + + {{ __('Firmen') }} + + + {{ __('Kontakte') }} + + @can('users:manage') + + {{ __('Benutzer') }} + + @endcan + + + + @can('compliance:manage') + + {{ __('Verlauf') }} + + {{-- Kontakt: Anliegen aus dem Kontakt-Widget (Widget + Hub-Formular) --}} + + {{ __('Anliegen') }} + + + {{-- Recht & Compliance --}} + + {{ __('Anfragen') }} + + @endcan + + + {{-- Billing --}} + @can('billing:manage') + + + {{ __('Legacy Rechnungen') }} + + + {{ __('Zahlungen') }} + + + {{ __('Tarife & Pakete') }} + + + {{ __('Gutscheine') }} + + + {{ __('Newsletter Sync') }} + + + @endcan + {{-- Content --}} + @can('categories:manage') @@ -129,77 +194,13 @@ @endcan - - {{-- CRM --}} - - - {{ __('Firmen') }} - - - {{ __('Kontakte') }} - - - - @can('compliance:manage') - {{-- Kontakt: Anliegen aus dem Kontakt-Widget (Widget + Hub-Formular) --}} - - - {{ __('Anliegen') }} - - - - {{-- Recht & Compliance --}} - - - {{ __('Anfragen') }} - - - @endcan - - {{-- Billing --}} - @can('billing:manage') - - - {{ __('Legacy Rechnungen') }} - - - {{ __('Zahlungen') }} - - - {{ __('Tarife & Pakete') }} - - - {{ __('Gutscheine') }} - - - {{ __('Newsletter Sync') }} - - - @endcan - {{-- Administration --}} - + {{ __('Voreinstellungen') }} - @can('users:manage') - - {{ __('Benutzer') }} - - @endcan + @can('roles:manage') @@ -210,10 +211,7 @@ {{-- Reports --}} - - {{ __('Verlauf') }} - + {{ __('Performance') }} diff --git a/resources/views/livewire/admin/reports/activity.blade.php b/resources/views/livewire/admin/reports/activity.blade.php index ab34c46f..656543fc 100644 --- a/resources/views/livewire/admin/reports/activity.blade.php +++ b/resources/views/livewire/admin/reports/activity.blade.php @@ -65,7 +65,7 @@ new #[Layout('components.layouts.app'), Title('Verlauf')] class extends Componen :badge="__('Admin Backend')" :eyebrow="__('Administration · Reports')" :title="__('Verlauf')" - :description="__('Chronologisches Protokoll der Systemereignisse: Registrierungen, Anmeldungen, Pressemitteilungen, Zahlungen und Formular-Eingänge.')" + :description="__('Chronologisches Protokoll der Systemereignisse: Registrierungen, Anmeldungen, Pressemitteilungen, Zahlungen, Formular-Eingänge und versendete E-Mails.')" /> {{-- ============== FILTER-PANEL ============== --}} diff --git a/tests/Feature/ActivityLogTest.php b/tests/Feature/ActivityLogTest.php index 3b45da9a..bea2e281 100644 --- a/tests/Feature/ActivityLogTest.php +++ b/tests/Feature/ActivityLogTest.php @@ -11,6 +11,7 @@ use App\Models\WithdrawalRequest; use App\Services\PressRelease\PressReleaseService; use Database\Seeders\RolesAndPermissionsSeeder; use Illuminate\Auth\Events\Login; +use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Notification; use Illuminate\Support\Facades\Queue; use Livewire\Volt\Volt; @@ -111,3 +112,30 @@ test('the activity report page lists events and filters by type', function () { ->assertSee('Kontaktanfrage „Testthema“') ->assertDontSee('hat sich angemeldet'); }); + +test('a sent mail is recorded and linked to the recipient user', function () { + $user = User::factory()->create(['email' => 'empfaenger@example.com']); + + Mail::raw('Inhalt', function ($message) { + $message->to('empfaenger@example.com')->subject('Willkommen bei pressekonto'); + }); + + $event = ActivityEvent::query()->where('type', ActivityType::MailSent->value)->sole(); + + expect($event->description)->toContain('Willkommen bei pressekonto') + ->and($event->description)->toContain('empfaenger@example.com') + ->and($event->user_id)->toBe($user->id) + ->and($event->meta['to'])->toBe(['empfaenger@example.com']) + ->and($event->meta['subject'])->toBe('Willkommen bei pressekonto'); +}); + +test('a mail to an unknown address is recorded with the address as actor label', function () { + Mail::raw('Inhalt', function ($message) { + $message->to('extern@example.com')->subject('Externe Info'); + }); + + $event = ActivityEvent::query()->where('type', ActivityType::MailSent->value)->sole(); + + expect($event->user_id)->toBeNull() + ->and($event->actor_label)->toBe('extern@example.com'); +});