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

@ -0,0 +1,20 @@
<?php
namespace App\Livewire\Concerns;
use Illuminate\Support\Facades\DB;
/**
* Gemeinsame Heuristik, ob für einen Suchbegriff der DB-Volltextindex genutzt
* werden kann: ab drei Zeichen und nur auf MySQL/PostgreSQL (SQLite im Test
* fällt auf die LIKE-Suche zurück). Single Source of Truth für die Admin-Listen
* und Firmen-Comboboxen.
*/
trait HasFullTextSearch
{
private function supportsFullTextSearch(string $term): bool
{
return mb_strlen($term) >= 3
&& in_array(DB::connection()->getDriverName(), ['mysql', 'pgsql'], true);
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace App\Livewire\Concerns;
/**
* Spalten-Sortierung für Admin-Tabellen. Erwartet die öffentlichen Properties
* `$sortBy`/`$sortDir` (deren Defaults bleiben bewusst in der jeweiligen
* Komponente, da sie je Tabelle divergieren) sowie `resetPage()` aus
* `Livewire\WithPagination`.
*/
trait HasSortableTable
{
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();
}
}

View file

@ -5,6 +5,7 @@ namespace App\Services\Admin;
use App\Enums\Portal;
use App\Enums\PressReleaseStatus;
use App\Models\PressRelease;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Cache;
class AdminPerformanceCache
@ -49,6 +50,29 @@ class AdminPerformanceCache
}
}
/**
* Aktiv-/Inaktiv-Kennzahlen einer `is_active`-Tabelle in einer einzigen
* Aggregat-Abfrage. Single Source of Truth für die identischen Stat-Blöcke
* der Benutzer- und Firmen-Liste.
*
* @param Builder<covariant \Illuminate\Database\Eloquent\Model> $query
* @return array{total: int, active: int, inactive: int}
*/
public function activeInactiveStats(Builder $query): array
{
$stats = $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),
];
}
public function pressReleaseReviewCount(): int
{
return (int) $this->remember(self::PressReleaseReviewCount, self::StatsTtl, function (): int {