Phase 8 (Rest) + Umbauten vom 10./11.06.: - Ein Titelbild pro PM (Cover 1280x580), SVG-Platzhalter-Set + Picker, PressReleaseCoverImage-Resolver - Lizenz-/Rechteformular nach "Lizenztyp Bildupload" (7 Lizenztypen, Personen-/Sachrechte-Status, bedingte Pflichtfelder, Risikohinweise) - Veroeffentlichungs-Box vereinfacht (Embargo aus der Form-UI entfernt), geplante Termine in Europe/Berlin (Speicherung UTC, DISPLAY_TIMEZONE) - Quota-Stub (users.press_release_quota) + monatlicher Reset-Command - Einreichungs-Modal einheitlich in Show/Create/Edit; Ghost-Buttons auf filled; PM-Editor-Layout responsive entkoppelt (.pr-editor-layout) KI-Pruef-Pipeline (Phasen 1-5 des Entwicklungsplans): - API-Haertung: status nicht mehr per API setzbar, eigene Submit-Route durch denselben Funnel (Blacklist, Quota, Status-Log) - Klassifikation Rot/Gelb/Gruen asynchron (Queue classification, OpenAI-Treiber + deterministischer Fallback), ki_audits-Audit-Log - Routing: Rot -> rejected + Mail, Gelb -> Review-Queue, Gruen -> Auto-Publish; Scheduler publiziert nur gruene faellige PMs - Content-Score 0-100 -> Stufe (Standard/Geprueft/Hochwertig) inkl. Editor-Panel und Badges; Re-Klassifikation/-Score bei Aenderung - Admin: KI-Badge + Filter, On-Demand-Pruefung mit Anbieter-Override Suite: 442 passed, 4 skipped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
171 lines
7.5 KiB
PHP
171 lines
7.5 KiB
PHP
<?php
|
|
|
|
use App\Models\NewsletterSubscription;
|
|
use App\Services\Admin\AdminPerformanceCache;
|
|
use App\Services\Newsletter\NewsletterSyncService;
|
|
use Livewire\Attributes\Layout;
|
|
use Livewire\Attributes\Title;
|
|
use Livewire\Volt\Component;
|
|
|
|
new #[Layout('components.layouts.app'), Title('Newsletter Sync')] class extends Component
|
|
{
|
|
public ?string $syncMessage = null;
|
|
|
|
public ?string $dryRunMessage = null;
|
|
|
|
public function triggerDryRun(): void
|
|
{
|
|
$subscription = NewsletterSubscription::query()->latest('id')->first();
|
|
|
|
if ($subscription === null) {
|
|
$this->dryRunMessage = 'Dry-Run: Kein Datensatz fuer Vorschau vorhanden.';
|
|
|
|
return;
|
|
}
|
|
|
|
$action = ($subscription->is_confirmed && $subscription->unsubscribed_at === null)
|
|
? 'subscribe'
|
|
: 'unsubscribe';
|
|
|
|
$this->dryRunMessage = "Dry-Run: Es wuerde {$action} fuer Subscription #{$subscription->id} ({$subscription->email}) ausgefuehrt.";
|
|
}
|
|
|
|
public function triggerTestSync(): void
|
|
{
|
|
$subscription = NewsletterSubscription::query()->latest('id')->first();
|
|
|
|
if ($subscription === null) {
|
|
$this->syncMessage = 'Kein Datensatz fuer Test-Sync vorhanden.';
|
|
|
|
return;
|
|
}
|
|
|
|
app(NewsletterSyncService::class)->syncSubscription($subscription);
|
|
|
|
$action = ($subscription->is_confirmed && $subscription->unsubscribed_at === null)
|
|
? 'subscribe'
|
|
: 'unsubscribe';
|
|
|
|
$this->syncMessage = "Test-Sync ausgefuehrt ({$action}) fuer Subscription #{$subscription->id}.";
|
|
}
|
|
|
|
public function with(): array
|
|
{
|
|
return [
|
|
'stats' => $this->stats(),
|
|
'syncConfig' => [
|
|
'enabled' => (bool) config('newsletter.sync.enabled'),
|
|
'provider' => (string) config('newsletter.sync.provider'),
|
|
'endpoint' => (string) (config('newsletter.sync.endpoint') ?? '-'),
|
|
'timeout' => (int) config('newsletter.sync.timeout', 10),
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{total: int, confirmed: int, pending: int, unsubscribed: int}
|
|
*/
|
|
private function stats(): array
|
|
{
|
|
return app(AdminPerformanceCache::class)->remember(AdminPerformanceCache::NewsletterStats, AdminPerformanceCache::StatsTtl, function (): array {
|
|
$stats = NewsletterSubscription::query()
|
|
->toBase()
|
|
->selectRaw('COUNT(*) as total')
|
|
->selectRaw('SUM(CASE WHEN is_confirmed = ? THEN 1 ELSE 0 END) as confirmed', [true])
|
|
->selectRaw('SUM(CASE WHEN is_confirmed = ? THEN 1 ELSE 0 END) as pending', [false])
|
|
->selectRaw('SUM(CASE WHEN unsubscribed_at IS NOT NULL THEN 1 ELSE 0 END) as unsubscribed')
|
|
->first();
|
|
|
|
return [
|
|
'total' => (int) ($stats->total ?? 0),
|
|
'confirmed' => (int) ($stats->confirmed ?? 0),
|
|
'pending' => (int) ($stats->pending ?? 0),
|
|
'unsubscribed' => (int) ($stats->unsubscribed ?? 0),
|
|
];
|
|
});
|
|
}
|
|
}; ?>
|
|
|
|
<div class="space-y-8">
|
|
{{-- ============== PAGE HEADER ============== --}}
|
|
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
|
|
<div class="min-w-0">
|
|
<div class="flex items-center gap-3 mb-3 flex-wrap">
|
|
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
|
|
<span class="eyebrow muted">{{ __('Administration · Newsletter') }}</span>
|
|
@if ($syncConfig['enabled'])
|
|
<span class="badge ok dot">{{ __('Sync aktiv') }}</span>
|
|
@else
|
|
<span class="badge dot">{{ __('Sync deaktiviert') }}</span>
|
|
@endif
|
|
</div>
|
|
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
|
|
{{ __('Newsletter Synchronisierung') }}
|
|
</h1>
|
|
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
|
|
{{ __('Vorbereitung fuer die kuenftige externe API-Anbindung. Aktuell ist nur das technische Grundgeruest aktiv.') }}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2 flex-shrink-0">
|
|
<flux:button size="sm" variant="filled" icon="eye" wire:click="triggerDryRun">
|
|
{{ __('Dry Run') }}
|
|
</flux:button>
|
|
<flux:button size="sm" variant="primary" icon="play" wire:click="triggerTestSync">
|
|
{{ __('Test-Sync ausfuehren') }}
|
|
</flux:button>
|
|
</div>
|
|
</header>
|
|
|
|
@if ($dryRunMessage)
|
|
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-start gap-3
|
|
bg-[color:var(--color-hub-soft)] border-[color:var(--color-hub-soft-2)] text-[color:var(--color-ink-2)]">
|
|
<flux:icon.eye class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-hub)]" />
|
|
<div class="flex-1">{{ $dryRunMessage }}</div>
|
|
</div>
|
|
@endif
|
|
|
|
@if ($syncMessage)
|
|
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-center gap-2
|
|
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
|
|
<flux:icon.check-circle class="size-[16px] flex-shrink-0" />
|
|
{{ $syncMessage }}
|
|
</div>
|
|
@endif
|
|
|
|
{{-- ============== KPI-Reihe ============== --}}
|
|
<section class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
<x-portal.stat-card variant="primary" :label="__('Gesamt')" :value="number_format($stats['total'])">
|
|
<x-slot:meta>{{ __('Subscriptions') }}</x-slot:meta>
|
|
</x-portal.stat-card>
|
|
<x-portal.stat-card variant="ok" :label="__('Bestaetigt')" :value="number_format($stats['confirmed'])">
|
|
<x-slot:meta>{{ __('Double Opt-in') }}</x-slot:meta>
|
|
</x-portal.stat-card>
|
|
<x-portal.stat-card variant="warn" :label="__('Unbestaetigt')" :value="number_format($stats['pending'])">
|
|
<x-slot:meta>{{ __('warten auf Opt-in') }}</x-slot:meta>
|
|
</x-portal.stat-card>
|
|
<x-portal.stat-card variant="muted" :label="__('Abgemeldet')" :value="number_format($stats['unsubscribed'])">
|
|
<x-slot:meta>{{ __('Unsubscribed') }}</x-slot:meta>
|
|
</x-portal.stat-card>
|
|
</section>
|
|
|
|
<article class="panel">
|
|
<div class="panel-head">
|
|
<span class="section-eyebrow">{{ __('Konfiguration') }}</span>
|
|
</div>
|
|
<dl class="p-5 grid grid-cols-1 gap-4 sm:grid-cols-2 text-[12.5px]">
|
|
<div>
|
|
<dt class="text-[10.5px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-1">{{ __('Provider') }}</dt>
|
|
<dd class="text-[color:var(--color-ink)] font-mono">{{ $syncConfig['provider'] }}</dd>
|
|
</div>
|
|
<div>
|
|
<dt class="text-[10.5px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-1">{{ __('Timeout') }}</dt>
|
|
<dd class="text-[color:var(--color-ink)] tabular-nums">{{ $syncConfig['timeout'] }}s</dd>
|
|
</div>
|
|
<div class="sm:col-span-2">
|
|
<dt class="text-[10.5px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-1">{{ __('Endpoint') }}</dt>
|
|
<dd class="text-[color:var(--color-ink)] font-mono break-all">{{ $syncConfig['endpoint'] }}</dd>
|
|
</div>
|
|
</dl>
|
|
</article>
|
|
</div>
|