22-05-2026 Optimierung der User und Admin Panels
This commit is contained in:
parent
d2ba22c0cf
commit
e8c47b7553
73 changed files with 10282 additions and 1546 deletions
|
|
@ -66,7 +66,7 @@ new #[Layout('components.layouts.app'), Title('Kategorien')] class extends Compo
|
|||
})
|
||||
->orderBy($sort, $this->sortDir);
|
||||
|
||||
$categories = $categoriesQuery->simplePaginate(50);
|
||||
$categories = $categoriesQuery->paginate(50);
|
||||
|
||||
if (! $sortsByCount) {
|
||||
$this->hydrateCounts($categories);
|
||||
|
|
@ -306,5 +306,5 @@ new #[Layout('components.layouts.app'), Title('Kategorien')] class extends Compo
|
|||
@endforelse
|
||||
</section>
|
||||
|
||||
{{ $categories->links() }}
|
||||
{{ $categories->links('components.portal.pagination') }}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ new #[Layout('components.layouts.app'), Title('Firmen')] class extends Component
|
|||
})
|
||||
->orderBy($sort, $this->sortDir);
|
||||
|
||||
$companies = $companiesQuery->simplePaginate(50);
|
||||
$companies = $companiesQuery->paginate(50);
|
||||
|
||||
if (! $sortsByCount) {
|
||||
$this->hydrateCounts($companies);
|
||||
|
|
@ -586,7 +586,7 @@ new #[Layout('components.layouts.app'), Title('Firmen')] class extends Component
|
|||
</flux:table.rows>
|
||||
</flux:table>
|
||||
<div class="border-t border-[color:var(--color-bg-rule)] p-4">
|
||||
{{ $companies->links() }}
|
||||
{{ $companies->links('components.portal.pagination') }}
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ new #[Layout('components.layouts.app'), Title('Kontakte')] class extends Compone
|
|||
$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);
|
||||
->paginate(50);
|
||||
|
||||
// Firmen-Filter: nur Live-Suche, nie alle laden
|
||||
$term = trim($this->companySearch);
|
||||
|
|
@ -745,7 +745,7 @@ new #[Layout('components.layouts.app'), Title('Kontakte')] class extends Compone
|
|||
</flux:table>
|
||||
|
||||
<div class="border-t border-[color:var(--color-bg-rule)] p-4">
|
||||
{{ $contacts->links() }}
|
||||
{{ $contacts->links('components.portal.pagination') }}
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ new #[Layout('components.layouts.app'), Title('Footer-Codes')] class extends Com
|
|||
</flux:table>
|
||||
|
||||
<div class="border-t border-[color:var(--color-bg-rule)] p-4">
|
||||
{{ $codes->links() }}
|
||||
{{ $codes->links('components.portal.pagination') }}
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -328,7 +328,7 @@ new #[Layout('components.layouts.app'), Title('Legacy Rechnungen')] class extend
|
|||
</flux:table>
|
||||
|
||||
<div class="border-t border-[color:var(--color-bg-rule)] p-4">
|
||||
{{ $invoices->links() }}
|
||||
{{ $invoices->links('components.portal.pagination') }}
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@ new #[Layout('components.layouts.app'), Title('Voreinstellungen')] class extends
|
|||
</flux:table.rows>
|
||||
</flux:table>
|
||||
<div class="border-t border-[color:var(--color-bg-rule)] p-4">
|
||||
{{ $presets->links() }}
|
||||
{{ $presets->links('components.portal.pagination') }}
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -10,6 +10,7 @@ use App\Models\User;
|
|||
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;
|
||||
|
|
@ -157,16 +158,21 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
|
|||
try {
|
||||
app(PressReleaseService::class)->publish($pr);
|
||||
} catch (BlacklistViolationException $e) {
|
||||
session()->flash('error', __('Automatisch abgelehnt: unzulässiges Wort ":word".', ['word' => $e->word]));
|
||||
Flux::toast(
|
||||
heading: __('Automatisch abgelehnt'),
|
||||
text: __('Unzulässiges Wort gefunden: ":word".', ['word' => $e->word]),
|
||||
variant: 'danger',
|
||||
duration: 8000,
|
||||
);
|
||||
|
||||
return;
|
||||
} catch (\LogicException $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
Flux::toast(text: $e->getMessage(), variant: 'danger');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
session()->flash('success', __('Pressemitteilung veröffentlicht.'));
|
||||
Flux::toast(text: __('Pressemitteilung veröffentlicht.'), variant: 'success');
|
||||
}
|
||||
|
||||
public function reject(int $id): void
|
||||
|
|
@ -176,12 +182,12 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
|
|||
try {
|
||||
app(PressReleaseService::class)->reject($pr, __('Bitte überarbeiten Sie die Pressemitteilung.'));
|
||||
} catch (\LogicException $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
Flux::toast(text: $e->getMessage(), variant: 'danger');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
session()->flash('success', __('Pressemitteilung abgelehnt.'));
|
||||
Flux::toast(text: __('Pressemitteilung abgelehnt.'), variant: 'warning');
|
||||
}
|
||||
|
||||
public function archive(int $id): void
|
||||
|
|
@ -191,12 +197,12 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
|
|||
try {
|
||||
app(PressReleaseService::class)->archive($pr);
|
||||
} catch (\LogicException $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
Flux::toast(text: $e->getMessage(), variant: 'danger');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
session()->flash('success', __('Pressemitteilung archiviert.'));
|
||||
Flux::toast(text: __('Pressemitteilung archiviert.'), variant: 'success');
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
|
|
@ -226,7 +232,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
|
|||
->when($this->companyFilter !== 'all', fn ($q) => $q->where('company_id', (int) $this->companyFilter))
|
||||
->when($this->contactFilter !== 'all', fn ($q) => $q->whereHas('contacts', fn ($contactQuery) => $contactQuery->where('contacts.id', (int) $this->contactFilter)))
|
||||
->orderBy(in_array($this->sortBy, ['title', 'status', 'portal', 'hits', 'created_at']) ? $this->sortBy : 'created_at', $this->sortDir)
|
||||
->simplePaginate(50);
|
||||
->paginate(50);
|
||||
|
||||
return [
|
||||
'pressReleases' => $query,
|
||||
|
|
@ -373,18 +379,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
|
|||
}; ?>
|
||||
|
||||
<div class="space-y-8">
|
||||
@if (session('success'))
|
||||
<div class="px-4 py-3 rounded-[5px] border text-[12.5px]
|
||||
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
@if (session('error'))
|
||||
<div class="px-4 py-3 rounded-[5px] border text-[12.5px]
|
||||
bg-[color:var(--color-err-soft)] border-[color:var(--color-err)]/30 text-[color:var(--color-loss)]">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
{{-- Flash-Banner ersetzt durch <flux:toast /> im Layout. --}}
|
||||
|
||||
{{-- ============== PAGE HEADER ============== --}}
|
||||
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
|
||||
|
|
@ -905,6 +900,18 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
|
|||
<div class="text-[10.5px] text-[color:var(--color-ink-4)] mt-0.5">
|
||||
{{ $dateSubLabel }} · {{ $primaryDate?->format('H:i') }}
|
||||
</div>
|
||||
@if ($status === 'review' && $pr->scheduled_at && $pr->scheduled_at->isFuture())
|
||||
<div class="mt-1 inline-flex items-center gap-1 text-[10.5px] font-mono tabular-nums text-[color:var(--color-accent-deep)]">
|
||||
<flux:icon.calendar variant="micro" class="size-3" />
|
||||
<span>{{ __('geplant') }} · {{ $pr->scheduled_at->format('d.m. H:i') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@if ($pr->embargo_at && $pr->embargo_at->isFuture())
|
||||
<div class="mt-1 inline-flex items-center gap-1 text-[10.5px] font-mono tabular-nums text-[color:var(--color-accent-deep)]">
|
||||
<flux:icon.lock-closed variant="micro" class="size-3" />
|
||||
<span>{{ __('Embargo bis') }} {{ $pr->embargo_at->format('d.m.') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</flux:table.cell>
|
||||
|
||||
<flux:table.cell>
|
||||
|
|
@ -1029,5 +1036,5 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
|
|||
</flux:table>
|
||||
</article>
|
||||
|
||||
{{ $pressReleases->links() }}
|
||||
{{ $pressReleases->links('components.portal.pagination') }}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -28,13 +28,18 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
try {
|
||||
app(PressReleaseService::class)->publish($pr);
|
||||
} catch (BlacklistViolationException $e) {
|
||||
session()->flash('error', __('Automatisch abgelehnt: unzulässiges Wort ":word".', ['word' => $e->word]));
|
||||
Flux::toast(
|
||||
heading: __('Automatisch abgelehnt'),
|
||||
text: __('Unzulässiges Wort gefunden: ":word".', ['word' => $e->word]),
|
||||
variant: 'danger',
|
||||
duration: 8000,
|
||||
);
|
||||
Flux::modal('confirm-show-publish')->close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
session()->flash('success', __('Pressemitteilung veröffentlicht. Autor wurde benachrichtigt.'));
|
||||
Flux::toast(text: __('Pressemitteilung veröffentlicht. Autor wurde benachrichtigt.'), variant: 'success');
|
||||
Flux::modal('confirm-show-publish')->close();
|
||||
}
|
||||
|
||||
|
|
@ -49,7 +54,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
|
||||
$this->rejectReason = '';
|
||||
|
||||
session()->flash('success', __('Pressemitteilung abgelehnt. Autor wurde benachrichtigt.'));
|
||||
Flux::toast(text: __('Pressemitteilung abgelehnt. Autor wurde benachrichtigt.'), variant: 'warning');
|
||||
Flux::modal('confirm-show-reject')->close();
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +62,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
{
|
||||
$pr = PressRelease::withoutGlobalScopes()->findOrFail($this->id);
|
||||
app(PressReleaseService::class)->archive($pr);
|
||||
session()->flash('success', __('Archiviert.'));
|
||||
Flux::toast(text: __('Archiviert.'), variant: 'success');
|
||||
Flux::modal('confirm-show-archive')->close();
|
||||
}
|
||||
|
||||
|
|
@ -65,17 +70,31 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
{
|
||||
$pr = PressRelease::withoutGlobalScopes()
|
||||
->with([
|
||||
'company:id,name,slug',
|
||||
'company:id,name,email,phone,slug',
|
||||
'category.translations',
|
||||
'user:id,name',
|
||||
'user:id,name,email',
|
||||
'images',
|
||||
'attachments',
|
||||
'contacts' => fn ($query) => $query
|
||||
->withoutGlobalScopes()
|
||||
->orderBy('last_name')
|
||||
->orderBy('first_name')
|
||||
->select(['contacts.id', 'contacts.company_id', 'contacts.first_name', 'contacts.last_name', 'contacts.responsibility', 'contacts.email', 'contacts.phone']),
|
||||
'statusLogs.changedBy:id,name',
|
||||
])
|
||||
->findOrFail($this->id);
|
||||
|
||||
$latestRejection = null;
|
||||
if ($pr->status->value === 'rejected') {
|
||||
$latestRejection = $pr->statusLogs
|
||||
->firstWhere(fn ($log) => $log->to_status?->value === 'rejected');
|
||||
}
|
||||
|
||||
return [
|
||||
'pr' => $pr,
|
||||
'statusLogs' => $pr->statusLogs,
|
||||
'contacts' => $pr->contacts,
|
||||
'latestRejection' => $latestRejection,
|
||||
'categoryName' => $pr->category?->translations->firstWhere('locale', 'de')?->name
|
||||
?? $pr->category?->translations->first()?->name
|
||||
?? '–',
|
||||
|
|
@ -100,18 +119,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
};
|
||||
@endphp
|
||||
|
||||
@if (session('success'))
|
||||
<div class="px-4 py-3 rounded-[5px] border text-[12.5px]
|
||||
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
@if (session('error'))
|
||||
<div class="px-4 py-3 rounded-[5px] border text-[12.5px]
|
||||
bg-[color:var(--color-err-soft)] border-[color:var(--color-err)]/30 text-[color:var(--color-loss)]">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
{{-- Flash-Banner ersetzt durch <flux:toast /> im Layout. --}}
|
||||
|
||||
{{-- ============== PAGE HEADER ============== --}}
|
||||
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
|
||||
|
|
@ -126,6 +134,11 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
|
||||
{{ $pr->title }}
|
||||
</h1>
|
||||
@if ($pr->subtitle)
|
||||
<p class="text-[18px] font-medium tracking-[-0.2px] leading-[1.35] mt-2 m-0 max-w-[720px] text-[color:var(--color-ink-2)]">
|
||||
{{ $pr->subtitle }}
|
||||
</p>
|
||||
@endif
|
||||
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[720px] text-[color:var(--color-ink-2)]">
|
||||
<strong class="font-semibold text-[color:var(--color-ink)]">{{ __('Firma') }}:</strong>
|
||||
{{ $pr->company?->name ?? '–' }}
|
||||
|
|
@ -148,39 +161,98 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
</div>
|
||||
</header>
|
||||
|
||||
{{-- ============== REJECTION-HINWEIS ============== --}}
|
||||
@if ($pr->status === \App\Enums\PressReleaseStatus::Rejected && $latestRejection)
|
||||
<article class="panel" style="border-color:var(--color-err); border-left-width:3px;">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Diese Pressemitteilung wurde abgelehnt') }}</span>
|
||||
<span class="badge err dot">{{ __('Handlung erforderlich') }}</span>
|
||||
</div>
|
||||
<div class="p-5 flex items-start gap-3">
|
||||
<div class="w-9 h-9 rounded-[5px] flex items-center justify-center flex-shrink-0
|
||||
bg-[color:var(--color-err-soft)] border border-[color:var(--color-err)]/30 text-[color:var(--color-loss)]">
|
||||
<flux:icon.exclamation-triangle class="size-[18px]" />
|
||||
</div>
|
||||
<div class="flex-1 text-[13px] text-[color:var(--color-ink-2)]">
|
||||
@if ($latestRejection->reason)
|
||||
<strong class="text-[color:var(--color-ink)] font-semibold">{{ __('Begründung') }}:</strong>
|
||||
<span class="block mt-1 whitespace-pre-line">{{ $latestRejection->reason }}</span>
|
||||
@else
|
||||
{{ __('Der Autor sollte den Inhalt überarbeiten und erneut einreichen.') }}
|
||||
@endif
|
||||
<span class="mt-2 block text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ __('Abgelehnt am') }} {{ $latestRejection->created_at->format('d.m.Y H:i') }}
|
||||
@if ($latestRejection->changedBy)
|
||||
· {{ __('durch :name', ['name' => $latestRejection->changedBy->name]) }}
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@endif
|
||||
|
||||
{{-- ============== STATUS-WORKFLOW ============== --}}
|
||||
@if ($pr->status === \App\Enums\PressReleaseStatus::Review)
|
||||
<article class="panel">
|
||||
<article class="panel" style="border-color:var(--color-warn); border-left-width:3px;">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Status-Workflow') }}</span>
|
||||
<span class="badge warn dot">{{ __('Wartet auf Prüfung') }}</span>
|
||||
</div>
|
||||
<div class="p-5 flex flex-wrap items-center gap-3">
|
||||
<p class="text-[13px] text-[color:var(--color-ink-2)] m-0 flex-1 min-w-[200px]">
|
||||
{{ __('Diese PM wartet auf Prüfung.') }}
|
||||
</p>
|
||||
<flux:modal.trigger name="confirm-show-publish">
|
||||
<flux:button type="button" variant="primary">{{ __('Veröffentlichen') }}</flux:button>
|
||||
</flux:modal.trigger>
|
||||
<flux:modal.trigger name="confirm-show-reject">
|
||||
<flux:button type="button" variant="danger">{{ __('Ablehnen') }}</flux:button>
|
||||
</flux:modal.trigger>
|
||||
<div class="p-5 flex flex-wrap items-start gap-3">
|
||||
<div class="w-9 h-9 rounded-[5px] flex items-center justify-center flex-shrink-0
|
||||
bg-[color:var(--color-warn-soft)] border border-[color:var(--color-warn)]/30 text-[color:var(--color-accent-deep)]">
|
||||
<flux:icon.clock class="size-[18px]" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-[220px] text-[13px] text-[color:var(--color-ink-2)]">
|
||||
<p class="m-0">{{ __('Diese PM wartet auf eine redaktionelle Entscheidung.') }}</p>
|
||||
@if ($pr->scheduled_at)
|
||||
<p class="m-0 mt-1 text-[12px] text-[color:var(--color-ink-3)]">
|
||||
<flux:icon.calendar variant="micro" class="inline-block size-3.5 -mt-0.5 mr-0.5" />
|
||||
{{ __('Geplante Veröffentlichung: :date', ['date' => $pr->scheduled_at->format('d.m.Y H:i')]) }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<flux:modal.trigger name="confirm-show-publish">
|
||||
<flux:button type="button" variant="primary">{{ __('Veröffentlichen') }}</flux:button>
|
||||
</flux:modal.trigger>
|
||||
<flux:modal.trigger name="confirm-show-reject">
|
||||
<flux:button type="button" variant="danger">{{ __('Ablehnen') }}</flux:button>
|
||||
</flux:modal.trigger>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@endif
|
||||
|
||||
@if ($pr->status === \App\Enums\PressReleaseStatus::Published)
|
||||
<article class="panel">
|
||||
<article class="panel" style="border-color:var(--color-ok); border-left-width:3px;">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Status-Workflow') }}</span>
|
||||
<span class="badge ok dot">{{ __('Live') }}</span>
|
||||
</div>
|
||||
<div class="p-5 flex flex-wrap items-center gap-3">
|
||||
@if ($pr->hits > 0)
|
||||
<p class="text-[13px] text-[color:var(--color-ink-2)] m-0 flex-1 min-w-[200px]">
|
||||
<strong class="text-[color:var(--color-ink)] font-semibold">{{ number_format($pr->hits) }}</strong>
|
||||
{{ __('Aufrufe seit Veröffentlichung') }}
|
||||
<div class="p-5 flex flex-wrap items-start gap-3">
|
||||
<div class="w-9 h-9 rounded-[5px] flex items-center justify-center flex-shrink-0
|
||||
bg-[color:var(--color-ok-soft)] border border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
|
||||
<flux:icon.check-circle class="size-[18px]" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-[220px] text-[13px] text-[color:var(--color-ink-2)]">
|
||||
<p class="m-0">
|
||||
{{ __('Veröffentlicht am') }}
|
||||
<strong class="text-[color:var(--color-ink)] font-semibold">{{ $pr->published_at?->format('d.m.Y H:i') ?? '–' }}</strong>
|
||||
</p>
|
||||
@endif
|
||||
@if ($pr->embargo_at && $pr->embargo_at->isFuture())
|
||||
<p class="m-0 mt-1 text-[12px] text-[color:var(--color-ink-3)]">
|
||||
<flux:icon.lock-closed variant="micro" class="inline-block size-3.5 -mt-0.5 mr-0.5" />
|
||||
{{ __('Sperrfrist bis: :date', ['date' => $pr->embargo_at->format('d.m.Y H:i')]) }}
|
||||
</p>
|
||||
@endif
|
||||
@if ($pr->hits > 0)
|
||||
<p class="m-0 mt-1 text-[12px] text-[color:var(--color-ink-3)]">
|
||||
<strong class="text-[color:var(--color-ink)] font-semibold">{{ number_format($pr->hits, 0, ',', '.') }}</strong>
|
||||
{{ __('Aufrufe seit Veröffentlichung') }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
<flux:modal.trigger name="confirm-show-archive">
|
||||
<flux:button type="button" variant="ghost">{{ __('Archivieren') }}</flux:button>
|
||||
</flux:modal.trigger>
|
||||
|
|
@ -188,142 +260,256 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
</article>
|
||||
@endif
|
||||
|
||||
{{-- ============== TEXT + SIDEBAR ============== --}}
|
||||
<div class="grid gap-6 lg:grid-cols-[1fr,300px]">
|
||||
{{-- ============== KONTAKTE + STATUS/VERLAUF ============== --}}
|
||||
<div class="grid gap-6 xl:grid-cols-2">
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Inhalt') }}</span>
|
||||
<span class="section-eyebrow">{{ __('Zugeordnete Pressekontakte') }}</span>
|
||||
@if ($pr->company)
|
||||
<flux:button size="sm" variant="ghost" icon="building-office" href="{{ route('admin.companies.show', $pr->company->id) }}" wire:navigate>
|
||||
{{ __('Firma') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<div class="prose prose-zinc dark:prose-invert max-w-none text-[color:var(--color-ink)]">
|
||||
{!! $pr->renderedText() !!}
|
||||
<p class="text-[12px] text-[color:var(--color-ink-3)] mt-0 mb-4">
|
||||
{{ __('Kontakte, die dieser Pressemitteilung zugeordnet sind.') }}
|
||||
</p>
|
||||
<div class="space-y-2">
|
||||
@forelse ($contacts as $contact)
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)]">
|
||||
{{ trim(($contact->first_name ?? '').' '.($contact->last_name ?? '')) ?: __('Kontakt ohne Name') }}
|
||||
</div>
|
||||
<div class="text-[12px] text-[color:var(--color-ink-3)]">
|
||||
{{ $contact->responsibility ?: __('Keine Rolle hinterlegt') }}
|
||||
</div>
|
||||
<div class="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
@if ($contact->email)
|
||||
<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 }}
|
||||
</a>
|
||||
@endif
|
||||
@if ($contact->phone)
|
||||
<span>{{ $contact->phone }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="rounded-[5px] border border-dashed border-[color:var(--color-bg-rule)] p-4 text-[12.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ __('Dieser Pressemitteilung ist kein Pressekontakt zugeordnet.') }}
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<aside class="space-y-4">
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Details') }}</span>
|
||||
</div>
|
||||
<dl class="p-5 space-y-2.5 text-[12.5px]">
|
||||
<div class="flex justify-between gap-2">
|
||||
<dt class="text-[color:var(--color-ink-3)]">{{ __('Status') }}</dt>
|
||||
<dd class="font-semibold text-[color:var(--color-ink)]">{{ $pr->status->label() }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-2">
|
||||
<dt class="text-[color:var(--color-ink-3)]">{{ __('Erstellt') }}</dt>
|
||||
<dd class="text-[color:var(--color-ink)]">{{ $pr->created_at->format('d.m.Y H:i') }}</dd>
|
||||
</div>
|
||||
@if ($pr->published_at)
|
||||
<div class="flex justify-between gap-2">
|
||||
<dt class="text-[color:var(--color-ink-3)]">{{ __('Veröffentlicht') }}</dt>
|
||||
<dd class="text-[color:var(--color-ink)]">{{ $pr->published_at->format('d.m.Y H:i') }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex justify-between gap-2">
|
||||
<dt class="text-[color:var(--color-ink-3)]">{{ __('Aufrufe') }}</dt>
|
||||
<dd class="text-[color:var(--color-ink)] font-semibold">{{ number_format($pr->hits) }}</dd>
|
||||
</div>
|
||||
@if ($pr->keywords)
|
||||
<div class="pt-2 border-t border-[color:var(--color-bg-rule)]">
|
||||
<dt class="text-[color:var(--color-ink-3)] mb-1">{{ __('Stichwörter') }}</dt>
|
||||
<dd class="text-[color:var(--color-ink)]">{{ $pr->keywords }}</dd>
|
||||
</div>
|
||||
@endif
|
||||
@if ($pr->backlink_url)
|
||||
<div class="pt-2 border-t border-[color:var(--color-bg-rule)]">
|
||||
<dt class="text-[color:var(--color-ink-3)] mb-1">{{ __('Backlink') }}</dt>
|
||||
<dd class="break-all">
|
||||
<a href="{{ $pr->backlink_url }}" target="_blank"
|
||||
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
|
||||
{{ $pr->backlink_url }}
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
@endif
|
||||
@if ($pr->no_export)
|
||||
<div class="mt-2 pt-2 border-t border-[color:var(--color-bg-rule)]">
|
||||
<span class="badge hub">{{ __('Kein Export') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
@if ($pr->images->isNotEmpty())
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Bilder') }}</span>
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ $pr->images->count() }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="p-5 space-y-2">
|
||||
@foreach ($pr->images as $image)
|
||||
<div class="flex items-center gap-2 text-[12.5px]">
|
||||
<flux:icon.photo class="size-4 flex-shrink-0 text-[color:var(--color-ink-3)]" />
|
||||
<span class="truncate text-[color:var(--color-ink-2)]">{{ basename($image->path) }}</span>
|
||||
@if ($image->is_preview)
|
||||
<span class="badge hub">{{ __('Preview') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</article>
|
||||
@endif
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{{-- ============== STATUS-VERLAUF ============== --}}
|
||||
@if ($statusLogs->isNotEmpty())
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Status-Verlauf') }}</span>
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ $statusLogs->count() }} {{ __('Einträge') }}
|
||||
</span>
|
||||
<span class="section-eyebrow">{{ __('Status & Verlauf') }}</span>
|
||||
<span @class(['badge', $statusClass])>{{ $pr->status->label() }}</span>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<ol class="space-y-3 border-s border-[color:var(--color-bg-rule)] ps-4">
|
||||
@foreach ($statusLogs as $log)
|
||||
<li class="text-[12.5px]">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@php
|
||||
$logClass = match ($log->to_status?->value) {
|
||||
'published' => 'ok',
|
||||
'review' => 'warn',
|
||||
'rejected' => 'err',
|
||||
default => 'hub',
|
||||
};
|
||||
@endphp
|
||||
<span @class(['badge', $logClass])>{{ $log->to_status?->label() ?? $log->to_status }}</span>
|
||||
@if ($log->from_status)
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ __('von') }} {{ $log->from_status->label() }}
|
||||
</span>
|
||||
@endif
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">·</span>
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ $log->created_at->format('d.m.Y H:i') }}
|
||||
</span>
|
||||
@if ($log->changedBy)
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">·</span>
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">{{ $log->changedBy->name }}</span>
|
||||
@endif
|
||||
@if ($log->source !== 'admin')
|
||||
<span class="badge hub">{{ $log->source }}</span>
|
||||
@endif
|
||||
<div class="grid gap-2 sm:grid-cols-2">
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Autor') }}</div>
|
||||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1 truncate">
|
||||
{{ $pr->user?->name ?? '–' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Erstellt') }}</div>
|
||||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
|
||||
{{ $pr->created_at?->format('d.m.Y H:i') ?? '–' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Veröffentlicht') }}</div>
|
||||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
|
||||
{{ $pr->published_at?->format('d.m.Y H:i') ?? '–' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Aufrufe') }}</div>
|
||||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
|
||||
{{ number_format($pr->hits, 0, ',', '.') }}
|
||||
</div>
|
||||
</div>
|
||||
@if ($pr->scheduled_at)
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Geplant') }}</div>
|
||||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
|
||||
{{ $pr->scheduled_at->format('d.m.Y H:i') }}
|
||||
</div>
|
||||
@if ($log->reason)
|
||||
<p class="mt-1.5 text-[color:var(--color-ink-2)] m-0">{{ $log->reason }}</p>
|
||||
@endif
|
||||
</li>
|
||||
@endforeach
|
||||
</ol>
|
||||
</div>
|
||||
@endif
|
||||
@if ($pr->embargo_at)
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Sperrfrist bis') }}</div>
|
||||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
|
||||
{{ $pr->embargo_at->format('d.m.Y H:i') }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($pr->no_export)
|
||||
<div class="mt-3 flex items-center gap-2 text-[12px] text-[color:var(--color-ink-3)]">
|
||||
<flux:icon.no-symbol variant="micro" class="size-3.5" />
|
||||
<span>{{ __('Kein Export aktiv (PM wird nicht über Feeds verteilt).') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="my-4 border-t border-[color:var(--color-bg-rule)]"></div>
|
||||
|
||||
@if ($statusLogs->isNotEmpty())
|
||||
<ol class="space-y-3 border-s border-[color:var(--color-bg-rule)] ps-4">
|
||||
@foreach ($statusLogs as $log)
|
||||
<li class="text-[12.5px]">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@php
|
||||
$logClass = match ($log->to_status?->value) {
|
||||
'published' => 'ok',
|
||||
'review' => 'warn',
|
||||
'rejected' => 'err',
|
||||
default => 'hub',
|
||||
};
|
||||
@endphp
|
||||
<span @class(['badge', $logClass])>{{ $log->to_status?->label() ?? $log->to_status }}</span>
|
||||
@if ($log->from_status)
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ __('von') }} {{ $log->from_status->label() }}
|
||||
</span>
|
||||
@endif
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">·</span>
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ $log->created_at->format('d.m.Y H:i') }}
|
||||
</span>
|
||||
@if ($log->changedBy)
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">·</span>
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">{{ $log->changedBy->name }}</span>
|
||||
@endif
|
||||
@if ($log->source && $log->source !== 'admin')
|
||||
<span class="badge hub">{{ $log->source }}</span>
|
||||
@endif
|
||||
</div>
|
||||
@if ($log->reason)
|
||||
<p class="mt-1.5 text-[color:var(--color-ink-2)] m-0 whitespace-pre-line">{{ $log->reason }}</p>
|
||||
@endif
|
||||
</li>
|
||||
@endforeach
|
||||
</ol>
|
||||
@else
|
||||
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">
|
||||
{{ __('Noch keine Statusänderungen protokolliert.') }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
{{-- ============== INHALT ============== --}}
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Inhalt') }}</span>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<div class="prose prose-zinc dark:prose-invert max-w-none text-[color:var(--color-ink)]">
|
||||
{!! $pr->renderedText() !!}
|
||||
</div>
|
||||
|
||||
@if ($pr->keywords || $pr->backlink_url)
|
||||
<div class="mt-6 space-y-2 border-t border-[color:var(--color-bg-rule)] pt-4 text-[12.5px] text-[color:var(--color-ink-2)]">
|
||||
@if ($pr->keywords)
|
||||
<p class="m-0">
|
||||
<strong class="text-[color:var(--color-ink)] font-semibold">{{ __('Stichwörter') }}:</strong>
|
||||
{{ $pr->keywords }}
|
||||
</p>
|
||||
@endif
|
||||
@if ($pr->backlink_url)
|
||||
<p class="m-0">
|
||||
<strong class="text-[color:var(--color-ink)] font-semibold">{{ __('Backlink') }}:</strong>
|
||||
<a href="{{ $pr->backlink_url }}" target="_blank"
|
||||
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
|
||||
{{ $pr->backlink_url }}
|
||||
</a>
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{{-- ============== BOILERPLATE-OVERRIDE ============== --}}
|
||||
@if ($pr->boilerplate_override)
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Eigener Abbinder (Boilerplate)') }}</span>
|
||||
<span class="badge hub">{{ __('Override') }}</span>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<p class="text-[12px] text-[color:var(--color-ink-3)] mt-0 mb-3">
|
||||
{{ __('Dieser Text wird für diese Pressemitteilung anstelle des Standard-Abbinders der Firma verwendet.') }}
|
||||
</p>
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-4 text-[13px] leading-[1.6] text-[color:var(--color-ink-2)] whitespace-pre-line">
|
||||
{{ $pr->boilerplate_override }}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@endif
|
||||
|
||||
{{-- ============== MEDIEN ============== --}}
|
||||
@if ($pr->images->isNotEmpty())
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Bilder') }}</span>
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ $pr->images->count() }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="p-5 space-y-2">
|
||||
@foreach ($pr->images as $image)
|
||||
<div class="flex items-center gap-2 text-[12.5px]">
|
||||
<flux:icon.photo class="size-4 flex-shrink-0 text-[color:var(--color-ink-3)]" />
|
||||
<span class="truncate text-[color:var(--color-ink-2)]">{{ basename($image->path) }}</span>
|
||||
@if ($image->is_preview)
|
||||
<span class="badge hub">{{ __('Preview') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</article>
|
||||
@endif
|
||||
|
||||
{{-- ANHÄNGE-ANZEIGE — TEMPORÄR DEAKTIVIERT
|
||||
Datei-Uploads erfordern eine vollständige Sicherheitsprüfung.
|
||||
Wird mit dem Anhang-Manager in einer späteren Phase wieder aktiviert.
|
||||
@if ($pr->attachments->isNotEmpty())
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Anhänge') }}</span>
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ $pr->attachments->count() }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="p-5 space-y-2">
|
||||
@foreach ($pr->attachments as $attachment)
|
||||
<div class="flex items-center gap-2 text-[12.5px]">
|
||||
<flux:icon.paper-clip class="size-4 flex-shrink-0 text-[color:var(--color-ink-3)]" />
|
||||
<span class="truncate text-[color:var(--color-ink-2)] flex-1">
|
||||
{{ $attachment->title ?: $attachment->original_name }}
|
||||
</span>
|
||||
<span class="text-[11px] text-[color:var(--color-ink-3)] flex-shrink-0">
|
||||
{{ number_format($attachment->size / 1024, 0, ',', '.') }} KB
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</article>
|
||||
@endif
|
||||
--}}
|
||||
|
||||
@if($pr->status === \App\Enums\PressReleaseStatus::Review)
|
||||
<flux:modal name="confirm-show-publish" class="max-w-lg">
|
||||
<div class="space-y-6">
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use App\Models\Company;
|
|||
use App\Models\User;
|
||||
use App\Services\Admin\AdminPerformanceCache;
|
||||
use Flux\Flux;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Title;
|
||||
|
|
@ -66,18 +66,8 @@ new #[Layout('components.layouts.app'), Title('Benutzer')] class extends Compone
|
|||
'pressReleases as published_press_releases_count' => fn ($query) => $query->where('status', PressReleaseStatus::Published->value),
|
||||
])
|
||||
->withExists(['profile', 'billingAddress'])
|
||||
->when($this->search, function ($query): void {
|
||||
$term = trim($this->search);
|
||||
|
||||
if ($this->supportsFullTextSearch($term)) {
|
||||
$query->whereFullText(['name', 'email'], $term);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$query->where(function ($searchQuery): void {
|
||||
$searchQuery->where('name', 'like', '%'.$this->search.'%')->orWhere('email', 'like', '%'.$this->search.'%');
|
||||
});
|
||||
->when(filled(trim($this->search)), function (Builder $query): void {
|
||||
$this->applySearch($query, $this->search);
|
||||
})
|
||||
->when($this->activeFilter !== 'all', function ($query): void {
|
||||
$query->where('is_active', $this->activeFilter === 'active');
|
||||
|
|
@ -115,7 +105,7 @@ new #[Layout('components.layouts.app'), Title('Benutzer')] class extends Compone
|
|||
};
|
||||
})
|
||||
->orderBy($sort, $this->sortDir)
|
||||
->simplePaginate(50);
|
||||
->paginate(50);
|
||||
|
||||
$this->hydrateCompanyCounts($users);
|
||||
|
||||
|
|
@ -266,9 +256,30 @@ new #[Layout('components.layouts.app'), Title('Benutzer')] class extends Compone
|
|||
->find($this->viewingUserId);
|
||||
}
|
||||
|
||||
private function supportsFullTextSearch(string $term): bool
|
||||
private function applySearch(Builder $query, string $search): void
|
||||
{
|
||||
return mb_strlen($term) >= 3 && in_array(DB::connection()->getDriverName(), ['mysql', 'pgsql'], true);
|
||||
$terms = preg_split('/\s+/', trim($search), -1, PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
if ($terms === false || $terms === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$query->where(function (Builder $searchQuery) use ($terms): void {
|
||||
foreach ($terms as $term) {
|
||||
$pattern = '%'.$this->escapeLikeTerm($term).'%';
|
||||
|
||||
$searchQuery->where(function (Builder $termQuery) use ($pattern): void {
|
||||
$termQuery
|
||||
->whereLike('name', $pattern)
|
||||
->orWhereLike('email', $pattern);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function escapeLikeTerm(string $term): string
|
||||
{
|
||||
return addcslashes($term, '\%_');
|
||||
}
|
||||
|
||||
public function updatedSearch(): void
|
||||
|
|
@ -586,7 +597,7 @@ new #[Layout('components.layouts.app'), Title('Benutzer')] class extends Compone
|
|||
</flux:table>
|
||||
|
||||
<div class="border-t border-[color:var(--color-bg-rule)] p-4">
|
||||
{{ $users->links() }}
|
||||
{{ $users->links('components.portal.pagination') }}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue