12-05-2026 Frontend dev
This commit is contained in:
parent
405df0a122
commit
5b8bdf4182
779 changed files with 480564 additions and 6241 deletions
729
resources/views/livewire/admin/contacts/index.blade.php
Normal file
729
resources/views/livewire/admin/contacts/index.blade.php
Normal file
|
|
@ -0,0 +1,729 @@
|
|||
<?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)
|
||||
->simplePaginate(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-6">
|
||||
@if ($notification)
|
||||
<div x-data="{ show: true }" x-init="setTimeout(() => show = false, 3000)" x-show="show" x-transition
|
||||
class="rounded-md px-4 py-3 text-sm border
|
||||
{{ $notificationType === 'error'
|
||||
? 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 text-red-800 dark:text-red-300'
|
||||
: 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800 text-green-800 dark:text-green-300' }}">
|
||||
{{ $notification }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Statistiken --}}
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<flux:card>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Gesamt') }}</flux:text>
|
||||
<flux:text size="xl" weight="bold">{{ $stats['total'] }}</flux:text>
|
||||
</div>
|
||||
<flux:icon.user-group class="size-8 text-blue-500" />
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
<flux:card>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Firmen mit Kontakten') }}
|
||||
</flux:text>
|
||||
<flux:text size="xl" weight="bold">{{ $stats['companies_with_contacts'] }}</flux:text>
|
||||
</div>
|
||||
<flux:icon.building-office class="size-8 text-green-500" />
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
<flux:card>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Ø pro Firma') }}</flux:text>
|
||||
<flux:text size="xl" weight="bold">{{ number_format($stats['avg_per_company'], 1) }}
|
||||
</flux:text>
|
||||
</div>
|
||||
<flux:icon.chart-bar class="size-8 text-purple-500" />
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
|
||||
{{-- Filter & Actions --}}
|
||||
<flux:card>
|
||||
<div class="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>
|
||||
|
||||
@if (\Illuminate\Support\Facades\Route::has('admin.contacts.create'))
|
||||
<flux:button icon="plus" href="{{ route('admin.contacts.create') }}" wire:navigate>
|
||||
{{ __('Neuer Kontakt') }}
|
||||
</flux:button>
|
||||
@else
|
||||
<flux:button icon="plus" disabled>
|
||||
{{ __('Neuer Kontakt') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
<flux:card>
|
||||
<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="subtle" icon="bookmark">
|
||||
{{ __('Preset speichern') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<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" class="mt-3" />
|
||||
</flux:card>
|
||||
|
||||
{{-- Tabelle --}}
|
||||
<flux:card class="overflow-hidden">
|
||||
<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>
|
||||
<flux:text weight="semibold truncate">
|
||||
{{ $contactDisplayName }}
|
||||
</flux:text>
|
||||
</div>
|
||||
</flux:table.cell>
|
||||
|
||||
<flux:table.cell>
|
||||
<div class="space-y-1">
|
||||
<flux:text class="text-sm">
|
||||
<a href="mailto:{{ $contact->email }}"
|
||||
class="text-blue-600 hover:underline dark:text-blue-400">
|
||||
{{ $contact->email ?: __('Keine E-Mail') }}
|
||||
</a>
|
||||
</flux:text>
|
||||
<flux:text class="text-sm text-zinc-500">{{ $contact->phone ?: __('Kein Telefon') }}
|
||||
</flux:text>
|
||||
</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-blue-600 hover:underline dark:text-blue-400">
|
||||
{{ \Illuminate\Support\Str::limit($contact->company->name, 60) }}
|
||||
</a>
|
||||
@else
|
||||
<flux:text>
|
||||
{{ \Illuminate\Support\Str::limit($contact->company?->name ?? __('Unbekannte Firma'), 80) }}
|
||||
</flux:text>
|
||||
@endif
|
||||
</flux:table.cell>
|
||||
|
||||
<flux:table.cell>
|
||||
<flux:badge color="{{ $this->portalBadgeColor($contact->portal) }}" size="sm">
|
||||
{{ $contact->portal?->label() ?? __('Unbekannt') }}
|
||||
</flux:badge>
|
||||
</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
|
||||
<flux:badge color="zinc" size="sm">0</flux:badge>
|
||||
@endif
|
||||
</flux:table.cell>
|
||||
|
||||
<flux:table.cell>
|
||||
<flux:text class="text-sm text-zinc-500">
|
||||
{{ $contact->created_at?->format('d.m.Y H:i') ?? '-' }}
|
||||
</flux:text>
|
||||
</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 py-12">
|
||||
<flux:icon.user-group class="size-12 text-zinc-400 dark:text-zinc-600" />
|
||||
<flux:text class="mt-4 text-zinc-500">{{ __('Keine Kontakte gefunden') }}</flux:text>
|
||||
</div>
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
@endforelse
|
||||
</flux:table.rows>
|
||||
</flux:table>
|
||||
|
||||
<div class="border-t border-zinc-200 p-4 dark:border-zinc-700">
|
||||
{{ $contacts->links() }}
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
Loading…
Add table
Add a link
Reference in a new issue