presseportale/resources/views/livewire/admin/newsletter/campaigns/index.blade.php
Kevin Adametz 215cbcbdbf admin: internes Newsletter-Modul mit Filtern, Protokoll und Abmeldung
Neues Modul unter Admin ▸ Newsletter (nur users:manage): Kampagnen mit
Betreff + leicht formatiertem Text (Purifier-Profil newsletter: fett/
kursiv, Listen, Links) im zentralen Mail-Layout. Empfänger-Filter auf
der User-Datenbank: Portal, Legacy-Vertrag (Bestandsschutz), Login
innerhalb/außerhalb X Monaten, Sortierung nach Login/Erstellung — mit
Live-Zähler und Vorschau im Editor.

Versand friert die Empfängerliste als Protokoll ein (newsletter_
campaign_recipients) und stellt pro Empfänger einen Queue-Job ein;
der Opt-out wird im Worker erneut geprüft. Jede Mail trägt den
signierten One-Click-Abmelde-Link (neuer EmailNotificationType::
Newsletter, bestehende Unsubscribe-Infrastruktur inkl. RFC 8058);
die Abmeldung greift sofort für alle künftigen Auswahlen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 09:26:57 +00:00

118 lines
6 KiB
PHP

<?php
use App\Models\NewsletterCampaign;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Volt\Component;
use Livewire\WithPagination;
new #[Layout('components.layouts.app'), Title('Newsletter')] class extends Component
{
use WithPagination;
public function with(): array
{
return [
'campaigns' => NewsletterCampaign::query()
->with('createdBy:id,name')
->orderByDesc('created_at')
->paginate(25),
];
}
}; ?>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<x-portal.page-header
:badge="__('Admin Backend')"
:eyebrow="__('Administration · Newsletter')"
:title="__('Newsletter-Kampagnen')"
:description="__('Interne Mailings an Bestands-User auf Basis der hinterlegten E-Mail-Adressen — mit Empfänger-Filtern, Versand-Protokoll und Abmelde-Link in jeder Mail.')"
>
<x-slot:actions>
<flux:button variant="primary" icon="plus" href="{{ route('admin.newsletter.campaigns.create') }}" wire:navigate>
{{ __('Neue Kampagne') }}
</flux:button>
</x-slot:actions>
</x-portal.page-header>
{{-- ============== KAMPAGNEN ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Kampagnen') }}</span>
<span class="badge hub">{{ number_format($campaigns->total(), 0, ',', '.') }} {{ __('Einträge') }}</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Erstellt') }}</flux:table.column>
<flux:table.column>{{ __('Betreff') }}</flux:table.column>
<flux:table.column>{{ __('Status') }}</flux:table.column>
<flux:table.column>{{ __('Empfänger') }}</flux:table.column>
<flux:table.column>{{ __('Angelegt von') }}</flux:table.column>
<flux:table.column></flux:table.column>
</flux:table.columns>
<flux:table.rows>
@forelse ($campaigns as $campaign)
<flux:table.row :key="$campaign->id">
<flux:table.cell class="whitespace-nowrap">
<span class="text-[12px] text-[color:var(--color-ink-2)] tabular-nums">
{{ $campaign->created_at->format('d.m.Y H:i') }}
</span>
</flux:table.cell>
<flux:table.cell>
<a href="{{ route('admin.newsletter.campaigns.show', $campaign->id) }}" wire:navigate
class="text-[12.5px] font-semibold text-[color:var(--color-ink)] hover:underline">
{{ $campaign->subject }}
</a>
</flux:table.cell>
<flux:table.cell>
<flux:badge size="sm" :color="$campaign->status->badgeColor()">{{ $campaign->status->label() }}</flux:badge>
</flux:table.cell>
<flux:table.cell class="whitespace-nowrap">
@if ($campaign->isDraft())
<span class="text-[12px] text-[color:var(--color-ink-3)]">—</span>
@else
<span class="text-[12px] text-[color:var(--color-ink-2)] tabular-nums">
{{ number_format($campaign->sent_count, 0, ',', '.') }} / {{ number_format($campaign->recipients_total, 0, ',', '.') }}
</span>
@if ($campaign->failed_count > 0)
<flux:badge size="sm" color="red">{{ $campaign->failed_count }} {{ __('Fehler') }}</flux:badge>
@endif
@if ($campaign->skipped_count > 0)
<flux:badge size="sm" color="amber">{{ $campaign->skipped_count }} {{ __('abgemeldet') }}</flux:badge>
@endif
@endif
</flux:table.cell>
<flux:table.cell class="whitespace-nowrap">
<span class="text-[12.5px] text-[color:var(--color-ink-3)]">{{ $campaign->createdBy?->name ?? '—' }}</span>
</flux:table.cell>
<flux:table.cell class="whitespace-nowrap text-right">
@if ($campaign->isDraft())
<flux:button size="xs" variant="filled" icon="pencil" href="{{ route('admin.newsletter.campaigns.edit', $campaign->id) }}" wire:navigate>
{{ __('Bearbeiten') }}
</flux:button>
@else
<flux:button size="xs" variant="filled" icon="eye" href="{{ route('admin.newsletter.campaigns.show', $campaign->id) }}" wire:navigate>
{{ __('Protokoll') }}
</flux:button>
@endif
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="6">
<div class="py-8 text-center text-[12px] text-[color:var(--color-ink-3)]">
{{ __('Noch keine Kampagnen. Legen Sie die erste Kampagne an, um Bestands-User zu informieren.') }}
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
@if ($campaigns->hasPages())
<div class="px-5 py-4 border-t border-[color:var(--color-bg-rule)]">
{{ $campaigns->links() }}
</div>
@endif
</article>
</div>