presseportale/resources/views/livewire/admin/footer-codes/create.blade.php
Kevin Adametz 40783192d0 security: Rechteausweitung schließen & Admin-Panel feingranular gaten (K1, H1, H2, M8)
- users:manage/roles:manage auf allen User-/Rollen-Masken erzwungen;
  is_super_admin aus $fillable entfernt und nur per autorisiertem
  forceFill setzbar; zentraler Gate::before-Super-Admin-Kurzschluss (K1/M8).
- Nicht-Super-Admins können Super-Admin-Konten nicht mehr bearbeiten (Fix#2);
  Impersonation mit Rangprüfung, keine Token-Erstellung während Impersonation (H1).
- Destruktive/verwaltende Admin-Aktionen (CRM/Billing/Compliance/Struktur)
  hängen an neuen admin-only Permissions; Editor behält reine Content-Rechte (H2).
- Kanonischer Permission-Katalog (App\Authorization\Permission) als Single
  Source of Truth für Seeder und Monitoring.

Deployment: `php artisan db:seed --class=RolesAndPermissionsSeeder` auf allen
Umgebungen ausführen (neue Permissions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 13:48:50 +00:00

200 lines
7.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use App\Enums\Portal;
use App\Models\Category;
use App\Models\FooterCode;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Attributes\Validate;
use Livewire\Volt\Component;
new #[Layout('components.layouts.app'), Title('Footer-Code anlegen')] class extends Component
{
#[Validate('required|string|min:2|max:255')]
public string $title = '';
#[Validate('required|string|min:5')]
public string $content = '';
#[Validate('required|in:'.Portal::Both->value.','.Portal::Presseecho->value.','.Portal::Businessportal24->value)]
public string $portal = Portal::Both->value;
#[Validate('nullable|in:de,en')]
public ?string $language = null;
public bool $isGlobal = false;
public bool $isActive = true;
#[Validate('integer|min:0|max:1000')]
public int $priority = 0;
/** @var array<int, int> */
public array $categoryIds = [];
public function save(): void
{
$this->authorize('footer-codes:manage');
$this->validate();
DB::transaction(function (): void {
$code = FooterCode::create([
'title' => $this->title,
'content' => $this->content,
'portal' => $this->portal,
'language' => $this->language,
'is_global' => $this->isGlobal,
'is_active' => $this->isActive,
'priority' => $this->priority,
]);
if (! $this->isGlobal && ! empty($this->categoryIds)) {
$code->categories()->sync($this->categoryIds);
}
});
session()->flash('success', __('Footer-Code wurde angelegt.'));
$this->redirect(route('admin.footer-codes.index'), navigate: true);
}
public function with(): array
{
return [
'portalOptions' => Portal::cases(),
'categoryOptions' => Category::query()
->with(['translations' => fn ($q) => $q->where('locale', 'de')])
->orderBy('id')
->get()
->map(fn (Category $cat) => [
'id' => $cat->id,
'name' => $cat->localizedName(),
'portal' => $cat->portal->value,
]),
];
}
}; ?>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<x-portal.page-header
:badge="__('Admin Backend')"
:eyebrow="__('Administration · Footer-Codes')"
:title="__('Footer-Code anlegen')"
:description="__('Snippet, das unter Pressemitteilungen ausgespielt wird.')"
>
<x-slot:actions>
<flux:button variant="filled" icon="arrow-left" :href="route('admin.footer-codes.index')" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</x-slot:actions>
</x-portal.page-header>
<form wire:submit="save" class="space-y-6">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Stammdaten') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:input
wire:model="title"
:label="__('Titel')"
placeholder="z. B. {{ __('Standard-Disclaimer DE') }}"
/>
<flux:textarea
wire:model="content"
:label="__('HTML-/Text-Inhalt')"
rows="10"
placeholder="<p>…</p>"
/>
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<flux:select wire:model="portal" :label="__('Portal')">
@foreach ($portalOptions as $option)
<option value="{{ $option->value }}">{{ $option->label() }}</option>
@endforeach
</flux:select>
<flux:select wire:model="language" :label="__('Sprache')">
<option value="">{{ __('Alle') }}</option>
<option value="de">{{ __('Deutsch') }}</option>
<option value="en">{{ __('Englisch') }}</option>
</flux:select>
<flux:input
wire:model="priority"
type="number"
min="0"
max="1000"
:label="__('Priorität')"
:description="__('Niedrigere Werte zuerst.')"
/>
</div>
</div>
</article>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Sichtbarkeit') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:switch
wire:model.live="isGlobal"
:label="__('Global ausspielen')"
:description="__('Wird unter allen Pressemitteilungen angezeigt – Kategorie-Zuordnung wird ignoriert.')"
/>
<flux:switch
wire:model="isActive"
:label="__('Aktiv')"
:description="__('Inaktive Codes werden niemals ausgespielt.')"
/>
</div>
</article>
@if (! $isGlobal)
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Kategorie-Zuordnung') }}</span>
</div>
<div class="p-5 space-y-4">
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">
{{ __('Nur Pressemitteilungen in diesen Kategorien zeigen den Footer-Code.') }}
</p>
<div class="grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3">
@forelse ($categoryOptions as $option)
<label class="flex items-center gap-2 rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] px-3 py-2 hover:border-[color:var(--color-hub)]/30 cursor-pointer">
<input
type="checkbox"
wire:model="categoryIds"
value="{{ $option['id'] }}"
class="rounded border-[color:var(--color-bg-rule)]"
/>
<span class="text-[12.5px] text-[color:var(--color-ink)]">{{ $option['name'] }}</span>
</label>
@empty
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">
{{ __('Keine Kategorien vorhanden.') }}
</p>
@endforelse
</div>
</div>
</article>
@endif
<article class="panel">
<div class="p-5 flex items-center justify-end gap-2">
<flux:button variant="filled" :href="route('admin.footer-codes.index')" wire:navigate>
{{ __('Abbrechen') }}
</flux:button>
<flux:button type="submit" variant="primary" icon="check">
{{ __('Speichern') }}
</flux:button>
</div>
</article>
</form>
</div>