presseportale/resources/views/livewire/admin/contact-messages/index.blade.php
Kevin Adametz fd8e050cb7 Backend-WIP: Kontakt-Queue, Page-Cache, Validierungs-Feedback, i18n
Gebuendelt, da ueber gemeinsame lang-/Service-Dateien verschraenkt:
- Kontakt-Nachrichten: contact_messages-Tabelle + Model/Enums/Factory,
  Admin-Queue (livewire/admin/contact-messages), ContactController
  persistiert das Anliegen zusaetzlich zur Brand-Mail
- Page-Data-Cache (Services/Web/*PageData) + web-archive-Indizes auf
  press_releases
- Validierungs-Feedback + Company-Portal-Restriction (Feature-Tests)
- i18n-Auslagerung lang/de|en (auth, passwords, validation, static)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 15:38:11 +00:00

185 lines
8.4 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\Enums\ContactMessageStatus;
use App\Enums\ContactMessageTopic;
use App\Models\ContactMessage;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Volt\Component;
use Livewire\WithPagination;
new #[Layout('components.layouts.app'), Title('Kontakt')] class extends Component
{
use WithPagination;
#[Url(as: 'status', except: 'open')]
public string $statusFilter = 'open';
#[Url(as: 'topic', except: 'all')]
public string $topicFilter = 'all';
public function updatedStatusFilter(): void
{
$this->resetPage();
}
public function updatedTopicFilter(): void
{
$this->resetPage();
}
public function with(): array
{
$messages = ContactMessage::query()
->when($this->statusFilter === 'open', fn ($query) => $query->open())
->when(! in_array($this->statusFilter, ['open', 'all'], true), fn ($query) => $query->where('status', $this->statusFilter))
->when($this->topicFilter !== 'all', fn ($query) => $query->where('topic', $this->topicFilter))
->latest()
->paginate(50);
return [
'messages' => $messages,
'topicOptions' => ContactMessageTopic::cases(),
'openCount' => ContactMessage::query()->open()->count(),
'totalCount' => ContactMessage::query()->count(),
];
}
public function statusBadgeClass(ContactMessageStatus $status): string
{
return match ($status) {
ContactMessageStatus::Open => 'warn',
ContactMessageStatus::InProgress => 'hub',
ContactMessageStatus::Resolved => 'ok',
ContactMessageStatus::Rejected => 'err',
};
}
}; ?>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<x-portal.page-header
:badge="__('Admin Backend')"
:eyebrow="__('Administration · Kontakt')"
:title="__('Kontakt')"
:description="__('Anliegen aus dem Kontakt-Widget und dem Kontaktformular: Kontaktaufnahmen, Fehlermeldungen, PM-Meldungen und Feedback – manuelle Bearbeitung.')"
/>
{{-- ============== KPI-Reihe ============== --}}
<section class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<x-portal.stat-card variant="primary" :label="__('Offen')" :value="number_format($openCount)">
<x-slot:meta>{{ __('zu bearbeiten') }}</x-slot:meta>
<x-slot:trend>{{ __('offen + in Bearbeitung') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Gesamt')" :value="number_format($totalCount)">
<x-slot:meta>{{ __('alle Anliegen') }}</x-slot:meta>
<x-slot:trend>{{ __('inkl. erledigt') }}</x-slot:trend>
</x-portal.stat-card>
</section>
{{-- ============== FILTER ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Filter') }}</span>
</div>
<div class="p-5 flex flex-col gap-4 sm:flex-row">
<flux:select wire:model.live="statusFilter" class="w-full sm:w-56">
<option value="open">{{ __('Offen & in Bearbeitung') }}</option>
<option value="all">{{ __('Alle Status') }}</option>
<option value="resolved">{{ __('Erledigt') }}</option>
<option value="rejected">{{ __('Abgelehnt') }}</option>
</flux:select>
<flux:select wire:model.live="topicFilter" class="w-full sm:w-56">
<option value="all">{{ __('Alle Anliegen') }}</option>
@foreach ($topicOptions as $topicOption)
<option value="{{ $topicOption->value }}">{{ $topicOption->label() }}</option>
@endforeach
</flux:select>
</div>
</article>
{{-- ============== TABELLE ============== --}}
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Anliegen') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __(':count Einträge', ['count' => $messages->total()]) }}
</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Anliegen') }}</flux:table.column>
<flux:table.column>{{ __('Status') }}</flux:table.column>
<flux:table.column>{{ __('Absender') }}</flux:table.column>
<flux:table.column>{{ __('Portal') }}</flux:table.column>
<flux:table.column>{{ __('Eingegangen') }}</flux:table.column>
<flux:table.column>{{ __('Aktion') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@forelse ($messages as $message)
<flux:table.row :key="$message->id">
<flux:table.cell>
<span class="text-[13px] font-semibold text-[color:var(--color-ink)]">
{{ $message->topic->label() }}
</span>
<span class="block text-[11px] font-mono text-[color:var(--color-ink-4)]">
{{ $message->reference() }}
</span>
</flux:table.cell>
<flux:table.cell>
<span class="badge dot {{ $this->statusBadgeClass($message->status) }}">
{{ $message->status->label() }}
</span>
</flux:table.cell>
<flux:table.cell>
<span class="text-[12.5px] text-[color:var(--color-ink)]">
{{ $message->name ?: __('anonym') }}
</span>
<span class="block text-[11.5px] text-[color:var(--color-ink-3)]">
{{ $message->email }}
</span>
</flux:table.cell>
<flux:table.cell>
<span class="text-[12.5px] text-[color:var(--color-ink-3)]">
{{ $message->portal ?: '—' }}
</span>
</flux:table.cell>
<flux:table.cell>
<span class="text-[12px] text-[color:var(--color-ink-3)]">
{{ $message->created_at?->format('d.m.Y H:i') ?? '-' }}
</span>
</flux:table.cell>
<flux:table.cell>
<flux:button size="sm" variant="filled" icon="eye"
href="{{ route('admin.contact-messages.show', $message->id) }}" wire:navigate>
{{ __('Öffnen') }}
</flux:button>
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="6">
<div class="flex flex-col items-center justify-center px-4 py-10 text-center">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-3
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.chat-bubble-left-right class="size-6" />
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Keine Anliegen in dieser Ansicht') }}
</div>
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0 max-w-[420px]">
{{ __('Anliegen aus dem Kontakt-Widget (Kontaktaufnahme, Fehlermeldung, PM-Meldung, Feedback) laufen hier auf.') }}
</p>
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
<x-portal.table-pagination :paginator="$messages" />
</article>
</div>