751 lines
33 KiB
PHP
751 lines
33 KiB
PHP
<?php
|
||
|
||
use App\Enums\Portal;
|
||
use App\Models\Company;
|
||
use App\Models\Contact;
|
||
use App\Models\User;
|
||
use App\Models\UserFilterPreset;
|
||
use App\Services\Admin\AdminPerformanceCache;
|
||
use App\Services\CurrentPortalContext;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Validation\Rule;
|
||
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('Kontakte')] class extends Component
|
||
{
|
||
use WithPagination;
|
||
|
||
public string $search = '';
|
||
|
||
#[Url(as: 'company', except: 'all')]
|
||
public string $companyFilter = 'all';
|
||
|
||
public string $companySearch = '';
|
||
|
||
#[Url(as: 'user', except: 'all')]
|
||
public string $userFilter = 'all';
|
||
|
||
public string $userSearch = '';
|
||
|
||
#[Url(as: 'data', except: 'all')]
|
||
public string $qualityFilter = 'all';
|
||
|
||
public string $portalFilter = 'all';
|
||
|
||
public ?int $selectedPresetId = null;
|
||
|
||
public string $presetName = '';
|
||
|
||
public string $notification = '';
|
||
|
||
public string $notificationType = 'success';
|
||
|
||
public string $sortBy = 'created_at';
|
||
|
||
public string $sortDir = 'desc';
|
||
|
||
public function sort(string $column): void
|
||
{
|
||
if ($this->sortBy === $column) {
|
||
$this->sortDir = $this->sortDir === 'asc' ? 'desc' : 'asc';
|
||
} else {
|
||
$this->sortBy = $column;
|
||
$this->sortDir = 'asc';
|
||
}
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function updatedCompanyFilter(): void
|
||
{
|
||
// Flux clearable setzt den Wert auf null – normalisieren auf 'all'
|
||
if (blank($this->companyFilter)) {
|
||
$this->companyFilter = 'all';
|
||
}
|
||
$this->companySearch = '';
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function updatedCompanySearch(): void
|
||
{
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function updatedUserFilter(): void
|
||
{
|
||
if (blank($this->userFilter)) {
|
||
$this->userFilter = 'all';
|
||
}
|
||
|
||
$this->userSearch = '';
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function updatedUserSearch(): void
|
||
{
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function clearCompanySearch(): void
|
||
{
|
||
$this->companyFilter = 'all';
|
||
$this->companySearch = '';
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function clearUserSearch(): void
|
||
{
|
||
$this->userFilter = 'all';
|
||
$this->userSearch = '';
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function updatedQualityFilter(): void
|
||
{
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function mount(): void
|
||
{
|
||
$currentUser = auth()->user();
|
||
if (! $currentUser) {
|
||
return;
|
||
}
|
||
|
||
$defaultPreset = UserFilterPreset::query()->where('user_id', $currentUser->id)->where('page', 'admin.contacts.index')->where('is_default', true)->first();
|
||
|
||
if (! $defaultPreset) {
|
||
return;
|
||
}
|
||
|
||
$this->selectedPresetId = (int) $defaultPreset->id;
|
||
$filters = $defaultPreset->filters ?? [];
|
||
$this->search = (string) ($filters['search'] ?? '');
|
||
$this->companyFilter = (string) ($filters['company_filter'] ?? 'all');
|
||
$this->userFilter = (string) ($filters['user_filter'] ?? 'all');
|
||
$this->qualityFilter = (string) ($filters['quality_filter'] ?? 'all');
|
||
$this->portalFilter = (string) ($filters['portal_filter'] ?? 'all');
|
||
}
|
||
|
||
public function with(): array
|
||
{
|
||
$currentUser = auth()->user();
|
||
|
||
$contacts = Contact::query()
|
||
->with('company:id,name')
|
||
->withCount('pressReleases')
|
||
->when($this->search, function ($query): void {
|
||
$term = trim($this->search);
|
||
|
||
if ($this->supportsFullTextSearch($term)) {
|
||
$query->where(function ($query) use ($term): void {
|
||
$query->whereFullText(['first_name', 'last_name', 'email', 'responsibility'], $term)
|
||
->orWhereHas('company', fn ($companyQuery) => $companyQuery->whereFullText(['name', 'email', 'slug'], $term));
|
||
});
|
||
|
||
return;
|
||
}
|
||
|
||
$query->where(function ($searchQuery): void {
|
||
$searchQuery
|
||
->where('first_name', 'like', '%'.$this->search.'%')
|
||
->orWhere('last_name', 'like', '%'.$this->search.'%')
|
||
->orWhere('email', 'like', '%'.$this->search.'%')
|
||
->orWhereHas('company', function ($companyQuery): void {
|
||
$companyQuery->where('name', 'like', '%'.$this->search.'%');
|
||
});
|
||
});
|
||
})
|
||
->when($this->companyFilter !== 'all', function ($query): void {
|
||
$query->where('company_id', (int) $this->companyFilter);
|
||
})
|
||
->when($this->userFilter !== 'all', function ($query): void {
|
||
$query->whereHas('users', fn ($userQuery) => $userQuery->where('users.id', (int) $this->userFilter));
|
||
})
|
||
->when($this->qualityFilter !== 'all', function ($query): void {
|
||
match ($this->qualityFilter) {
|
||
'with_press_releases' => $query->whereHas('pressReleases'),
|
||
'without_press_releases' => $query->whereDoesntHave('pressReleases'),
|
||
default => null,
|
||
};
|
||
})
|
||
->when($this->portalFilter !== 'all', function ($query): void {
|
||
$query->where('portal', $this->portalFilter);
|
||
})
|
||
->orderBy(in_array($this->sortBy, ['last_name', 'email', 'company_id', 'press_releases_count', 'created_at'], true) ? $this->sortBy : 'created_at', $this->sortDir)
|
||
->paginate(50);
|
||
|
||
// Firmen-Filter: nur Live-Suche, nie alle laden
|
||
$term = trim($this->companySearch);
|
||
$selectedCompanyId = $this->companyFilter !== 'all' ? (int) $this->companyFilter : null;
|
||
$filterCompanies = Company::withoutGlobalScopes()
|
||
->when(filled($term), function ($q) use ($term): void {
|
||
if ($this->supportsFullTextSearch($term)) {
|
||
$q->whereFullText(['name', 'email', 'slug'], $term);
|
||
|
||
return;
|
||
}
|
||
|
||
$q->where('name', 'like', '%'.$term.'%');
|
||
})
|
||
->when(blank($term) && $selectedCompanyId, fn ($q) => $q->whereIn('id', [$selectedCompanyId]))
|
||
->when(blank($term) && ! $selectedCompanyId, fn ($q) => $q->whereRaw('0 = 1'))
|
||
->orderBy('name')
|
||
->limit(50)
|
||
->get(['id', 'name']);
|
||
|
||
$userTerm = trim($this->userSearch);
|
||
$selectedUserId = $this->userFilter !== 'all' ? (int) $this->userFilter : null;
|
||
$filterUsers = User::query()
|
||
->select(['id', 'name', 'email'])
|
||
->where(function ($query) use ($userTerm, $selectedUserId): void {
|
||
if ($selectedUserId) {
|
||
$query->where('id', $selectedUserId);
|
||
}
|
||
|
||
if ($userTerm !== '') {
|
||
$query->orWhere(function ($searchQuery) use ($userTerm): void {
|
||
$searchQuery
|
||
->where('name', 'like', '%'.$userTerm.'%')
|
||
->orWhere('email', 'like', '%'.$userTerm.'%');
|
||
});
|
||
}
|
||
})
|
||
->when($userTerm === '' && ! $selectedUserId, fn ($query) => $query->whereRaw('0 = 1'))
|
||
->orderBy('name')
|
||
->limit(20)
|
||
->get();
|
||
|
||
return [
|
||
'contacts' => $contacts,
|
||
'filterCompanies' => $filterCompanies,
|
||
'filterUsers' => $filterUsers,
|
||
'portalOptions' => Portal::cases(),
|
||
'presets' => $currentUser
|
||
? UserFilterPreset::query()
|
||
->where('user_id', $currentUser->id)
|
||
->where('page', 'admin.contacts.index')
|
||
->orderByDesc('is_default')
|
||
->orderByDesc('last_used_at')
|
||
->orderBy('name')
|
||
->get(['id', 'name', 'is_default', 'last_used_at', 'filters'])
|
||
: collect(),
|
||
'stats' => $this->stats(),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* @return array{total: int, companies_with_contacts: int, avg_per_company: float}
|
||
*/
|
||
private function stats(): array
|
||
{
|
||
$portal = CurrentPortalContext::get()?->value ?? 'all';
|
||
$cache = app(AdminPerformanceCache::class);
|
||
|
||
return $cache->remember($cache->contactsStatsKey($portal), AdminPerformanceCache::StatsTtl, function (): array {
|
||
$total = Contact::count();
|
||
$companiesWithContacts = Contact::query()
|
||
->distinct()
|
||
->count('company_id');
|
||
|
||
return [
|
||
'total' => $total,
|
||
'companies_with_contacts' => $companiesWithContacts,
|
||
'avg_per_company' => $companiesWithContacts > 0 ? round($total / $companiesWithContacts, 1) : 0.0,
|
||
];
|
||
});
|
||
}
|
||
|
||
private function supportsFullTextSearch(string $term): bool
|
||
{
|
||
return mb_strlen($term) >= 3
|
||
&& in_array(DB::connection()->getDriverName(), ['mysql', 'pgsql'], true);
|
||
}
|
||
|
||
public function updatedSearch(): void
|
||
{
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function updatedPortalFilter(): void
|
||
{
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function savePreset(): void
|
||
{
|
||
$currentUser = auth()->user();
|
||
if (! $currentUser) {
|
||
return;
|
||
}
|
||
|
||
$validated = $this->validate([
|
||
'presetName' => ['required', 'string', 'min:2', 'max:120', Rule::unique('user_filter_presets', 'name')->where(fn ($query) => $query->where('user_id', $currentUser->id)->where('page', 'admin.contacts.index'))],
|
||
]);
|
||
|
||
UserFilterPreset::query()->create([
|
||
'user_id' => $currentUser->id,
|
||
'page' => 'admin.contacts.index',
|
||
'name' => $validated['presetName'],
|
||
'last_used_at' => now(),
|
||
'filters' => [
|
||
'search' => $this->search,
|
||
'company_filter' => $this->companyFilter,
|
||
'user_filter' => $this->userFilter,
|
||
'quality_filter' => $this->qualityFilter,
|
||
'portal_filter' => $this->portalFilter,
|
||
],
|
||
]);
|
||
|
||
$this->presetName = '';
|
||
$this->notification = __('Filter-Preset wurde gespeichert.');
|
||
$this->notificationType = 'success';
|
||
}
|
||
|
||
public function applyPreset(): void
|
||
{
|
||
$currentUser = auth()->user();
|
||
if (! $currentUser || ! $this->selectedPresetId) {
|
||
return;
|
||
}
|
||
|
||
$preset = UserFilterPreset::query()->where('user_id', $currentUser->id)->where('page', 'admin.contacts.index')->find($this->selectedPresetId);
|
||
|
||
if (! $preset) {
|
||
return;
|
||
}
|
||
|
||
$filters = $preset->filters ?? [];
|
||
$this->search = (string) ($filters['search'] ?? '');
|
||
$this->companyFilter = (string) ($filters['company_filter'] ?? 'all');
|
||
$this->userFilter = (string) ($filters['user_filter'] ?? 'all');
|
||
$this->qualityFilter = (string) ($filters['quality_filter'] ?? 'all');
|
||
$this->portalFilter = (string) ($filters['portal_filter'] ?? 'all');
|
||
$preset->update(['last_used_at' => now()]);
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function deletePreset(): void
|
||
{
|
||
$currentUser = auth()->user();
|
||
if (! $currentUser || ! $this->selectedPresetId) {
|
||
return;
|
||
}
|
||
|
||
UserFilterPreset::query()->where('user_id', $currentUser->id)->where('page', 'admin.contacts.index')->whereKey($this->selectedPresetId)->delete();
|
||
|
||
$this->selectedPresetId = null;
|
||
$this->notification = __('Filter-Preset wurde gelöscht.');
|
||
$this->notificationType = 'success';
|
||
}
|
||
|
||
public function setDefaultPreset(): void
|
||
{
|
||
$currentUser = auth()->user();
|
||
if (! $currentUser || ! $this->selectedPresetId) {
|
||
return;
|
||
}
|
||
|
||
UserFilterPreset::query()
|
||
->where('user_id', $currentUser->id)
|
||
->where('page', 'admin.contacts.index')
|
||
->update(['is_default' => false]);
|
||
|
||
UserFilterPreset::query()
|
||
->where('user_id', $currentUser->id)
|
||
->where('page', 'admin.contacts.index')
|
||
->whereKey($this->selectedPresetId)
|
||
->update(['is_default' => true]);
|
||
|
||
$this->notification = __('Standard-Preset wurde gesetzt.');
|
||
$this->notificationType = 'success';
|
||
}
|
||
|
||
public function deleteContactFromIndex(int $contactId): void
|
||
{
|
||
$contact = Contact::query()->find($contactId);
|
||
if (! $contact) {
|
||
$this->notification = __('Der angeforderte Kontakt wurde nicht gefunden.');
|
||
$this->notificationType = 'error';
|
||
|
||
return;
|
||
}
|
||
|
||
$contact->delete();
|
||
$this->notification = __('Kontakt wurde gelöscht.');
|
||
$this->notificationType = 'success';
|
||
$this->resetPage();
|
||
}
|
||
|
||
private function portalBadgeColor(?Portal $portal): string
|
||
{
|
||
return match ($portal) {
|
||
Portal::Presseecho => 'blue',
|
||
Portal::Businessportal24 => 'purple',
|
||
Portal::Both => 'zinc',
|
||
default => 'zinc',
|
||
};
|
||
}
|
||
}; ?>
|
||
|
||
<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-nowrap whitespace-nowrap">
|
||
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
|
||
<span class="eyebrow muted">{{ __('Administration · Pressekontakte') }}</span>
|
||
</div>
|
||
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
|
||
{{ __('Kontakte') }}
|
||
</h1>
|
||
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
|
||
{{ __('Pressekontakte aller Firmen über alle Portale hinweg.') }}
|
||
</p>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-2 flex-shrink-0">
|
||
@if (\Illuminate\Support\Facades\Route::has('admin.contacts.create'))
|
||
<flux:button variant="primary" icon="plus" href="{{ route('admin.contacts.create') }}" wire:navigate>
|
||
{{ __('Neuer Kontakt') }}
|
||
</flux:button>
|
||
@else
|
||
<flux:button variant="primary" icon="plus" disabled>
|
||
{{ __('Neuer Kontakt') }}
|
||
</flux:button>
|
||
@endif
|
||
</div>
|
||
</header>
|
||
|
||
@if ($notification)
|
||
<div x-data="{ show: true }" x-init="setTimeout(() => show = false, 3000)" x-show="show" x-transition
|
||
@class([
|
||
'px-4 py-3 rounded-[5px] border text-[12.5px] flex items-center gap-2',
|
||
'bg-[color:var(--color-err-soft)] border-[color:var(--color-err)]/30 text-[color:var(--color-ink-2)]' => $notificationType === 'error',
|
||
'bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]' => $notificationType !== 'error',
|
||
])>
|
||
@if ($notificationType === 'error')
|
||
<flux:icon.exclamation-triangle class="size-[16px] flex-shrink-0" />
|
||
@else
|
||
<flux:icon.check-circle class="size-[16px] flex-shrink-0" />
|
||
@endif
|
||
{{ $notification }}
|
||
</div>
|
||
@endif
|
||
|
||
{{-- ============== KPI-Reihe ============== --}}
|
||
<section class="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||
<x-portal.stat-card variant="primary" :label="__('Gesamt')" :value="number_format($stats['total'])">
|
||
<x-slot:meta>{{ __('Pressekontakte') }}</x-slot:meta>
|
||
<x-slot:trend>{{ __('alle Portale') }}</x-slot:trend>
|
||
</x-portal.stat-card>
|
||
<x-portal.stat-card variant="ok" :label="__('Firmen mit Kontakten')" :value="number_format($stats['companies_with_contacts'])">
|
||
<x-slot:meta>{{ __('aktiv versorgt') }}</x-slot:meta>
|
||
<x-slot:trend>{{ __('mind. ein Kontakt') }}</x-slot:trend>
|
||
</x-portal.stat-card>
|
||
<x-portal.stat-card variant="muted" :label="__('Ø pro Firma')" :value="number_format($stats['avg_per_company'], 1)">
|
||
<x-slot:meta>{{ __('Pflegegrad') }}</x-slot:meta>
|
||
<x-slot:trend>{{ __('Kontakte / Firma') }}</x-slot:trend>
|
||
</x-portal.stat-card>
|
||
</section>
|
||
|
||
{{-- ============== FILTER-PANEL ============== --}}
|
||
<article class="panel">
|
||
<div class="panel-head">
|
||
<span class="section-eyebrow">{{ __('Filter & Suche') }}</span>
|
||
</div>
|
||
<div class="p-5 flex flex-col gap-4">
|
||
<div class="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
|
||
<flux:input wire:model.live.debounce.300ms="search"
|
||
placeholder="{{ __('Name, Email oder Firma suchen...') }}" icon="magnifying-glass" class="flex-1" />
|
||
|
||
<div class="flex w-full gap-2 xl:w-64">
|
||
<flux:select wire:model.live="companyFilter" variant="combobox" :filter="false" clearable
|
||
placeholder="{{ __('Alle Firmen') }}" class="min-w-0 flex-1">
|
||
<x-slot name="input">
|
||
<flux:select.input wire:model.live.debounce.300ms="companySearch"
|
||
placeholder="{{ __('Firma suchen…') }}" />
|
||
</x-slot>
|
||
|
||
@foreach ($filterCompanies as $company)
|
||
<flux:select.option :value="$company->id" wire:key="fc-{{ $company->id }}">
|
||
{{ $company->name }}
|
||
</flux:select.option>
|
||
@endforeach
|
||
|
||
<x-slot name="empty">
|
||
<flux:select.option.empty>
|
||
@if (blank(trim($companySearch)))
|
||
{{ __('Name eingeben…') }}
|
||
@else
|
||
{{ __('Keine Firma gefunden.') }}
|
||
@endif
|
||
</flux:select.option.empty>
|
||
</x-slot>
|
||
</flux:select>
|
||
|
||
<flux:button
|
||
type="button"
|
||
size="sm"
|
||
variant="ghost"
|
||
icon="x-mark"
|
||
wire:click="clearCompanySearch"
|
||
title="{{ __('Firmensuche zurücksetzen') }}"
|
||
/>
|
||
</div>
|
||
|
||
<div class="flex w-full gap-2 xl:w-64">
|
||
<flux:select wire:model.live="userFilter" variant="combobox" :filter="false" clearable
|
||
placeholder="{{ __('Alle User') }}" class="min-w-0 flex-1">
|
||
<x-slot name="input">
|
||
<flux:select.input wire:model.live.debounce.300ms="userSearch"
|
||
placeholder="{{ __('User suchen…') }}" />
|
||
</x-slot>
|
||
|
||
@foreach ($filterUsers as $user)
|
||
<flux:select.option :value="$user->id" wire:key="contact-user-{{ $user->id }}">
|
||
{{ $user->name }}
|
||
<span class="ml-1 text-zinc-400">· {{ $user->email }}</span>
|
||
</flux:select.option>
|
||
@endforeach
|
||
|
||
<x-slot name="empty">
|
||
<flux:select.option.empty>
|
||
@if (blank(trim($userSearch)))
|
||
{{ __('Usernamen oder E-Mail eingeben…') }}
|
||
@else
|
||
{{ __('Kein User gefunden.') }}
|
||
@endif
|
||
</flux:select.option.empty>
|
||
</x-slot>
|
||
</flux:select>
|
||
|
||
<flux:button
|
||
type="button"
|
||
size="sm"
|
||
variant="ghost"
|
||
icon="x-mark"
|
||
wire:click="clearUserSearch"
|
||
title="{{ __('Usersuche zurücksetzen') }}"
|
||
/>
|
||
</div>
|
||
|
||
<flux:select wire:model.live="qualityFilter" class="w-full xl:w-56">
|
||
<option value="all">{{ __('Alle Datenstände') }}</option>
|
||
<option value="with_press_releases">{{ __('Mit Pressemitteilungen') }}</option>
|
||
<option value="without_press_releases">{{ __('Ohne Pressemitteilungen') }}</option>
|
||
</flux:select>
|
||
|
||
<flux:select wire:model.live="portalFilter" class="w-full xl:w-48">
|
||
<option value="all">{{ __('Alle Portale') }}</option>
|
||
@foreach ($portalOptions as $portalOption)
|
||
<option value="{{ $portalOption->value }}">{{ $portalOption->label() }}</option>
|
||
@endforeach
|
||
</flux:select>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
|
||
{{-- ============== PRESET-PANEL ============== --}}
|
||
<article class="panel">
|
||
<div class="panel-head">
|
||
<span class="section-eyebrow">{{ __('Filter-Presets') }}</span>
|
||
</div>
|
||
<div class="p-5 space-y-3">
|
||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||
<div class="flex flex-1 gap-3">
|
||
<flux:input wire:model="presetName" placeholder="{{ __('Neues Preset speichern...') }}"
|
||
class="flex-1" />
|
||
<flux:button wire:click="savePreset" variant="ghost" icon="bookmark">
|
||
{{ __('Preset speichern') }}
|
||
</flux:button>
|
||
</div>
|
||
|
||
<div class="flex gap-2 flex-wrap">
|
||
<flux:select wire:model="selectedPresetId" class="w-64">
|
||
<option value="">{{ __('Preset auswählen') }}</option>
|
||
@foreach ($presets as $preset)
|
||
<option value="{{ $preset->id }}">
|
||
{{ $preset->name }}{{ $preset->is_default ? ' (Standard)' : '' }}
|
||
</option>
|
||
@endforeach
|
||
</flux:select>
|
||
<flux:button wire:click="applyPreset" variant="ghost">{{ __('Anwenden') }}</flux:button>
|
||
<flux:button wire:click="setDefaultPreset" variant="ghost">{{ __('Als Standard') }}</flux:button>
|
||
<flux:button wire:click="deletePreset" variant="danger">{{ __('Löschen') }}</flux:button>
|
||
</div>
|
||
</div>
|
||
<flux:error name="presetName" />
|
||
</div>
|
||
</article>
|
||
|
||
{{-- ============== TABELLE ============== --}}
|
||
<article class="panel overflow-hidden">
|
||
<div class="panel-head">
|
||
<span class="section-eyebrow">{{ __('Alle Kontakte') }}</span>
|
||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||
{{ __(':count Einträge', ['count' => $contacts->count()]) }}
|
||
</span>
|
||
</div>
|
||
<flux:table>
|
||
<flux:table.columns>
|
||
<flux:table.column>{{ __('Aktionen') }}</flux:table.column>
|
||
<flux:table.column sortable :sorted="$sortBy === 'last_name'" :direction="$sortDir"
|
||
wire:click="sort('last_name')">{{ __('Name') }}</flux:table.column>
|
||
<flux:table.column sortable :sorted="$sortBy === 'email'" :direction="$sortDir"
|
||
wire:click="sort('email')">{{ __('Kontakt') }}</flux:table.column>
|
||
<flux:table.column sortable :sorted="$sortBy === 'company_id'" :direction="$sortDir"
|
||
wire:click="sort('company_id')">{{ __('Firma') }}</flux:table.column>
|
||
<flux:table.column>{{ __('Portal') }}</flux:table.column>
|
||
<flux:table.column sortable :sorted="$sortBy === 'press_releases_count'" :direction="$sortDir"
|
||
wire:click="sort('press_releases_count')">{{ __('PMs') }}</flux:table.column>
|
||
<flux:table.column sortable :sorted="$sortBy === 'created_at'" :direction="$sortDir"
|
||
wire:click="sort('created_at')">{{ __('Hinzugefügt') }}</flux:table.column>
|
||
<flux:table.column>{{ __('Aktionen') }}</flux:table.column>
|
||
</flux:table.columns>
|
||
|
||
<flux:table.rows>
|
||
@forelse($contacts as $contact)
|
||
@php
|
||
$contactDisplayName =
|
||
trim(($contact->first_name ?? '') . ' ' . ($contact->last_name ?? '')) ?:
|
||
__('Kontakt ohne Name');
|
||
$contactCompanyName = $contact->company?->name ?? __('Unbekannte Firma');
|
||
@endphp
|
||
|
||
|
||
<flux:table.row :key="$contact->id">
|
||
|
||
<flux:table.cell>
|
||
<div class="flex gap-2">
|
||
@if (\Illuminate\Support\Facades\Route::has('admin.contacts.edit'))
|
||
<flux:button size="sm" variant="ghost" icon="pencil"
|
||
href="{{ route('admin.contacts.edit', $contact->id) }}" wire:navigate />
|
||
@endif
|
||
@if ($contact->company && \Illuminate\Support\Facades\Route::has('admin.companies.show'))
|
||
<flux:button size="sm" variant="ghost" icon="building-office"
|
||
href="{{ route('admin.companies.show', $contact->company_id) }}"
|
||
wire:navigate />
|
||
@endif
|
||
|
||
</div>
|
||
</flux:table.cell>
|
||
<flux:table.cell>
|
||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] truncate">
|
||
{{ $contactDisplayName }}
|
||
</div>
|
||
</flux:table.cell>
|
||
|
||
<flux:table.cell>
|
||
<div class="space-y-0.5">
|
||
<div class="text-[12.5px]">
|
||
<a href="mailto:{{ $contact->email }}"
|
||
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
|
||
{{ $contact->email ?: __('Keine E-Mail') }}
|
||
</a>
|
||
</div>
|
||
<div class="text-[12px] text-[color:var(--color-ink-3)]">
|
||
{{ $contact->phone ?: __('Kein Telefon') }}
|
||
</div>
|
||
</div>
|
||
</flux:table.cell>
|
||
|
||
<flux:table.cell>
|
||
@if ($contact->company && \Illuminate\Support\Facades\Route::has('admin.companies.show'))
|
||
<a href="{{ route('admin.companies.show', $contact->company_id) }}" wire:navigate
|
||
class="text-[12.5px] text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
|
||
{{ \Illuminate\Support\Str::limit($contact->company->name, 60) }}
|
||
</a>
|
||
@else
|
||
<span class="text-[12.5px] text-[color:var(--color-ink)]">
|
||
{{ \Illuminate\Support\Str::limit($contact->company?->name ?? __('Unbekannte Firma'), 80) }}
|
||
</span>
|
||
@endif
|
||
</flux:table.cell>
|
||
|
||
<flux:table.cell>
|
||
<span class="badge hub">{{ $contact->portal?->label() ?? __('Unbekannt') }}</span>
|
||
</flux:table.cell>
|
||
|
||
<flux:table.cell>
|
||
@if ($contact->press_releases_count > 0)
|
||
<flux:button
|
||
size="sm"
|
||
variant="ghost"
|
||
href="{{ route('admin.press-releases.index', ['contact' => $contact->id]) }}"
|
||
wire:navigate
|
||
>
|
||
{{ $contact->press_releases_count }} {{ __('PMs') }}
|
||
</flux:button>
|
||
@else
|
||
<span class="badge dot">0</span>
|
||
@endif
|
||
</flux:table.cell>
|
||
|
||
<flux:table.cell>
|
||
<span class="text-[12px] text-[color:var(--color-ink-3)]">
|
||
{{ $contact->created_at?->format('d.m.Y H:i') ?? '-' }}
|
||
</span>
|
||
</flux:table.cell>
|
||
|
||
<flux:table.cell>
|
||
<div class="flex gap-2">
|
||
<flux:modal.trigger name="confirm-contact-delete-{{ $contact->id }}">
|
||
<flux:button size="sm" variant="ghost" icon="trash" type="button"
|
||
x-data=""
|
||
x-on:click.prevent="$dispatch('open-modal', 'confirm-contact-delete-{{ $contact->id }}')" />
|
||
</flux:modal.trigger>
|
||
<flux:button size="sm" variant="ghost" icon="envelope"
|
||
href="mailto:{{ $contact->email }}" />
|
||
</div>
|
||
|
||
<flux:modal name="confirm-contact-delete-{{ $contact->id }}" class="max-w-lg">
|
||
<div class="space-y-6">
|
||
<div>
|
||
<flux:heading size="lg">{{ __('Kontakt wirklich löschen?') }}
|
||
</flux:heading>
|
||
<flux:subheading>
|
||
{{ __('Du löschst: :contact (Firma: :company). Dieser Kontakt wird archiviert (Soft Delete) und aus den Standardlisten entfernt.', ['contact' => $contactDisplayName, 'company' => $contactCompanyName]) }}
|
||
</flux:subheading>
|
||
</div>
|
||
|
||
<div class="flex justify-end space-x-2 rtl:space-x-reverse">
|
||
<flux:modal.close>
|
||
<flux:button variant="filled">{{ __('Abbrechen') }}</flux:button>
|
||
</flux:modal.close>
|
||
|
||
<flux:button variant="danger"
|
||
wire:click="deleteContactFromIndex({{ $contact->id }})">
|
||
{{ __('Löschung bestätigen') }}
|
||
</flux:button>
|
||
</div>
|
||
</div>
|
||
</flux:modal>
|
||
</flux:table.cell>
|
||
</flux:table.row>
|
||
@empty
|
||
<flux:table.row>
|
||
<flux:table.cell colspan="8">
|
||
<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.user-group class="size-6" />
|
||
</div>
|
||
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
|
||
{{ __('Keine Kontakte gefunden') }}
|
||
</div>
|
||
</div>
|
||
</flux:table.cell>
|
||
</flux:table.row>
|
||
@endforelse
|
||
</flux:table.rows>
|
||
</flux:table>
|
||
|
||
<div class="border-t border-[color:var(--color-bg-rule)] p-4">
|
||
{{ $contacts->links('components.portal.pagination') }}
|
||
</div>
|
||
</article>
|
||
</div>
|