presseportale/resources/views/components/layouts/app/sidebar.blade.php
Kevin Adametz b5b70416f2 archive: Beweisarchiv — verschlüsselte Einträge, Legal Hold, Purge mit Tombstone, Restore-Abgleich, Auslöser und Admin-Seite (Bildkennzeichnung Block F)
- archive_entries (append-only) + archive_access_log; Disk archive außerhalb des Web-Roots; eigener ARCHIVE_KEY (archive:key), AES-256-CBC, SHA-256 am Eintrag
- EvidenceArchive: record/open/manifest/export (signiertes ZIP), setLegalHold/releaseLegalHold (Frist läuft neu), purgeDue, exportTombstones, reconcileAfterRestore
- PressReleaseArchiver: image_removed (Original, Snapshot, Bestätigungen), pm_depublished (letzter veröffentlichter Stand), gdpr_change (Anonymisierung, Korrekturen), label_corrected
- Hooks in Remover, depublish/archive/changeStatusFromAdmin/deleteFromAdmin, AnonymizationService, CorrectionService, ImageCorrectionService
- Scheduler archive:purge 03:20 und archive:export-tombstones 03:30; archive:reconcile-after-restore im Runbook
- Admin ▸ Management ▸ Beweisarchiv (Liste, Legal Hold mit Pflichtvermerk, Öffnen/Export nur mit Zweck)
- Scope-Wächter: neue Befehle nutzen withoutGlobalScope(PortalScope::class)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 10:04:21 +00:00

453 lines
24 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.

<!DOCTYPE html>
{{--
Hub × FluxUI Phase 5 — Portal-Shell im Hub-Design.
Erscheinung (Light/Dark) wird über FluxUI Appearance-Switcher
gesteuert. Server liest das `flux_appearance`-Cookie (gesetzt vom
JS-Bridge in partials/head.blade.php) und rendert class="dark"
direkt im <html>, damit es bei wire:navigate kein Theme-Flash gibt.
Bei fehlendem Cookie (Erstbesuch) wird Light gerendert und das JS
schaltet bei dunkler Präferenz nach Page-Load nach — der einmalige
Flash beim allerersten Aufruf ist akzeptiert.
--}}
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" @class(['dark' => request()->cookie('flux_appearance') === 'dark'])>
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-bg text-ink antialiased">
<flux:sidebar sticky stashable breakpoint="1280px" class="border-e border-bg-rule">
<flux:sidebar.toggle class="xl:hidden" icon="x-mark" />
{{-- Brand-Block: Wortmarke + Hub-Eyebrow --}}
<a href="{{ config('domains.domain_main_url') }}" class="block px-2 pt-1 pb-3 no-underline">
<span class="text-[19px] font-bold tracking-[-0.4px] leading-none">
<x-web.brand-mark brand="pressekonto" :serif="false" />
</span>
<div class="mt-1.5 text-[10px] font-semibold tracking-[0.18em] uppercase text-ink-3">
Publisher · Hub
</div>
</a>
@php
$user = auth()->user();
$impersonation = app(\App\Actions\Admin\UserImpersonation::class);
$impersonator = $impersonation->impersonator();
$isImpersonating = $impersonation->isActive();
$canAdmin = ($user?->canAccessAdmin() ?? false) && !$isImpersonating;
$canCustomer = $user?->canAccessCustomer() ?? false;
$reviewCount = $canAdmin
? app(\App\Services\Admin\AdminPerformanceCache::class)->pressReleaseReviewCount()
: 0;
$legalOpenCount = $canAdmin ? \App\Models\LegalRequest::query()->open()->count() : 0;
$contactOpenCount = $canAdmin ? \App\Models\ContactMessage::query()->open()->count() : 0;
@endphp
<flux:navlist variant="outline">
{{-- Dashboard (Admin/Editor) --}}
@if ($canAdmin)
<flux:navlist.item icon="chart-bar" :href="route('dashboard')"
:current="request()->routeIs('dashboard')" wire:navigate class="mb-4">
{{ __('Dashboard') }}
</flux:navlist.item>
@endif
{{-- Mein Bereich – sichtbar für alle Panel-User --}}
@if ($canCustomer)
<flux:navlist.group :heading="__('Mein Bereich')" class="grid mb-4">
<flux:navlist.item icon="home" :href="route('me.dashboard')"
:current="request()->routeIs('me.dashboard')" wire:navigate>
{{ __('Übersicht') }}
</flux:navlist.item>
<flux:navlist.item icon="newspaper" :href="route('me.press-releases.index')"
:current="request()->routeIs('me.press-releases.*')" wire:navigate>
{{ __('Pressemitteilungen') }}
</flux:navlist.item>
<flux:navlist.item icon="building-office" :href="route('me.press-kits.index')"
:current="request()->routeIs('me.press-kits.*')" wire:navigate>
{{ __('Firmen') }}
</flux:navlist.item>
</flux:navlist.group>
<flux:navlist.group :heading="__('Finanzen')" class="grid mb-4">
<flux:navlist.item icon="shopping-bag" :href="route('me.bookings.index')"
:current="request()->routeIs('me.bookings.*')" wire:navigate>
{{ __('Buchungen & Add-ons') }}
</flux:navlist.item>
<flux:navlist.item icon="document-text" :href="route('me.invoices.index')"
:current="request()->routeIs('me.invoices.*')" wire:navigate>
{{ __('Rechnungen') }}
</flux:navlist.item>
</flux:navlist.group>
<flux:navlist.group :heading="__('Konto')" class="grid mb-4">
<flux:navlist.item icon="user" :href="route('me.profile')"
:current="request()->routeIs('me.profile')" wire:navigate>
{{ __('Profil') }}
</flux:navlist.item>
<flux:navlist.item icon="shield-check" :href="route('me.security')"
:current="request()->routeIs('me.security')" wire:navigate>
{{ __('Sicherheit') }}
</flux:navlist.item>
<flux:navlist.item icon="key" :href="route('me.tokens.index')"
:current="request()->routeIs('me.tokens.*')" wire:navigate>
{{ __('API & Integrationen') }}
</flux:navlist.item>
</flux:navlist.group>
<flux:navlist.group :heading="__('Portale')" class="grid mb-4">
<flux:navlist.item icon="arrow-top-right-on-square" :href="config('domains.domain_presseecho_url')"
target="_blank">
{{ __('presseecho') }}
</flux:navlist.item>
<flux:navlist.item icon="arrow-top-right-on-square"
:href="config('domains.domain_businessportal_url')" target="_blank">
{{ __('businessportal24') }}
</flux:navlist.item>
</flux:navlist.group>
@endif
{{-- Content Management (Admin/Editor) --}}
@if ($canAdmin)
<flux:navlist.group :heading="__('Administration')" class="grid mb-4">
<flux:navlist.item icon="newspaper"
:href="route('admin.press-releases.index', $reviewCount > 0 ? ['status' => 'review'] : [])"
:current="request()->routeIs('admin.press-releases.*')"
:badge="$reviewCount > 0 ? $reviewCount : null" badge-color="yellow" wire:navigate>
{{ __('Pressemitteilungen') }}
</flux:navlist.item>
<flux:navlist.item icon="building-office" :href="route('admin.companies.index')"
:current="request()->routeIs('admin.companies.*')" wire:navigate>
{{ __('Firmen') }}
</flux:navlist.item>
<flux:navlist.item icon="user-group" :href="route('admin.contacts.index')"
:current="request()->routeIs('admin.contacts.*')" wire:navigate>
{{ __('Kontakte') }}
</flux:navlist.item>
@can('users:manage')
<flux:navlist.item icon="users" :href="route('admin.users.index')"
:current="request()->routeIs('admin.users.*')" wire:navigate>
{{ __('Benutzer') }}
</flux:navlist.item>
<flux:navlist.item icon="paper-airplane" :href="route('admin.newsletter.campaigns.index')"
:current="request()->routeIs('admin.newsletter.campaigns.*')" wire:navigate>
{{ __('Newsletter') }}
</flux:navlist.item>
@endcan
</flux:navlist.group>
<flux:navlist.group :heading="__('Management')" class="grid mb-4">
@can('compliance:manage')
<flux:navlist.item icon="queue-list" :href="route('admin.reports.activity')"
:current="request()->routeIs('admin.reports.activity')" wire:navigate>
{{ __('Verlauf') }}
</flux:navlist.item>
<flux:navlist.item icon="archive-box" :href="route('admin.archive.index')"
:current="request()->routeIs('admin.archive.*')" wire:navigate>
{{ __('Beweisarchiv') }}
</flux:navlist.item>
{{-- Kontakt: Anliegen aus dem Kontakt-Widget (Widget + Hub-Formular) --}}
<flux:navlist.item icon="chat-bubble-left-right" :href="route('admin.contact-messages.index')"
:current="request()->routeIs('admin.contact-messages.*')"
:badge="$contactOpenCount > 0 ? $contactOpenCount : null" badge-color="red" wire:navigate>
{{ __('Anliegen') }}
</flux:navlist.item>
{{-- Recht & Compliance --}}
<flux:navlist.item icon="scale" :href="route('admin.legal-requests.index')"
:current="request()->routeIs('admin.legal-requests.*')"
:badge="$legalOpenCount > 0 ? $legalOpenCount : null" badge-color="red" wire:navigate>
{{ __('Anfragen') }}
</flux:navlist.item>
@endcan
</flux:navlist.group>
{{-- Billing --}}
@can('billing:manage')
<flux:navlist.group :heading="__('Billing')" class="grid mb-4">
<flux:navlist.item icon="archive-box" :href="route('admin.invoices.index')"
:current="request()->routeIs('admin.invoices.*')" wire:navigate>
{{ __('Legacy Rechnungen') }}
</flux:navlist.item>
<flux:navlist.item icon="credit-card" :href="route('admin.payments.index')"
:current="request()->routeIs('admin.payments.index')" wire:navigate>
{{ __('Zahlungen') }}
</flux:navlist.item>
<flux:navlist.item icon="rectangle-stack" :href="route('admin.payments.plans')"
:current="request()->routeIs('admin.payments.plans')" wire:navigate>
{{ __('Tarife & Pakete') }}
</flux:navlist.item>
<flux:navlist.item icon="ticket" :href="route('admin.coupons.index')"
:current="request()->routeIs('admin.coupons.*')" wire:navigate>
{{ __('Gutscheine') }}
</flux:navlist.item>
<flux:navlist.item icon="envelope" :href="route('admin.newsletter.sync')"
:current="request()->routeIs('admin.newsletter.sync')" wire:navigate>
{{ __('Newsletter Sync') }}
</flux:navlist.item>
@can('users:manage')
<flux:navlist.item icon="no-symbol" :href="route('admin.mail.suppressions')"
:current="request()->routeIs('admin.mail.suppressions')" wire:navigate>
{{ __('Mail-Sperrliste') }}
</flux:navlist.item>
@endcan
</flux:navlist.group>
@endcan
{{-- Content --}}
<flux:navlist.group :heading="__('Content')" class="grid mb-4">
@can('categories:manage')
<flux:navlist.item icon="folder" :href="route('admin.categories.index')"
:current="request()->routeIs('admin.categories.*')" wire:navigate>
{{ __('Kategorien') }}
</flux:navlist.item>
@endcan
@can('footer-codes:manage')
<flux:navlist.item icon="code-bracket-square" :href="route('admin.footer-codes.index')"
:current="request()->routeIs('admin.footer-codes.*')" wire:navigate>
{{ __('Footer-Codes') }}
</flux:navlist.item>
@endcan
</flux:navlist.group>
{{-- Administration --}}
<flux:navlist.group :heading="__('Settings')" class="grid mb-4">
<flux:navlist.item icon="cog" :href="route('admin.presets.index')"
:current="request()->routeIs('admin.presets.*')" wire:navigate>
{{ __('Voreinstellungen') }}
</flux:navlist.item>
@can('roles:manage')
<flux:navlist.item icon="shield-check" :href="route('admin.roles.index')"
:current="request()->routeIs('admin.roles.*')" wire:navigate>
{{ __('Rollen & Rechte') }}
</flux:navlist.item>
@endcan
</flux:navlist.group>
{{-- Reports --}}
<flux:navlist.group :heading="__('Reports')" class="grid mb-4">
<flux:navlist.item icon="chart-bar-square" :href="route('admin.reports.slow-requests')"
:current="request()->routeIs('admin.reports.slow-requests')" wire:navigate>
{{ __('Performance') }}
</flux:navlist.item>
<flux:navlist.item icon="inbox-stack" :href="route('admin.reports.submitter-contacts')"
:current="request()->routeIs('admin.reports.submitter-contacts')" wire:navigate>
{{ __('Einlieferer-Bericht') }}
</flux:navlist.item>
<flux:navlist.item icon="magnifying-glass-circle" :href="route('admin.reports.index-status')"
:current="request()->routeIs('admin.reports.index-status')" wire:navigate>
{{ __('Indexierung & Abrufe') }}
</flux:navlist.item>
@can('settings:manage')
<flux:navlist.item icon="heart" :href="route('admin.reports.system-health')"
:current="request()->routeIs('admin.reports.system-health')" wire:navigate>
{{ __('Betriebsüberwachung') }}
</flux:navlist.item>
@endcan
</flux:navlist.group>
@endif
</flux:navlist>
{{-- Portal-Filter für Admin-Benutzer (P2.6) --}}
@auth
@if ($canAdmin)
<div class="border-t border-zinc-200 dark:border-zinc-700 mt-2 pt-2">
<livewire:admin.portal-switcher />
</div>
@endif
@endauth
@if ($impersonator)
{{-- Testmodus-Block im Hub-Stil (statt Amber-Warnfarbe).
Dunkles Hub-Blau-Panel mit Bernstein-Eyebrow, klare
CTA „Zurück zum Admin" als helle Pille. --}}
<div class="mt-3 relative overflow-hidden rounded-[5px] bg-hub p-4 text-ink-on-dark">
<div class="absolute -top-6 -right-6 w-16 h-16 rounded-full bg-hub-3 opacity-50"></div>
<div class="absolute -bottom-8 -left-8 w-20 h-20 rounded-full bg-hub-3 opacity-30"></div>
<div class="relative">
<div class="flex items-center gap-2 mb-2">
<span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse"></span>
<span class="text-[10.5px] font-bold tracking-[0.20em] uppercase text-accent-soft">
{{ __('Testmodus aktiv') }}
</span>
</div>
<p class="text-[12px] leading-[1.5] text-ink-on-dark-2 m-0">
{{ __('Angemeldet als') }}
<strong class="text-white font-semibold">{{ $user?->name }}</strong>.<br />
{{ __('Admin:') }}
<strong class="text-white font-semibold">{{ $impersonator->name }}</strong>
</p>
<form method="POST" action="{{ route('admin.impersonate.leave') }}" class="mt-3">
@csrf
<button type="submit"
class="w-full px-3 py-2 bg-white text-hub text-[12px] font-semibold rounded-[3px] hover:bg-bg transition-colors flex items-center justify-center gap-1.5">
<svg width="11" height="11" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<path d="M9 3L3 9M3 9H8M3 9V4" stroke="currentColor" stroke-width="1.5"
stroke-linecap="round" stroke-linejoin="round" />
</svg>
{{ __('Zurück zum Admin') }}
</button>
</form>
</div>
</div>
@endif
<flux:spacer />
<!-- Desktop User Menu -->
<flux:dropdown position="bottom" align="start">
<flux:profile :name="auth()->user()->name" :initials="auth()->user()->initials()"
icon-trailing="chevrons-up-down" />
<flux:menu class="w-[220px]">
<flux:menu.radio.group>
<div class="p-0 text-sm font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
<span class="relative flex h-8 w-8 shrink-0 overflow-hidden rounded-lg">
<span
class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white">
{{ auth()->user()->initials() }}
</span>
</span>
<div class="grid flex-1 text-start text-sm leading-tight">
<span class="truncate font-semibold">{{ auth()->user()->name }}</span>
<span class="truncate text-xs">{{ auth()->user()->email }}</span>
</div>
</div>
</div>
</flux:menu.radio.group>
<flux:menu.separator />
<flux:menu.radio.group>
<flux:menu.item :href="route('me.profile')" icon="user" wire:navigate>
{{ __('Profil') }}</flux:menu.item>
</flux:menu.radio.group>
<flux:menu.separator />
{{-- Phase 5: Appearance-Switcher direkt im User-Menü.
`$flux.appearance` ist FluxUIs Magic-Object, persistent
über LocalStorage. Werte: 'light' | 'dark' | 'system'. --}}
<div class="px-3 py-2">
<div
class="mb-1.5 text-[10px] font-semibold tracking-[0.16em] uppercase text-[color:var(--color-ink-3)]">
{{ __('Erscheinung') }}
</div>
<flux:radio.group x-data variant="segmented" size="sm" x-model="$flux.appearance"
class="w-full">
<flux:radio value="light" icon="sun" :title="__('Hell')" />
<flux:radio value="dark" icon="moon" :title="__('Dunkel')" />
<flux:radio value="system" icon="computer-desktop" :title="__('System')" />
</flux:radio.group>
</div>
<flux:menu.separator />
<form method="POST" action="{{ route('logout') }}" class="w-full">
@csrf
<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle"
class="w-full">
{{ __('Abmelden') }}
</flux:menu.item>
</form>
</flux:menu>
</flux:dropdown>
</flux:sidebar>
{{-- Desktop-Topbar: immer verfügbarer Status (Buchung, freie PMs, Credits)
oben rechts. Nur für Panel-User mit Kundenbereich. --}}
@if ($canCustomer)
<flux:header class="hidden xl:flex !min-h-0 !pt-3 !pb-0">
<flux:spacer />
<x-customer.status-menu :user="$user" />
</flux:header>
@endif
<!-- Mobile User Menu -->
<flux:header class="xl:hidden">
<flux:sidebar.toggle class="xl:hidden" icon="bars-2" inset="left" />
<a href="{{ config('domains.domain_main_url') }}" class="me-5 ml-2 flex items-baseline no-underline">
<span class="text-[16px] font-bold tracking-[-0.3px] leading-none">
<x-web.brand-mark brand="pressekonto" :serif="false" />
</span>
</a>
<flux:spacer />
@if ($canCustomer)
<x-customer.status-menu :user="$user" />
@endif
<flux:dropdown position="top" align="end">
<flux:profile :initials="auth()->user()->initials()" icon-trailing="chevron-down" />
<flux:menu>
<flux:menu.radio.group>
<div class="p-0 text-sm font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
<span class="relative flex h-8 w-8 shrink-0 overflow-hidden rounded-lg">
<span
class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white">
{{ auth()->user()->initials() }}
</span>
</span>
<div class="grid flex-1 text-start text-sm leading-tight">
<span class="truncate font-semibold">{{ auth()->user()->name }}</span>
<span class="truncate text-xs">{{ auth()->user()->email }}</span>
</div>
</div>
</div>
</flux:menu.radio.group>
<flux:menu.separator />
<flux:menu.radio.group>
<flux:menu.item :href="route('me.profile')" icon="user" wire:navigate>
{{ __('Profil') }}</flux:menu.item>
</flux:menu.radio.group>
<flux:menu.separator />
{{-- Phase 5: Appearance-Switcher (Mobile-Dropdown). --}}
<div class="px-3 py-2">
<div
class="mb-1.5 text-[10px] font-semibold tracking-[0.16em] uppercase text-[color:var(--color-ink-3)]">
{{ __('Erscheinung') }}
</div>
<flux:radio.group x-data variant="segmented" size="sm" x-model="$flux.appearance"
class="w-full">
<flux:radio value="light" icon="sun" :title="__('Hell')" />
<flux:radio value="dark" icon="moon" :title="__('Dunkel')" />
<flux:radio value="system" icon="computer-desktop" :title="__('System')" />
</flux:radio.group>
</div>
<flux:menu.separator />
<form method="POST" action="{{ route('logout') }}" class="w-full">
@csrf
<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle"
class="w-full">
{{ __('Abmelden') }}
</flux:menu.item>
</form>
</flux:menu>
</flux:dropdown>
</flux:header>
{{ $slot }}
{{-- Schwebendes Kontakt-Widget (unten rechts, eingeloggter Bereich) --}}
<x-web.contact-launcher />
@persist('toast')
<flux:toast position="top end" class="pt-24" />
@endpersist
@vite(['resources/js/portal-form-hooks.js'], 'build/portal')
@fluxScripts
</body>
</html>