presseportale/resources/views/livewire/admin/newsletter/unsubscribes.blade.php
Kevin Adametz 58c0cc3f23 admin: Newsletter mit Abmelde-Liste, Follow-up-Filtern und gedrosseltem Versand
- Unterseite /admin/newsletter/abmeldungen listet Newsletter-Opt-outs mit
  Zeitpunkt, Suche und Link zur User-Detailseite
- Kampagnen-Filter: Empfänger einer früheren Kampagne ausschließen
  (Abgleich per User-ID und E-Mail-Snapshot) für Follow-up-Mailings
- Login-Filter kann Monats-Scheiben (von–bis) abgrenzen, um große
  Listen Monat für Monat abzuarbeiten
- Versand läuft in Wellen (Standard 50 Mails / 15 Min, per .env
  justierbar) gegen SMTP-Limits und Blocklisten; Hinweis im Senden-Modal
- web: Firmen-Website im Newsroom & Co. als absolute URL verlinkt
  (Company::websiteUrl), statt relativ auf /newsroom/www.example.de
- kunde: API-Doku-Button öffnet die /de/api-Seite; OpenAPI-Spezifikation
  als eigener, benannter Download (pressekonto-api-v1.yml)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 12:38:47 +00:00

124 lines
5.7 KiB
PHP

<?php
use App\Enums\EmailNotificationType;
use App\Models\EmailNotificationOptOut;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Volt\Component;
use Livewire\WithPagination;
/*
* Newsletter-Abmeldungen: alle User, die den Newsletter über den Abmelde-Link
* (oder die Mail-Einstellungen) abbestellt haben — mit Zeitpunkt der Abmeldung.
*/
new #[Layout('components.layouts.app'), Title('Newsletter-Abmeldungen')] class extends Component
{
use WithPagination;
public string $search = '';
public function updated(string $property): void
{
if ($property === 'search') {
$this->resetPage();
}
}
public function with(): array
{
return [
'optOuts' => EmailNotificationOptOut::query()
->where('type', EmailNotificationType::Newsletter->value)
->whereHas('user', fn ($query) => $query
->when(trim($this->search) !== '', fn ($inner) => $inner
->where(fn ($match) => $match
->where('name', 'like', '%'.trim($this->search).'%')
->orWhere('email', 'like', '%'.trim($this->search).'%'))))
->with('user:id,name,email,portal,is_active')
->orderByDesc('created_at')
->paginate(50),
];
}
}; ?>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<x-portal.page-header
:badge="__('Admin Backend')"
:eyebrow="__('Administration · Newsletter')"
:title="__('Newsletter-Abmeldungen')"
:description="__('User, die den Newsletter über den Abmelde-Link oder die Mail-Einstellungen abbestellt haben. Sie werden bei künftigen Kampagnen automatisch übersprungen.')"
>
<x-slot:actions>
<flux:button variant="filled" icon="arrow-left" href="{{ route('admin.newsletter.campaigns.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</x-slot:actions>
</x-portal.page-header>
{{-- ============== ABMELDUNGEN ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Abmeldungen') }}</span>
<div class="flex items-center gap-2">
<flux:input size="sm" icon="magnifying-glass" wire:model.live.debounce.400ms="search"
:placeholder="__('Name oder E-Mail suchen…')" clearable />
<span class="badge hub">{{ number_format($optOuts->total(), 0, ',', '.') }} {{ __('Einträge') }}</span>
</div>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('User') }}</flux:table.column>
<flux:table.column>{{ __('E-Mail') }}</flux:table.column>
<flux:table.column>{{ __('Portal') }}</flux:table.column>
<flux:table.column>{{ __('Status') }}</flux:table.column>
<flux:table.column>{{ __('Abgemeldet am') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@forelse ($optOuts as $optOut)
<flux:table.row :key="$optOut->id">
<flux:table.cell class="whitespace-nowrap">
<a href="{{ route('admin.users.show', $optOut->user->id) }}" wire:navigate
class="text-[12.5px] font-semibold text-[color:var(--color-ink)] hover:underline">
{{ $optOut->user->name }}
</a>
</flux:table.cell>
<flux:table.cell class="whitespace-nowrap">
<span class="text-[12px] text-[color:var(--color-ink-2)]">{{ $optOut->user->email }}</span>
</flux:table.cell>
<flux:table.cell class="whitespace-nowrap">
<span class="text-[12px] text-[color:var(--color-ink-3)]">{{ $optOut->user->portal?->label() ?? '—' }}</span>
</flux:table.cell>
<flux:table.cell class="whitespace-nowrap">
@if ($optOut->user->is_active)
<flux:badge size="sm" color="green">{{ __('aktiv') }}</flux:badge>
@else
<flux:badge size="sm" color="zinc">{{ __('inaktiv') }}</flux:badge>
@endif
</flux:table.cell>
<flux:table.cell class="whitespace-nowrap">
<span class="text-[12px] text-[color:var(--color-ink-2)] tabular-nums">
{{ $optOut->created_at?->format('d.m.Y H:i') ?? '—' }}
</span>
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="5">
<div class="py-8 text-center text-[12px] text-[color:var(--color-ink-3)]">
{{ trim($search) !== ''
? __('Keine Abmeldungen für diese Suche.')
: __('Bislang hat sich niemand vom Newsletter abgemeldet.') }}
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
@if ($optOuts->hasPages())
<div class="px-5 py-4 border-t border-[color:var(--color-bg-rule)]">
{{ $optOuts->links() }}
</div>
@endif
</article>
</div>