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>
118 lines
6 KiB
PHP
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>
|