Optimierung 5.1: Tabellen-Concerns (Sortierung, Volltext-Heuristik, Stats)

- HasSortableTable::sort() ersetzt 5 byte-identische sort()-Kopien in den
  Admin-Listen (Benutzer, Firmen, Kategorien, Kontakte, Pressemitteilungen).
  $sortBy/$sortDir-Defaults bleiben je Komponente, da sie divergieren.
- HasFullTextSearch::supportsFullTextSearch() ersetzt 9 byte-identische Kopien
  (4 Index-Listen + 5 Firmen-Comboboxen); ungenutzte DB-Imports entfernt.
- AdminPerformanceCache::activeInactiveStats(Builder) buendelt den identischen
  SUM(CASE WHEN is_active)-Block der Benutzer- und Firmenliste.

Zurueckgestellt (real divergent): HasResettableFilters (updatedXxxFilter
normalisieren teils zusaetzlich) und eine gemeinsame hydrateRelationCounts().

Tests: erweiterte AdminPerformanceCacheTest; 99 Admin-Tests bleiben gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-06-19 12:36:48 +00:00
parent 8e0aedaf95
commit a8958c176e
15 changed files with 111 additions and 145 deletions

View file

@ -5,7 +5,6 @@ use App\Models\Category;
use App\Models\PressRelease;
use App\Services\Admin\AdminPerformanceCache;
use App\Services\CurrentPortalContext;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Volt\Component;
@ -13,6 +12,8 @@ use Livewire\WithPagination;
new #[Layout('components.layouts.app'), Title('Kategorien')] class extends Component
{
use \App\Livewire\Concerns\HasFullTextSearch;
use \App\Livewire\Concerns\HasSortableTable;
use WithPagination;
public string $search = '';
@ -21,17 +22,6 @@ new #[Layout('components.layouts.app'), Title('Kategorien')] class extends Compo
public string $sortDir = 'asc';
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 updatedSearch(): void
{
$this->resetPage();
@ -134,11 +124,6 @@ new #[Layout('components.layouts.app'), Title('Kategorien')] class extends Compo
});
}
private function supportsFullTextSearch(string $term): bool
{
return mb_strlen($term) >= 3
&& in_array(DB::connection()->getDriverName(), ['mysql', 'pgsql'], true);
}
}; ?>
<div class="space-y-8">

View file

@ -7,7 +7,6 @@ use App\Models\PressRelease;
use App\Models\User;
use App\Services\Admin\AdminPerformanceCache;
use App\Services\CurrentPortalContext;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
@ -16,6 +15,8 @@ use Livewire\WithPagination;
new #[Layout('components.layouts.app'), Title('Firmen')] class extends Component
{
use \App\Livewire\Concerns\HasFullTextSearch;
use \App\Livewire\Concerns\HasSortableTable;
use WithPagination;
public string $search = '';
@ -42,17 +43,6 @@ new #[Layout('components.layouts.app'), Title('Firmen')] class extends Component
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 with(): array
{
$sortable = ['name', 'email', 'is_active', 'press_releases_count', 'contacts_count', 'created_at'];
@ -124,20 +114,11 @@ new #[Layout('components.layouts.app'), Title('Firmen')] class extends Component
$portal = CurrentPortalContext::get()?->value ?? 'all';
$cache = app(AdminPerformanceCache::class);
return $cache->remember($cache->companiesStatsKey($portal), AdminPerformanceCache::StatsTtl, function (): array {
$stats = Company::query()
->toBase()
->selectRaw('COUNT(*) as total')
->selectRaw('SUM(CASE WHEN is_active = ? THEN 1 ELSE 0 END) as active', [true])
->selectRaw('SUM(CASE WHEN is_active = ? THEN 1 ELSE 0 END) as inactive', [false])
->first();
return [
'total' => (int) ($stats->total ?? 0),
'active' => (int) ($stats->active ?? 0),
'inactive' => (int) ($stats->inactive ?? 0),
];
});
return $cache->remember(
$cache->companiesStatsKey($portal),
AdminPerformanceCache::StatsTtl,
fn (): array => $cache->activeInactiveStats(Company::query()),
);
}
private function hydrateCounts($companies): void
@ -166,12 +147,6 @@ new #[Layout('components.layouts.app'), Title('Firmen')] class extends Component
});
}
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();

View file

@ -3,13 +3,14 @@
use App\Enums\Portal;
use App\Models\Company;
use App\Models\Contact;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Volt\Component;
new #[Layout('components.layouts.app'), Title('Firma anzeigen')] class extends Component
{
use \App\Livewire\Concerns\HasFullTextSearch;
public int $id;
public string $activeTab = 'overview';
@ -175,12 +176,6 @@ new #[Layout('components.layouts.app'), Title('Firma anzeigen')] class extends C
];
}
private function supportsFullTextSearch(string $term): bool
{
return mb_strlen($term) >= 3
&& in_array(DB::connection()->getDriverName(), ['mysql', 'pgsql'], true);
}
private function portalBadgeColor(?Portal $portal): string
{
return match ($portal) {

View file

@ -3,7 +3,6 @@
use App\Enums\Portal;
use App\Models\Company;
use App\Models\Contact;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
@ -11,6 +10,8 @@ use Livewire\Volt\Component;
new #[Layout('components.layouts.app'), Title('Kontakt anlegen')] class extends Component
{
use \App\Livewire\Concerns\HasFullTextSearch;
public int|string|null $companyId = null;
public string $companySearch = '';
@ -136,11 +137,6 @@ new #[Layout('components.layouts.app'), Title('Kontakt anlegen')] class extends
];
}
private function supportsFullTextSearch(string $term): bool
{
return mb_strlen($term) >= 3
&& in_array(DB::connection()->getDriverName(), ['mysql', 'pgsql'], true);
}
}; ?>
<form wire:submit="save" class="space-y-8">

View file

@ -3,7 +3,6 @@
use App\Enums\Portal;
use App\Models\Company;
use App\Models\Contact;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Locked;
@ -12,6 +11,8 @@ use Livewire\Volt\Component;
new #[Layout('components.layouts.app'), Title('Kontakt bearbeiten')] class extends Component
{
use \App\Livewire\Concerns\HasFullTextSearch;
#[Locked]
public int $id;
@ -140,12 +141,6 @@ new #[Layout('components.layouts.app'), Title('Kontakt bearbeiten')] class exten
];
}
private function supportsFullTextSearch(string $term): bool
{
return mb_strlen($term) >= 3
&& in_array(DB::connection()->getDriverName(), ['mysql', 'pgsql'], true);
}
public function deleteContact(): void
{
$contact = Contact::query()->find($this->id);

View file

@ -7,7 +7,6 @@ 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;
@ -17,6 +16,8 @@ use Livewire\WithPagination;
new #[Layout('components.layouts.app'), Title('Kontakte')] class extends Component
{
use \App\Livewire\Concerns\HasFullTextSearch;
use \App\Livewire\Concerns\HasSortableTable;
use WithPagination;
public string $search = '';
@ -48,17 +49,6 @@ new #[Layout('components.layouts.app'), Title('Kontakte')] class extends Compone
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'
@ -259,12 +249,6 @@ new #[Layout('components.layouts.app'), Title('Kontakte')] class extends Compone
});
}
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();
@ -616,7 +600,6 @@ new #[Layout('components.layouts.app'), Title('Kontakte')] class extends Compone
$contactCompanyName = $contact->company?->name ?? __('Unbekannte Firma');
@endphp
<flux:table.row :key="$contact->id">
<flux:table.cell>

View file

@ -11,7 +11,6 @@ use App\Services\Admin\AdminPerformanceCache;
use App\Services\PressRelease\BlacklistViolationException;
use App\Services\PressRelease\PressReleaseService;
use Flux\Flux;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
@ -21,6 +20,8 @@ use Livewire\WithPagination;
new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class extends Component
{
use \App\Livewire\Concerns\HandlesAdminPressReleaseActions;
use \App\Livewire\Concerns\HasFullTextSearch;
use \App\Livewire\Concerns\HasSortableTable;
use WithPagination;
public string $search = '';
@ -57,17 +58,6 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
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 updatedSearch(): void
{
$this->resetPage();
@ -382,11 +372,6 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
->get();
}
private function supportsFullTextSearch(string $term): bool
{
return mb_strlen($term) >= 3
&& in_array(DB::connection()->getDriverName(), ['mysql', 'pgsql'], true);
}
}; ?>
<div class="space-y-8">

View file

@ -16,6 +16,7 @@ use Spatie\Permission\Models\Role;
new #[Layout('components.layouts.app'), Title('Benutzer')] class extends Component
{
use \App\Livewire\Concerns\HasSortableTable;
use WithPagination;
public string $search = '';
@ -36,17 +37,6 @@ new #[Layout('components.layouts.app'), Title('Benutzer')] class extends Compone
public ?int $viewingUserId = null;
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 with(): array
{
$sortable = ['name', 'email', 'portal', 'is_active', 'last_login_at', 'created_at'];
@ -186,20 +176,13 @@ new #[Layout('components.layouts.app'), Title('Benutzer')] class extends Compone
*/
private function stats(): array
{
return app(AdminPerformanceCache::class)->remember(AdminPerformanceCache::UserStats, AdminPerformanceCache::StatsTtl, function (): array {
$stats = User::query()
->toBase()
->selectRaw('COUNT(*) as total')
->selectRaw('SUM(CASE WHEN is_active = ? THEN 1 ELSE 0 END) as active', [true])
->selectRaw('SUM(CASE WHEN is_active = ? THEN 1 ELSE 0 END) as inactive', [false])
->first();
$cache = app(AdminPerformanceCache::class);
return [
'total' => (int) ($stats->total ?? 0),
'active' => (int) ($stats->active ?? 0),
'inactive' => (int) ($stats->inactive ?? 0),
];
});
return $cache->remember(
AdminPerformanceCache::UserStats,
AdminPerformanceCache::StatsTtl,
fn (): array => $cache->activeInactiveStats(User::query()),
);
}
private function hydrateCompanyCounts($users): void
@ -576,7 +559,6 @@ new #[Layout('components.layouts.app'), Title('Benutzer')] class extends Compone
</flux:text>
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>

View file

@ -6,7 +6,6 @@ use App\Models\Company;
use App\Models\User;
use App\Services\Admin\AdminPerformanceCache;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
@ -15,6 +14,8 @@ use Spatie\Permission\Models\Role;
new #[Layout('components.layouts.app'), Title('Benutzer anlegen')] class extends Component
{
use \App\Livewire\Concerns\HasFullTextSearch;
public string $name = '';
public string $email = '';
@ -213,12 +214,6 @@ new #[Layout('components.layouts.app'), Title('Benutzer anlegen')] class extends
->get(['id', 'name']));
}
private function supportsFullTextSearch(string $term): bool
{
return mb_strlen($term) >= 3
&& in_array(DB::connection()->getDriverName(), ['mysql', 'pgsql'], true);
}
protected function billingIsComplete(): bool
{
return filled($this->billing['name'])

View file

@ -7,7 +7,6 @@ use App\Models\Contact;
use App\Models\User;
use App\Services\Admin\AdminPerformanceCache;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Locked;
@ -17,6 +16,8 @@ use Spatie\Permission\Models\Role;
new #[Layout('components.layouts.app'), Title('Benutzer bearbeiten')] class extends Component
{
use \App\Livewire\Concerns\HasFullTextSearch;
#[Locked]
public int $id;
@ -624,12 +625,6 @@ new #[Layout('components.layouts.app'), Title('Benutzer bearbeiten')] class exte
->get(['id', 'name']));
}
private function supportsFullTextSearch(string $term): bool
{
return mb_strlen($term) >= 3
&& in_array(DB::connection()->getDriverName(), ['mysql', 'pgsql'], true);
}
protected function syncContactFormsToSelectedCompanies(): void
{
$currentFormsById = collect($this->contactForms)->keyBy('id');