- 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>
124 lines
5.7 KiB
PHP
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>
|