presseportale/resources/views/livewire/admin/newsletter/sync.blade.php
Kevin Adametz 2cde7284a1 livewire: Volt entfernt — Komponenten als Livewire-4-Single-File-Components, Route::livewire(), Livewire::test(); livewire/volt deinstalliert
- 83 Komponenten: Livewire\Volt\Component → Livewire\Component
- 73 Routen (admin, auth, customer): Volt::route() → Route::livewire()
- 88 Testdateien: Volt::test/actingAs → Livewire::test/actingAs
- VoltServiceProvider gelöscht; composer remove livewire/volt
- config/livewire.php auf v4-Schlüssel (component_layout, component_placeholder,
  component_locations, make_command: SFC ohne ⚡). Der alte Schlüssel layout war
  wirkungslos; alle gerouteten Komponenten setzen ihr Layout selbst.
- CLAUDE.md, AGENTS.md, boost.json, .cursorrules, Skill volt-development
- Roadmap: erledigt; Larastan erfasst SFCs weiterhin nicht (Blade-Dateien)

Suite 2.325 grün, einer übersprungen; Larastan und Pint ohne Meldung;
route:cache und view:cache laufen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 13:59:36 +00:00

173 lines
7.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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\Component;
new #[Layout('components.layouts.app'), Title('Newsletter Sync')] class extends Component
{
public ?string $syncMessage = null;
public ?string $dryRunMessage = null;
public function triggerDryRun(): void
{
$this->authorize('billing:manage');
$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
{
$this->authorize('billing:manage');
$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}.";
if ((string) config('newsletter.sync.provider') === 'placeholder') {
$this->syncMessage .= ' Hinweis: Platzhalter-Provider – es wurde kein externes System aufgerufen.';
}
}
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 ============== --}}
<x-portal.page-header
:badge="__('Admin Backend')"
:eyebrow="__('Administration · Newsletter')"
:title="__('Newsletter Synchronisierung')"
:description="__('Vorbereitung fuer die kuenftige externe API-Anbindung. Aktuell ist nur das technische Grundgeruest aktiv.')"
>
<x-slot:meta>
@if ($syncConfig['enabled'])
<span class="badge ok dot">{{ __('Sync aktiv') }}</span>
@else
<span class="badge dot">{{ __('Sync deaktiviert') }}</span>
@endif
</x-slot:meta>
<x-slot:actions>
<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>
</x-slot:actions>
</x-portal.page-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>