Rebrand Hub+Flux
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run

This commit is contained in:
Kevin Adametz 2026-05-20 15:44:15 +02:00
parent 0a3e52d603
commit 9b47296cea
130 changed files with 9357 additions and 3345 deletions

View file

@ -141,6 +141,32 @@
color: var(--color-hub);
}
/* Dark Mode: --color-bg ist DUNKLER als die Sidebar (--color-bg-elev),
ein Hover damit würde das Item eindrücken" statt hervorheben. Im Dark
Mode nutzen wir deshalb das dezente Hub-Soft (`#1f2a47`) selbe
Farbfamilie wie der Active-State, nur ohne Active-Strip. */
.dark [data-flux-navlist-item]:hover {
background: var(--color-hub-soft);
color: var(--color-hub);
}
/* Klick/Focus/Tap-Highlight: konsistent mit Hub-Soft (statt browser-
default weißem Tap-Flash oder Flux's `bg-zinc-800/5`). Verhindert das
weiße Aufblitzen beim wire:navigate-Klick im Dark Mode. */
[data-flux-navlist-item]:active,
[data-flux-navlist-item]:focus {
background: var(--color-hub-soft);
color: var(--color-hub);
outline: none;
}
[data-flux-navlist-item] {
-webkit-tap-highlight-color: transparent;
}
[data-flux-navlist-item]:focus-visible {
outline: 2px solid var(--color-hub);
outline-offset: -2px;
}
[data-flux-navlist-item][data-current="true"],
[data-flux-navlist-item][aria-current="page"],
[data-flux-navlist-item].active {

View file

@ -245,4 +245,9 @@
/* color-scheme-Hint für native Form-Controls (Scrollbars, Inputs) */
color-scheme: dark;
/* Brand-Mark Wortmarke (z.B. presse" bei pressekonto) im Portal-Dark
auf weiß. Wird nur von `<x-web.brand-mark variant="auto">` ausgelesen;
Hub-Frontend ist Light-Only, dort bleibt der Inline-Fallback aktiv. */
--brand-mark-name-color: #ffffff;
}

View file

@ -244,6 +244,10 @@
background: var(--color-err-soft);
color: var(--color-loss);
}
.badge.muted {
background: var(--color-bg-rule-2);
color: var(--color-ink-3);
}
.badge.dot::before {
content: "";
width: 6px;
@ -337,4 +341,359 @@
.eyebrow.on-dark {
color: var(--color-hub-line);
}
/* ============================================================
* Counter-Strip (Inline-Zähler unter H1)
* ============================================================
* Beispiel:
* 24 Mitteilungen · 18 veröffentlicht · 1 in Prüfung ·
*/
.counter-strip {
display: inline-flex;
align-items: center;
gap: 14px;
padding: 6px 0;
flex-wrap: wrap;
}
.counter-strip .seg {
display: inline-flex;
align-items: baseline;
gap: 6px;
font-size: 12.5px;
color: var(--color-ink-2);
}
.counter-strip .seg b {
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
font-size: 13.5px;
font-weight: 600;
color: var(--color-ink);
letter-spacing: -0.2px;
}
.counter-strip .seg.is-ok b {
color: var(--color-gain-deep);
}
.counter-strip .seg.is-warn b {
color: var(--color-accent-deep);
}
.counter-strip .seg.is-err b {
color: var(--color-loss);
}
.counter-strip .seg.is-muted b {
color: var(--color-ink-3);
}
.counter-strip .sep {
width: 1px;
height: 11px;
background: var(--color-bg-rule);
}
/* ============================================================
* View-Tabs (Saved-Views Tab-Leiste über Filter)
* ============================================================ */
.view-tabs {
display: flex;
align-items: center;
gap: 4px;
border-bottom: 1px solid var(--color-bg-rule);
}
.view-tab {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 7px 12px 9px;
font-size: 12.5px;
font-weight: 500;
color: var(--color-ink-3);
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition:
color 0.12s,
border-color 0.12s;
cursor: pointer;
background: transparent;
border-top: 0;
border-left: 0;
border-right: 0;
}
.view-tab:hover {
color: var(--color-hub);
}
.view-tab.is-active {
color: var(--color-hub);
font-weight: 600;
border-bottom-color: var(--color-hub);
}
.view-tab .cnt {
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--color-ink-3);
background: var(--color-bg-rule-2);
padding: 1px 6px;
border-radius: 999px;
font-weight: 600;
letter-spacing: 0.04em;
}
.view-tab.is-active .cnt {
background: var(--color-hub);
color: var(--color-ink-on-dark);
}
/* ============================================================
* Filter-Chips (Dropdown-Buttons mit Caret)
* ============================================================ */
.filter-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px 6px 14px;
background: var(--color-bg-card);
border: 1px solid var(--color-hub-soft-2);
border-radius: 4px;
font-size: 12.5px;
color: var(--color-hub);
font-weight: 500;
transition:
border-color 0.15s,
background 0.15s;
white-space: nowrap;
cursor: pointer;
}
.filter-chip:hover {
border-color: var(--color-hub);
background: var(--color-bg-elev);
}
.filter-chip.is-active {
background: var(--color-hub);
color: var(--color-ink-on-dark);
border-color: var(--color-hub);
}
.filter-chip.is-active:hover {
background: var(--color-hub-2);
}
.filter-chip .caret {
opacity: 0.55;
}
/* ============================================================
* Active-Chips (entfernbare Filter-Anzeige)
* ============================================================ */
.active-chip {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 4px 6px 4px 11px;
background: var(--color-bg-elev);
border: 1px solid var(--color-bg-rule);
border-radius: 999px;
font-size: 11.5px;
color: var(--color-ink-2);
font-weight: 500;
}
.active-chip strong {
color: var(--color-hub);
font-weight: 600;
}
.active-chip .x {
width: 16px;
height: 16px;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--color-ink-3);
background: var(--color-bg-rule-2);
transition:
background 0.12s,
color 0.12s;
border: 0;
cursor: pointer;
}
.active-chip .x:hover {
background: var(--color-hub);
color: var(--color-ink-on-dark);
}
/* ============================================================
* Portal-Pills (presseecho / businessportal24 Indikator)
* ============================================================ */
.portal-pill {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 8px;
border-radius: 999px;
font-size: 10.5px;
font-weight: 600;
letter-spacing: 0.06em;
background: var(--color-bg-elev);
border: 1px solid var(--color-bg-rule);
color: var(--color-ink-2);
white-space: nowrap;
}
.portal-pill .pdot {
width: 6px;
height: 6px;
border-radius: 999px;
}
.portal-pill.pe .pdot {
background: var(--color-bridge-presseecho);
}
.portal-pill.bp .pdot {
background: var(--color-bridge-businessportal);
}
/* ============================================================
* Inline-Action (kleine Aktion direkt neben Status-Badge)
* ============================================================ */
.inline-action {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
font-weight: 600;
color: var(--color-hub);
background: transparent;
padding: 3px 6px;
margin-left: 4px;
border-radius: 3px;
border: 1px dashed transparent;
transition:
border-color 0.12s,
background 0.12s;
cursor: pointer;
}
.inline-action:hover {
background: var(--color-hub-soft);
border-color: var(--color-hub-soft-2);
}
.inline-action.warn {
color: var(--color-accent-deep);
}
.inline-action.warn:hover {
background: var(--color-warn-soft);
border-color: color-mix(
in oklab,
var(--color-warn),
transparent 60%
);
}
.inline-action.err {
color: var(--color-loss);
}
.inline-action.err:hover {
background: var(--color-err-soft);
border-color: color-mix(
in oklab,
var(--color-err),
transparent 60%
);
}
/* ============================================================
* Row-Tinting (für Status-Hover-Hervorhebung in Listen)
* ============================================================ */
.is-row-warn:hover,
tr.is-row-warn:hover td {
background: color-mix(
in oklab,
var(--color-warn-soft),
var(--color-bg-card) 50%
) !important;
}
.is-row-err:hover,
tr.is-row-err:hover td {
background: color-mix(
in oklab,
var(--color-err-soft),
var(--color-bg-card) 50%
) !important;
}
/* ============================================================
* Empty-Stage (große Empty-States in Panels/Tabellen)
* ============================================================ */
.empty-stage {
padding: 64px 24px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.empty-stage .empty-ico {
width: 64px;
height: 64px;
border-radius: 6px;
background: var(--color-hub-soft);
border: 1px solid var(--color-hub-soft-2);
color: var(--color-hub);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
}
.empty-stage .empty-ico.warm {
background: var(--color-accent-soft);
border-color: color-mix(
in oklab,
var(--color-accent-warm),
transparent 50%
);
color: var(--color-accent-deep);
}
.empty-stage .empty-ico.err {
background: var(--color-err-soft);
border-color: color-mix(
in oklab,
var(--color-err),
transparent 50%
);
color: var(--color-loss);
}
.empty-stage .empty-title {
font-size: 17px;
font-weight: 600;
color: var(--color-ink);
margin: 0;
letter-spacing: -0.2px;
}
.empty-stage .empty-sub {
font-size: 13px;
color: var(--color-ink-3);
line-height: 1.55;
margin: 8px 0 0;
max-width: 440px;
}
}
/* ============================================================
* FluxUI-Tabellen im Hub-Panel-Kontext
* ============================================================
* FluxUI setzt per Default `first:ps-0 last:pe-0` auf alle
* Tabellen-Zellen/-Spalten die Tabelle klebt" am Rand des
* Containers. Im Hub-Stil wollen wir konsistente 18px
* Edge-Padding (wie im Mockup `table.list`).
*
* MUSS in `@layer utilities` stehen (gleicher Layer wie
* FluxUI's `first:ps-0`-Utility), sonst gewinnt FluxUI durch
* CSS-Cascade-Layer-Ordering, unabhängig von Spezifität.
*
* Greift überall, wo eine FluxUI-Tabelle innerhalb eines
* `.panel` oder `.panel-warm` sitzt. Tabellen in Modals,
* Dropdowns oder anderen Kontexten bleiben unangetastet.
*/
@layer utilities {
.panel [data-flux-table] [data-flux-column]:first-child,
.panel [data-flux-table] [data-flux-cell]:first-child,
.panel-warm [data-flux-table] [data-flux-column]:first-child,
.panel-warm [data-flux-table] [data-flux-cell]:first-child {
padding-inline-start: 18px;
}
.panel [data-flux-table] [data-flux-column]:last-child,
.panel [data-flux-table] [data-flux-cell]:last-child,
.panel-warm [data-flux-table] [data-flux-column]:last-child,
.panel-warm [data-flux-table] [data-flux-cell]:last-child {
padding-inline-end: 18px;
}
}

View file

@ -79,11 +79,25 @@
</div>
@forelse ($recentPRs as $pr)
@php
$badgeClass = match ($pr->status->value) {
'published' => 'ok',
'review' => 'warn',
'rejected' => 'err',
'archived', 'draft' => 'muted',
default => 'hub',
};
$portal = $pr->portal?->value ?? 'both';
$showPe = in_array($portal, ['presseecho', 'both'], true);
$showBp = in_array($portal, ['businessportal24', 'both'], true);
@endphp
<a href="{{ route('admin.press-releases.show', $pr->id) }}" wire:navigate
class="flex items-center justify-between gap-3 px-5 py-3 border-b border-[color:var(--color-bg-rule)] last:border-b-0 hover:bg-[color:var(--color-bg)] transition-colors">
class="flex items-center justify-between gap-4 px-5 py-3 border-b border-[color:var(--color-bg-rule)] last:border-b-0 hover:bg-[color:var(--color-bg)] transition-colors">
<div class="min-w-0 flex-1">
<p class="truncate text-[13px] font-medium text-[color:var(--color-ink)] m-0">{{ $pr->title }}</p>
<p class="text-[11px] text-[color:var(--color-ink-3)] mt-0.5 m-0 truncate">
PM-{{ $pr->id }}
<span class="text-[color:var(--color-ink-4)] mx-1">·</span>
{{ $pr->company?->name ?? '' }}
<span class="text-[color:var(--color-ink-4)] mx-1">·</span>
{{ $pr->user?->name ?? '' }}
@ -91,15 +105,15 @@
{{ $pr->created_at->format('d.m.Y') }}
</p>
</div>
<span @class([
'badge shrink-0',
'ok' => $pr->status->value === 'published',
'warn' => $pr->status->value === 'review',
'err' => $pr->status->value === 'rejected',
'hub' => in_array($pr->status->value, ['archived', 'draft'], true),
])>
{{ $pr->status->label() }}
</span>
<div class="flex items-center gap-1.5 shrink-0">
@if ($showPe)
<span class="portal-pill pe"><span class="pdot"></span>presseecho</span>
@endif
@if ($showBp)
<span class="portal-pill bp"><span class="pdot"></span>businessportal24</span>
@endif
<span class="badge {{ $badgeClass }} dot">{{ $pr->status->label() }}</span>
</div>
</a>
@empty
<p class="px-5 py-8 text-center text-[12.5px] text-[color:var(--color-ink-3)]">
@ -122,17 +136,44 @@
</div>
@forelse ($pendingReviews as $pr)
<a href="{{ route('admin.press-releases.show', $pr->id) }}" wire:navigate
class="block px-5 py-3 border-b border-[color:var(--color-bg-rule)] last:border-b-0 hover:bg-[color:var(--color-bg)] transition-colors">
<p class="truncate text-[13px] font-medium text-[color:var(--color-ink)] m-0">{{ $pr->title }}</p>
<p class="text-[11px] text-[color:var(--color-ink-3)] mt-0.5 m-0 truncate">
{{ $pr->company?->name ?? '' }}
<span class="text-[color:var(--color-ink-4)] mx-1">·</span>
{{ $pr->portal->label() }}
<span class="text-[color:var(--color-ink-4)] mx-1">·</span>
{{ $pr->created_at->format('d.m.Y') }}
</p>
</a>
@php
$portal = $pr->portal?->value ?? 'both';
$showPe = in_array($portal, ['presseecho', 'both'], true);
$showBp = in_array($portal, ['businessportal24', 'both'], true);
@endphp
<div
class="is-row-warn px-5 py-3 border-b border-[color:var(--color-bg-rule)] last:border-b-0 transition-colors">
<div class="flex items-start justify-between gap-3">
<a href="{{ route('admin.press-releases.show', $pr->id) }}" wire:navigate
class="block min-w-0 flex-1 group">
<p
class="truncate text-[13px] font-medium text-[color:var(--color-ink)] m-0 group-hover:text-[color:var(--color-hub)] group-hover:underline underline-offset-[3px] decoration-[color:var(--color-hub)]/40">
{{ $pr->title }}
</p>
</a>
<a href="{{ route('admin.press-releases.show', $pr->id) }}" wire:navigate
class="inline-action shrink-0" title="{{ __('Pressemitteilung prüfen') }}">
{{ __('Prüfen →') }}
</a>
</div>
<div class="flex items-center justify-between gap-3 mt-1.5">
<p class="text-[11px] text-[color:var(--color-ink-3)] m-0 truncate min-w-0">
PM-{{ $pr->id }}
<span class="text-[color:var(--color-ink-4)] mx-1">·</span>
{{ $pr->company?->name ?? '' }}
<span class="text-[color:var(--color-ink-4)] mx-1">·</span>
{{ $pr->created_at->format('d.m.Y') }}
</p>
<div class="flex items-center gap-1.5 shrink-0">
@if ($showPe)
<span class="portal-pill pe"><span class="pdot"></span>presseecho</span>
@endif
@if ($showBp)
<span class="portal-pill bp"><span class="pdot"></span>businessportal24</span>
@endif
</div>
</div>
</div>
@empty
<p class="px-5 py-8 text-center text-[12.5px] text-[color:var(--color-ink-3)]">
{{ __('Keine PMs in der Prüfwarteschlange.') }}

View file

@ -1,124 +0,0 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-white dark:bg-zinc-800">
<flux:header container class="border-b border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
<flux:sidebar.toggle class="lg:hidden" icon="bars-2" inset="left" />
<a href="{{ route('dashboard') }}" class="ms-2 me-5 flex items-center space-x-2 rtl:space-x-reverse lg:ms-0" wire:navigate>
<x-app-logo />
</a>
<flux:navbar class="-mb-px max-lg:hidden">
<flux:navbar.item icon="layout-grid" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>
{{ __('Dashboard') }}
</flux:navbar.item>
</flux:navbar>
<flux:spacer />
<flux:navbar class="me-1.5 space-x-0.5 rtl:space-x-reverse py-0!">
<flux:tooltip :content="__('Search')" position="bottom">
<flux:navbar.item class="!h-10 [&>div>svg]:size-5" icon="magnifying-glass" href="#" :label="__('Search')" />
</flux:tooltip>
<flux:tooltip :content="__('Repository')" position="bottom">
<flux:navbar.item
class="h-10 max-lg:hidden [&>div>svg]:size-5"
icon="folder-git-2"
href="https://github.com/laravel/livewire-starter-kit"
target="_blank"
:label="__('Repository')"
/>
</flux:tooltip>
<flux:tooltip :content="__('Documentation')" position="bottom">
<flux:navbar.item
class="h-10 max-lg:hidden [&>div>svg]:size-5"
icon="book-open-text"
href="https://laravel.com/docs/starter-kits#livewire"
target="_blank"
label="Documentation"
/>
</flux:tooltip>
</flux:navbar>
<!-- Desktop User Menu -->
<flux:dropdown position="top" align="end">
<flux:profile
class="cursor-pointer"
:initials="auth()->user()->initials()"
/>
<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('settings.profile')" icon="cog" wire:navigate>{{ __('Settings') }}</flux:menu.item>
</flux:menu.radio.group>
<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">
{{ __('Log Out') }}
</flux:menu.item>
</form>
</flux:menu>
</flux:dropdown>
</flux:header>
<!-- Mobile Menu -->
<flux:sidebar stashable sticky class="lg:hidden border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
<flux:sidebar.toggle class="lg:hidden" icon="x-mark" />
<a href="{{ route('dashboard') }}" class="ms-1 flex items-center space-x-2 rtl:space-x-reverse" wire:navigate>
<x-app-logo />
</a>
<flux:navlist variant="outline">
<flux:navlist.group :heading="__('Platform')">
<flux:navlist.item icon="layout-grid" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>
{{ __('Dashboard') }}
</flux:navlist.item>
</flux:navlist.group>
</flux:navlist>
<flux:spacer />
<flux:navlist variant="outline">
<flux:navlist.item icon="folder-git-2" href="https://github.com/laravel/livewire-starter-kit" target="_blank">
{{ __('Repository') }}
</flux:navlist.item>
<flux:navlist.item icon="book-open-text" href="https://laravel.com/docs/starter-kits#livewire" target="_blank">
{{ __('Documentation') }}
</flux:navlist.item>
</flux:navlist>
</flux:sidebar>
{{ $slot }}
@fluxScripts
</body>
</html>

View file

@ -1,10 +1,15 @@
<!DOCTYPE html>
{{--
Hub × FluxUI Phase 1 Portal-Shell im Hub-Design.
class="dark" wurde entfernt; Light Mode ist Default, Dark kommt mit
FluxUI Appearance-Switcher in Phase 5.
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()) }}">
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" @class(['dark' => request()->cookie('flux_appearance') === 'dark'])>
<head>
@include('partials.head')
</head>

View file

@ -1,3 +0,0 @@
<x-layouts.auth.simple :title="$title ?? null">
{{ $slot }}
</x-layouts.auth.simple>

View file

@ -1,26 +0,0 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-neutral-100 antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
<div class="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div class="flex w-full max-w-md flex-col gap-6">
<a href="{{ route('home') }}" class="flex flex-col items-center gap-2 font-medium" wire:navigate>
<span class="flex h-9 w-9 items-center justify-center rounded-md">
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
</span>
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
</a>
<div class="flex flex-col gap-6">
<div class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
<div class="px-10 py-8">{{ $slot }}</div>
</div>
</div>
</div>
</div>
@fluxScripts
</body>
</html>

View file

@ -1,35 +0,0 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-white antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
<div class="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div class="flex w-full max-w-sm flex-col gap-2">
<a href="{{ route('home') }}" class="flex flex-col items-center gap-2 font-medium" wire:navigate>
<span class="flex h-9 w-9 mb-1 items-center justify-center rounded-md">
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
</span>
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
</a>
<div class="flex flex-col gap-6">
{{ $slot }}
</div>
</div>
</div>
@livewireScripts
@fluxScripts
<script src="{{ asset('vendor/livewire/livewire.js') }}"></script>
<!-- Debug: Script-Status -->
<script>
console.log('Body Scripts geladen');
console.log('Livewire JS:', {{ file_exists(public_path('vendor/livewire/livewire.js')) ? 'true' : 'false' }});
if (typeof Livewire !== 'undefined') {
console.log('Livewire verfügbar:', true);
} else {
console.log('Livewire verfügbar:', false);
}
</script>
</body>
</html>

View file

@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
@include('partials.head')
</head>
<body class="min-h-screen bg-white antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
<div class="relative grid h-dvh flex-col items-center justify-center px-8 sm:px-0 lg:max-w-none lg:grid-cols-2 lg:px-0">
<div class="bg-muted relative hidden h-full flex-col p-10 text-white lg:flex dark:border-e dark:border-neutral-800">
<div class="absolute inset-0 bg-neutral-900"></div>
<a href="{{ route('home') }}" class="relative z-20 flex items-center text-lg font-medium" wire:navigate>
<span class="flex h-10 w-10 items-center justify-center rounded-md">
<x-app-logo-icon class="me-2 h-7 fill-current text-white" />
</span>
{{ config('app.name', 'Laravel') }}
</a>
@php
[$message, $author] = str(Illuminate\Foundation\Inspiring::quotes()->random())->explode('-');
@endphp
<div class="relative z-20 mt-auto">
<blockquote class="space-y-2">
<flux:heading size="lg">&ldquo;{{ trim($message) }}&rdquo;</flux:heading>
<footer><flux:heading>{{ trim($author) }}</flux:heading></footer>
</blockquote>
</div>
</div>
<div class="w-full lg:p-8">
<div class="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
<a href="{{ route('home') }}" class="z-20 flex flex-col items-center gap-2 font-medium lg:hidden" wire:navigate>
<span class="flex h-9 w-9 items-center justify-center rounded-md">
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
</span>
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
</a>
{{ $slot }}
</div>
</div>
</div>
@fluxScripts
</body>
</html>

View file

@ -1,20 +1,40 @@
<div class="flex items-start max-md:flex-col">
<div class="me-10 w-full pb-4 md:w-[220px]">
<flux:navlist>
<flux:navlist.item :href="route('settings.profile')" wire:navigate>{{ __('Profile') }}</flux:navlist.item>
<flux:navlist.item :href="route('settings.password')" wire:navigate>{{ __('Password') }}</flux:navlist.item>
<flux:navlist.item :href="route('settings.appearance')" wire:navigate>{{ __('Appearance') }}</flux:navlist.item>
</flux:navlist>
</div>
<flux:separator class="md:hidden" />
<div class="flex-1 self-stretch max-md:pt-6">
<flux:heading>{{ $heading ?? '' }}</flux:heading>
<flux:subheading>{{ $subheading ?? '' }}</flux:subheading>
<div class="mt-5 w-full max-w-lg">
{{ $slot }}
{{-- Hub-style settings layout: sidebar nav + content panel --}}
<div class="grid items-start gap-6 md:grid-cols-[220px_1fr]">
<aside class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Mein Konto') }}</span>
</div>
<nav class="p-2">
<flux:navlist>
<flux:navlist.item :href="route('settings.profile')" wire:navigate>
{{ __('Profile') }}
</flux:navlist.item>
<flux:navlist.item :href="route('settings.password')" wire:navigate>
{{ __('Password') }}
</flux:navlist.item>
<flux:navlist.item :href="route('settings.appearance')" wire:navigate>
{{ __('Appearance') }}
</flux:navlist.item>
</flux:navlist>
</nav>
</aside>
<div class="space-y-6 min-w-0">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ $heading ?? '' }}</span>
</div>
<div class="p-5 space-y-1">
@if (! empty($subheading))
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">
{{ $subheading }}
</p>
@endif
<div class="pt-4">
{{ $slot }}
</div>
</div>
</article>
</div>
</div>

View file

@ -79,8 +79,20 @@
$fontClass = $serif ? 'font-serif' : 'font-sans';
$baseAttributes = $attributes->merge(['class' => $fontClass]);
/**
* Variant=auto: Inline-Color über CSS-Custom-Property mit Light-Fallback.
* Damit kann das Portal im Dark Mode `--brand-mark-name-color` global auf
* weiß setzen, ohne dass das Hub-Frontend (Light-Only) tangiert wird
* dort ist die Variable nie definiert und der Fallback (Marken-Standardfarbe)
* greift. Bei `on-dark` und `mono` bleibt der Inline-Style hart, weil der
* Aufrufer dort explizit eine Farb-Intention setzt.
*/
$nameStyle = $variant === 'auto'
? "color: var(--brand-mark-name-color, {$nameColor});"
: "color: {$nameColor};";
@endphp
<span {{ $baseAttributes }}><span
style="color: {{ $nameColor }};">{{ $mark['name'] }}</span><span
style="{{ $nameStyle }}">{{ $mark['name'] }}</span><span
style="color: {{ $accentColor }};">{{ $mark['accent'] }}</span></span>

View file

@ -1,5 +1,8 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
{{-- Phase 5: Anti-Flash. class="dark" nicht mehr hardcoded Server liest
das `flux_appearance`-Cookie (vom JS-Bridge in partials/admin-head.blade.php
gesetzt) und rendert es direkt im <html>. --}}
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" @class(['dark' => request()->cookie('flux_appearance') === 'dark'])>
<head>
<meta charset="utf-8">

View file

@ -106,27 +106,45 @@ new #[Layout('components.layouts.app'), Title('Kategorie anlegen')] class extend
}
}; ?>
<div class="space-y-6">
@if(session('success'))
<flux:callout color="green" icon="check-circle">{{ session('success') }}</flux:callout>
@endif
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Kategorien') }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Kategorie anlegen') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Neue Themen-Kategorie mit deutscher und englischer Übersetzung.') }}
</p>
</div>
<flux:card>
<div class="flex items-center justify-between">
<flux:heading size="xl">{{ __('Kategorie anlegen') }}</flux:heading>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" :href="route('admin.categories.index')" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</flux:card>
</header>
@if (session('success'))
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-center gap-2
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
<flux:icon.check-circle class="size-[16px] flex-shrink-0" />
{{ session('success') }}
</div>
@endif
<form wire:submit="save" class="space-y-6">
<div class="grid gap-6 lg:grid-cols-[1fr,320px]">
<div class="space-y-6">
<flux:card>
<flux:heading size="md" class="mb-4">{{ __('Deutsche Übersetzung') }}</flux:heading>
<div class="space-y-4">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Deutsche Übersetzung') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:input
wire:model.live.debounce.400ms="nameDe"
:label="__('Name (DE)')"
@ -148,12 +166,13 @@ new #[Layout('components.layouts.app'), Title('Kategorie anlegen')] class extend
/>
<flux:error name="descriptionDe" />
</div>
</flux:card>
</article>
<flux:card>
<flux:heading size="md" class="mb-4">{{ __('English translation') }}</flux:heading>
<div class="space-y-4">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('English translation') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:input
wire:model.live.debounce.400ms="nameEn"
:label="__('Name (EN)')"
@ -175,18 +194,19 @@ new #[Layout('components.layouts.app'), Title('Kategorie anlegen')] class extend
/>
<flux:error name="descriptionEn" />
</div>
</flux:card>
</article>
</div>
<div class="space-y-4">
<flux:card>
<flux:heading size="sm" class="mb-3">{{ __('Einstellungen') }}</flux:heading>
<div class="space-y-4">
<div class="space-y-6">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Einstellungen') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:field>
<flux:label>{{ __('Portal') }}</flux:label>
<flux:select wire:model="portal">
@foreach($portalOptions as $option)
@foreach ($portalOptions as $option)
<option value="{{ $option->value }}">{{ $option->label() }}</option>
@endforeach
</flux:select>
@ -197,7 +217,7 @@ new #[Layout('components.layouts.app'), Title('Kategorie anlegen')] class extend
<flux:label>{{ __('Übergeordnete Kategorie') }}</flux:label>
<flux:select wire:model="parentId">
<option value="">{{ __('— Keine —') }}</option>
@foreach($parentOptions as $parent)
@foreach ($parentOptions as $parent)
<option value="{{ $parent->id }}">
{{ $parent->translations->first()?->name ?? '#'.$parent->id }}
</option>
@ -208,7 +228,7 @@ new #[Layout('components.layouts.app'), Title('Kategorie anlegen')] class extend
<flux:checkbox wire:model="isActive" :label="__('Aktiv')" />
</div>
</flux:card>
</article>
<flux:button type="submit" variant="primary" class="w-full" icon="check">
{{ __('Speichern') }}

View file

@ -172,33 +172,54 @@ new #[Layout('components.layouts.app'), Title('Kategorie bearbeiten')] class ext
}
}; ?>
<div class="space-y-6">
@if(session('success'))
<flux:callout color="green" icon="check-circle">{{ session('success') }}</flux:callout>
@endif
@if(session('error'))
<flux:callout color="red" icon="exclamation-triangle">{{ session('error') }}</flux:callout>
@endif
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl">{{ __('Kategorie bearbeiten') }}</flux:heading>
<flux:subheading>ID: {{ $id }} · {{ $releaseCount }} {{ __('PMs') }} · {{ $childrenCount }} {{ __('Unterkategorien') }}</flux:subheading>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Kategorien') }}</span>
<span class="badge hub">ID #{{ $id }}</span>
<span class="badge hub">{{ $releaseCount }} {{ __('PMs') }}</span>
@if ($childrenCount > 0)
<span class="badge hub">{{ $childrenCount }} {{ __('Unterkategorien') }}</span>
@endif
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Kategorie bearbeiten') }}
</h1>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" :href="route('admin.categories.index')" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</flux:card>
</header>
@if (session('success'))
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-center gap-2
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
<flux:icon.check-circle class="size-[16px] flex-shrink-0" />
{{ session('success') }}
</div>
@endif
@if (session('error'))
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-start gap-3
bg-[color:var(--color-err-soft)] border-[color:var(--color-err)]/30 text-[color:var(--color-ink-2)]">
<flux:icon.exclamation-triangle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-err)]" />
<div class="flex-1">{{ session('error') }}</div>
</div>
@endif
<form wire:submit="save" class="space-y-6">
<div class="grid gap-6 lg:grid-cols-[1fr,320px]">
<div class="space-y-6">
<flux:card>
<flux:heading size="md" class="mb-4">{{ __('Deutsche Übersetzung') }}</flux:heading>
<div class="space-y-4">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Deutsche Übersetzung') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:input wire:model="nameDe" :label="__('Name (DE)')" required />
<flux:error name="nameDe" />
@ -208,12 +229,13 @@ new #[Layout('components.layouts.app'), Title('Kategorie bearbeiten')] class ext
<flux:textarea wire:model="descriptionDe" :label="__('Beschreibung (DE)')" rows="3" />
<flux:error name="descriptionDe" />
</div>
</flux:card>
</article>
<flux:card>
<flux:heading size="md" class="mb-4">{{ __('English translation') }}</flux:heading>
<div class="space-y-4">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('English translation') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:input wire:model="nameEn" :label="__('Name (EN)')" required />
<flux:error name="nameEn" />
@ -223,18 +245,19 @@ new #[Layout('components.layouts.app'), Title('Kategorie bearbeiten')] class ext
<flux:textarea wire:model="descriptionEn" :label="__('Description (EN)')" rows="3" />
<flux:error name="descriptionEn" />
</div>
</flux:card>
</article>
</div>
<div class="space-y-4">
<flux:card>
<flux:heading size="sm" class="mb-3">{{ __('Einstellungen') }}</flux:heading>
<div class="space-y-4">
<div class="space-y-6">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Einstellungen') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:field>
<flux:label>{{ __('Portal') }}</flux:label>
<flux:select wire:model="portal">
@foreach($portalOptions as $option)
@foreach ($portalOptions as $option)
<option value="{{ $option->value }}">{{ $option->label() }}</option>
@endforeach
</flux:select>
@ -245,7 +268,7 @@ new #[Layout('components.layouts.app'), Title('Kategorie bearbeiten')] class ext
<flux:label>{{ __('Übergeordnete Kategorie') }}</flux:label>
<flux:select wire:model="parentId">
<option value="">{{ __('— Keine —') }}</option>
@foreach($parentOptions as $parent)
@foreach ($parentOptions as $parent)
<option value="{{ $parent->id }}">
{{ $parent->translations->first()?->name ?? '#'.$parent->id }}
</option>
@ -256,17 +279,27 @@ new #[Layout('components.layouts.app'), Title('Kategorie bearbeiten')] class ext
<flux:checkbox wire:model="isActive" :label="__('Aktiv')" />
</div>
</flux:card>
</article>
<flux:button type="submit" variant="primary" class="w-full" icon="check">
{{ __('Änderungen speichern') }}
</flux:button>
<flux:modal.trigger name="confirm-delete-category">
<flux:button type="button" variant="danger" icon="trash" class="w-full">
{{ __('Kategorie löschen') }}
</flux:button>
</flux:modal.trigger>
<article class="panel" style="border-left:3px solid var(--color-err);">
<div class="panel-head">
<span class="section-eyebrow" style="color:var(--color-err);">{{ __('Danger Zone') }}</span>
</div>
<div class="p-5 space-y-3">
<p class="text-[12.5px] text-[color:var(--color-ink-2)] m-0">
{{ __('Kategorie unwiderruflich entfernen, sofern keine PMs/Unterkategorien hängen.') }}
</p>
<flux:modal.trigger name="confirm-delete-category">
<flux:button type="button" variant="danger" icon="trash" class="w-full">
{{ __('Kategorie löschen') }}
</flux:button>
</flux:modal.trigger>
</div>
</article>
</div>
</div>
</form>

View file

@ -141,108 +141,107 @@ new #[Layout('components.layouts.app'), Title('Kategorien')] class extends Compo
}
}; ?>
<div class="space-y-6">
{{-- Statistiken --}}
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Kategorien') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['total'] }}</flux:text>
</div>
<flux:icon.folder class="size-8 text-blue-500" />
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Stammdaten') }}</span>
</div>
</flux:card>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Kategorien') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Themen-Taxonomie für Pressemitteilungen über alle Portale hinweg.') }}
</p>
</div>
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Mit PMs') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['with_releases'] }}</flux:text>
</div>
<flux:icon.document-text class="size-8 text-green-500" />
</div>
</flux:card>
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('PMs gesamt') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['total_releases'] }}</flux:text>
</div>
<flux:icon.newspaper class="size-8 text-purple-500" />
</div>
</flux:card>
<flux:card>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Portale') }}</flux:text>
<div class="mt-2 space-y-1 text-sm">
<div class="flex items-center justify-between gap-3">
<span>{{ __('Presseecho') }}</span>
<span class="font-semibold">{{ number_format($stats['presseecho_releases']) }}</span>
</div>
<div class="flex items-center justify-between gap-3">
<span>{{ __('Businessportal24') }}</span>
<span class="font-semibold">{{ number_format($stats['businessportal24_releases']) }}</span>
</div>
</div>
</flux:card>
</div>
{{-- Filter + Aktion --}}
<flux:card>
<div class="flex flex-wrap items-center gap-3">
<div class="min-w-[260px] flex-1">
<flux:input
wire:model.live.debounce.300ms="search"
placeholder="{{ __('Kategorie suchen (Name, Slug)…') }}"
icon="magnifying-glass"
/>
</div>
<flux:button
variant="primary"
icon="plus"
:href="route('admin.categories.create')"
wire:navigate
>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="primary" icon="plus" :href="route('admin.categories.create')" wire:navigate>
{{ __('Kategorie anlegen') }}
</flux:button>
</div>
</flux:card>
</header>
{{-- Karten --}}
{{-- Sortier-Buttons --}}
<flux:card>
<div class="flex items-center gap-2 text-sm">
<span class="text-zinc-500">{{ __('Sortierung:') }}</span>
<flux:button size="sm" variant="{{ $sortBy === 'id' ? 'primary' : 'ghost' }}" wire:click="sort('id')">
{{ __('Standard') }} @if($sortBy==='id') {{ $sortDir === 'asc' ? '↑' : '↓' }} @endif
</flux:button>
<flux:button size="sm" variant="{{ $sortBy === 'press_releases_count' ? 'primary' : 'ghost' }}" wire:click="sort('press_releases_count')">
{{ __('PMs') }} @if($sortBy==='press_releases_count') {{ $sortDir === 'asc' ? '↑' : '↓' }} @endif
</flux:button>
{{-- ============== KPI-Reihe ============== --}}
<section class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
<x-portal.stat-card variant="primary" :label="__('Kategorien')" :value="number_format($stats['total'])">
<x-slot:meta>{{ __('gesamt') }}</x-slot:meta>
<x-slot:trend>{{ __('Themen-Taxonomie') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="ok" :label="__('Mit PMs')" :value="number_format($stats['with_releases'])">
<x-slot:meta>{{ __('aktiv genutzt') }}</x-slot:meta>
<x-slot:trend>{{ __('mind. eine PM') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('PMs gesamt')" :value="number_format($stats['total_releases'])">
<x-slot:meta>{{ __('alle Portale') }}</x-slot:meta>
<x-slot:trend>{{ __('Content-Output') }}</x-slot:trend>
</x-portal.stat-card>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Portale') }}</span>
</div>
<dl class="p-5 space-y-2 text-[12.5px]">
<div class="flex items-center justify-between gap-3">
<dt class="text-[color:var(--color-ink-3)]">{{ __('Presseecho') }}</dt>
<dd class="font-semibold text-[color:var(--color-ink)] tabular-nums">{{ number_format($stats['presseecho_releases']) }}</dd>
</div>
<div class="flex items-center justify-between gap-3">
<dt class="text-[color:var(--color-ink-3)]">{{ __('Businessportal24') }}</dt>
<dd class="font-semibold text-[color:var(--color-ink)] tabular-nums">{{ number_format($stats['businessportal24_releases']) }}</dd>
</div>
</dl>
</article>
</section>
{{-- ============== FILTER + SORT ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Filter & Sortierung') }}</span>
</div>
</flux:card>
<div class="p-5 space-y-4">
<flux:input
wire:model.live.debounce.300ms="search"
placeholder="{{ __('Kategorie suchen (Name, Slug)…') }}"
icon="magnifying-glass"
/>
<div class="flex items-center gap-2 text-[12px] flex-wrap">
<span class="text-[10.5px] uppercase tracking-[0.6px] font-semibold text-[color:var(--color-ink-3)]">
{{ __('Sortierung') }}
</span>
<flux:button size="sm" variant="{{ $sortBy === 'id' ? 'primary' : 'ghost' }}" wire:click="sort('id')">
{{ __('Standard') }} @if ($sortBy === 'id') {{ $sortDir === 'asc' ? '↑' : '↓' }} @endif
</flux:button>
<flux:button size="sm" variant="{{ $sortBy === 'press_releases_count' ? 'primary' : 'ghost' }}" wire:click="sort('press_releases_count')">
{{ __('PMs') }} @if ($sortBy === 'press_releases_count') {{ $sortDir === 'asc' ? '↑' : '↓' }} @endif
</flux:button>
</div>
</div>
</article>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
@forelse($categories as $category)
{{-- ============== KATEGORIE-KARTEN ============== --}}
<section class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
@forelse ($categories as $category)
@php
$de = $category->translations->firstWhere('locale', 'de');
$en = $category->translations->firstWhere('locale', 'en');
@endphp
<flux:card class="group relative h-full transition hover:border-blue-300 hover:bg-blue-50/40 dark:hover:border-blue-700 dark:hover:bg-blue-950/20">
<article class="panel group relative h-full transition hover:border-[color:var(--color-hub)]/30">
<a href="{{ route('admin.press-releases.index', ['category' => $category->id]) }}" wire:navigate class="absolute inset-0 z-0" aria-hidden="true"></a>
<div class="relative z-10 space-y-4">
<div class="flex items-start justify-between gap-2">
<div class="min-w-0 flex-1">
<flux:heading size="lg" class="truncate">{{ $de?->name ?? '' }}</flux:heading>
<flux:text class="truncate text-sm text-zinc-500">{{ $en?->name ?? '' }}</flux:text>
<div class="relative z-10 flex h-full flex-col">
<div class="panel-head">
<div class="min-w-0">
<span class="section-eyebrow truncate">{{ __('Kategorie') }}</span>
</div>
<div class="flex items-center gap-2">
<flux:badge color="{{ $category->is_active ? 'green' : 'zinc' }}" size="sm">
{{ $category->is_active ? __('Aktiv') : __('Inaktiv') }}
</flux:badge>
@if ($category->is_active)
<span class="badge ok dot">{{ __('Aktiv') }}</span>
@else
<span class="badge dot">{{ __('Inaktiv') }}</span>
@endif
<flux:button
size="xs"
variant="ghost"
@ -254,43 +253,58 @@ new #[Layout('components.layouts.app'), Title('Kategorien')] class extends Compo
</div>
</div>
@if($de?->description)
<flux:text class="line-clamp-2 text-sm text-zinc-600 dark:text-zinc-400">
{{ $de->description }}
</flux:text>
@endif
<div class="grid grid-cols-2 gap-2 border-t border-zinc-200 pt-3 text-xs dark:border-zinc-700">
<div class="rounded-md bg-zinc-50 px-2 py-1 dark:bg-zinc-800">
<div class="text-zinc-500">{{ __('Presseecho') }}</div>
<div class="font-semibold">{{ number_format($category->presseecho_press_releases_count) }}</div>
<div class="p-5 space-y-4 flex-1">
<div>
<div class="text-[15px] font-semibold text-[color:var(--color-ink)] truncate">
{{ $de?->name ?? '' }}
</div>
<div class="text-[12px] text-[color:var(--color-ink-3)] truncate mt-0.5">
{{ $en?->name ?? '' }}
</div>
</div>
<div class="rounded-md bg-zinc-50 px-2 py-1 dark:bg-zinc-800">
<div class="text-zinc-500">{{ __('Businessportal24') }}</div>
<div class="font-semibold">{{ number_format($category->businessportal24_press_releases_count) }}</div>
@if ($de?->description)
<p class="line-clamp-2 text-[12.5px] text-[color:var(--color-ink-2)] m-0">
{{ $de->description }}
</p>
@endif
<div class="grid grid-cols-2 gap-2 pt-3 border-t border-[color:var(--color-bg-rule)] text-[11.5px]">
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] px-2 py-1.5">
<div class="text-[10px] uppercase tracking-[0.5px] font-semibold text-[color:var(--color-ink-3)]">{{ __('Presseecho') }}</div>
<div class="font-semibold text-[color:var(--color-ink)] tabular-nums">{{ number_format($category->presseecho_press_releases_count) }}</div>
</div>
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] px-2 py-1.5">
<div class="text-[10px] uppercase tracking-[0.5px] font-semibold text-[color:var(--color-ink-3)]">{{ __('Businessportal24') }}</div>
<div class="font-semibold text-[color:var(--color-ink)] tabular-nums">{{ number_format($category->businessportal24_press_releases_count) }}</div>
</div>
</div>
</div>
<div class="flex items-center justify-between border-t border-zinc-200 pt-3 dark:border-zinc-700">
<div class="flex items-center gap-1.5 text-sm text-zinc-500">
<div class="px-5 py-3 border-t border-[color:var(--color-bg-rule)] flex items-center justify-between">
<div class="flex items-center gap-1.5 text-[12px] text-[color:var(--color-ink-3)]">
<flux:icon.newspaper class="size-4" />
{{ $category->press_releases_count }} {{ __('PMs') }}
<span class="tabular-nums">{{ $category->press_releases_count }}</span>
{{ __('PMs') }}
</div>
<flux:badge color="zinc" size="sm">/{{ $de?->slug ?? $category->id }}</flux:badge>
<span class="badge hub">/{{ $de?->slug ?? $category->id }}</span>
</div>
</div>
</flux:card>
</article>
@empty
<div class="col-span-full">
<flux:card>
<div class="flex flex-col items-center justify-center py-12">
<flux:icon.folder class="size-12 text-zinc-400 dark:text-zinc-600" />
<flux:text class="mt-4 text-zinc-500">{{ __('Keine Kategorien gefunden.') }}</flux:text>
<article class="panel col-span-full">
<div class="p-10 flex flex-col items-center justify-center text-center">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-3
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.folder class="size-6" />
</div>
</flux:card>
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Keine Kategorien gefunden.') }}
</div>
</div>
</article>
@endforelse
</div>
</section>
{{ $categories->links() }}
</div>

View file

@ -117,165 +117,194 @@ new #[Layout('components.layouts.app'), Title('Neue Firma')] class extends Compo
}
}; ?>
<form wire:submit="save" class="space-y-6">
{{-- Basisinformationen --}}
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Basisinformationen') }}</flux:heading>
<div class="space-y-4">
<div class="grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Portal') }}</flux:label>
<flux:select wire:model="portal">
@foreach($portalOptions as $portalOption)
<option value="{{ $portalOption->value }}">{{ $portalOption->label() }}</option>
@endforeach
</flux:select>
</flux:field>
<flux:field>
<flux:label>{{ __('Typ') }}</flux:label>
<flux:select wire:model="type">
@foreach($typeOptions as $typeOption)
<option value="{{ $typeOption->value }}">{{ $typeOption->label() }}</option>
@endforeach
</flux:select>
</flux:field>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Stammdaten · Neue Firma') }}</span>
</div>
<flux:field>
<flux:label>{{ __('Firmenname') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="company_name" placeholder="{{ __('Vollständiger Firmenname...') }}" />
<flux:error name="company_name" />
</flux:field>
<flux:field>
<flux:label>{{ __('Beschreibung') }}</flux:label>
<flux:textarea wire:model="description" rows="4" placeholder="{{ __('Kurze Beschreibung der Firma (optional)...') }}" />
<flux:error name="description" />
</flux:field>
<div class="grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('E-Mail') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="email" type="email" placeholder="{{ __('kontakt@firma.de') }}" icon="envelope" />
<flux:error name="email" />
</flux:field>
<flux:field>
<flux:label>{{ __('Telefon') }}</flux:label>
<flux:input wire:model="phone" type="tel" placeholder="{{ __('+49 123 456789') }}" icon="phone" />
<flux:error name="phone" />
</flux:field>
</div>
<flux:field>
<flux:label>{{ __('Website') }}</flux:label>
<flux:input wire:model="website" type="url" placeholder="{{ __('https://www.firma.de') }}" icon="globe-alt" />
<flux:error name="website" />
</flux:field>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Neue Firma') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Stammdaten, Adresse, Logo und Rechtsangaben einer neuen Firma erfassen.') }}
</p>
</div>
</flux:card>
{{-- Adresse --}}
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Adresse') }}</flux:heading>
<div class="space-y-4">
<flux:field>
<flux:label>{{ __('Straße & Hausnummer') }}</flux:label>
<flux:input wire:model="street" placeholder="{{ __('Musterstraße 123') }}" />
<flux:error name="street" />
</flux:field>
<div class="grid gap-4 sm:grid-cols-3">
<flux:field>
<flux:label>{{ __('PLZ') }}</flux:label>
<flux:input wire:model="zip" placeholder="{{ __('12345') }}" />
<flux:error name="zip" />
</flux:field>
<flux:field class="sm:col-span-2">
<flux:label>{{ __('Stadt') }}</flux:label>
<flux:input wire:model="city" placeholder="{{ __('Berlin') }}" />
<flux:error name="city" />
</flux:field>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Bundesland / Region') }}</flux:label>
<flux:input wire:model="state" placeholder="{{ __('Bayern') }}" />
<flux:error name="state" />
</flux:field>
<flux:field>
<flux:label>{{ __('Land') }} <span class="text-red-500">*</span></flux:label>
<flux:select wire:model="country">
@foreach($countries as $country)
<option value="{{ $country['code'] }}">{{ $country['name'] }}</option>
@endforeach
</flux:select>
<flux:error name="country" />
</flux:field>
</div>
</div>
</flux:card>
{{-- Rechtliche Daten --}}
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Rechtliche Daten') }}</flux:heading>
<div class="grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Steuernummer / USt-IdNr.') }}</flux:label>
<flux:input wire:model="tax_id" placeholder="{{ __('DE123456789') }}" />
<flux:error name="tax_id" />
</flux:field>
<flux:field>
<flux:label>{{ __('Handelsregisternummer') }}</flux:label>
<flux:input wire:model="registration_number" placeholder="{{ __('HRB 12345') }}" />
<flux:error name="registration_number" />
</flux:field>
</div>
</flux:card>
{{-- Logo & Status --}}
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Logo & Status') }}</flux:heading>
<div class="space-y-4">
<flux:field>
<flux:label>{{ __('Firmenlogo') }}</flux:label>
<flux:input type="file" wire:model="logo" accept="image/*" />
<flux:description>{{ __('Maximal 1 MB. Empfohlen: quadratisch, min. 400x400px') }}</flux:description>
<flux:error name="logo" />
@if ($logo)
<div class="mt-4">
<flux:text class="text-sm text-zinc-500 mb-2">{{ __('Vorschau:') }}</flux:text>
<img src="{{ $logo->temporaryUrl() }}" width="128" height="128" class="h-32 max-h-32 w-32 max-w-32 rounded-lg border border-zinc-200 object-contain dark:border-zinc-700">
</div>
@endif
</flux:field>
<div class="flex gap-6">
<flux:checkbox wire:model="is_verified" label="{{ __('Firma ist verifiziert') }}" />
<flux:checkbox wire:model="is_active" label="{{ __('Firma ist aktiv') }}" />
</div>
</div>
</flux:card>
{{-- Aktionen --}}
<flux:card>
<div class="flex justify-end gap-3">
<flux:button variant="ghost" href="{{ route('admin.companies.index') }}" wire:navigate>
{{ __('Abbrechen') }}
</flux:button>
<flux:button type="submit" variant="primary">
{{ __('Firma erstellen') }}
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.companies.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</flux:card>
</form>
</header>
<form wire:submit="save" class="space-y-6">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Basisinformationen') }}</span>
</div>
<div class="p-5 space-y-4">
<div class="grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Portal') }}</flux:label>
<flux:select wire:model="portal">
@foreach ($portalOptions as $portalOption)
<option value="{{ $portalOption->value }}">{{ $portalOption->label() }}</option>
@endforeach
</flux:select>
</flux:field>
<flux:field>
<flux:label>{{ __('Typ') }}</flux:label>
<flux:select wire:model="type">
@foreach ($typeOptions as $typeOption)
<option value="{{ $typeOption->value }}">{{ $typeOption->label() }}</option>
@endforeach
</flux:select>
</flux:field>
</div>
<flux:field>
<flux:label>{{ __('Firmenname') }} <span class="text-[color:var(--color-err)]">*</span></flux:label>
<flux:input wire:model="company_name" placeholder="{{ __('Vollständiger Firmenname...') }}" />
<flux:error name="company_name" />
</flux:field>
<flux:field>
<flux:label>{{ __('Beschreibung') }}</flux:label>
<flux:textarea wire:model="description" rows="4" placeholder="{{ __('Kurze Beschreibung der Firma (optional)...') }}" />
<flux:error name="description" />
</flux:field>
<div class="grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('E-Mail') }} <span class="text-[color:var(--color-err)]">*</span></flux:label>
<flux:input wire:model="email" type="email" placeholder="{{ __('kontakt@firma.de') }}" icon="envelope" />
<flux:error name="email" />
</flux:field>
<flux:field>
<flux:label>{{ __('Telefon') }}</flux:label>
<flux:input wire:model="phone" type="tel" placeholder="{{ __('+49 123 456789') }}" icon="phone" />
<flux:error name="phone" />
</flux:field>
</div>
<flux:field>
<flux:label>{{ __('Website') }}</flux:label>
<flux:input wire:model="website" type="url" placeholder="{{ __('https://www.firma.de') }}" icon="globe-alt" />
<flux:error name="website" />
</flux:field>
</div>
</article>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Adresse') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:field>
<flux:label>{{ __('Straße & Hausnummer') }}</flux:label>
<flux:input wire:model="street" placeholder="{{ __('Musterstraße 123') }}" />
<flux:error name="street" />
</flux:field>
<div class="grid gap-4 sm:grid-cols-3">
<flux:field>
<flux:label>{{ __('PLZ') }}</flux:label>
<flux:input wire:model="zip" placeholder="{{ __('12345') }}" />
<flux:error name="zip" />
</flux:field>
<flux:field class="sm:col-span-2">
<flux:label>{{ __('Stadt') }}</flux:label>
<flux:input wire:model="city" placeholder="{{ __('Berlin') }}" />
<flux:error name="city" />
</flux:field>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Bundesland / Region') }}</flux:label>
<flux:input wire:model="state" placeholder="{{ __('Bayern') }}" />
<flux:error name="state" />
</flux:field>
<flux:field>
<flux:label>{{ __('Land') }} <span class="text-[color:var(--color-err)]">*</span></flux:label>
<flux:select wire:model="country">
@foreach ($countries as $country)
<option value="{{ $country['code'] }}">{{ $country['name'] }}</option>
@endforeach
</flux:select>
<flux:error name="country" />
</flux:field>
</div>
</div>
</article>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Rechtliche Daten') }}</span>
</div>
<div class="p-5 grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Steuernummer / USt-IdNr.') }}</flux:label>
<flux:input wire:model="tax_id" placeholder="{{ __('DE123456789') }}" />
<flux:error name="tax_id" />
</flux:field>
<flux:field>
<flux:label>{{ __('Handelsregisternummer') }}</flux:label>
<flux:input wire:model="registration_number" placeholder="{{ __('HRB 12345') }}" />
<flux:error name="registration_number" />
</flux:field>
</div>
</article>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Logo & Status') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:field>
<flux:label>{{ __('Firmenlogo') }}</flux:label>
<flux:input type="file" wire:model="logo" accept="image/*" />
<flux:description>{{ __('Maximal 1 MB. Empfohlen: quadratisch, min. 400x400px') }}</flux:description>
<flux:error name="logo" />
@if ($logo)
<div class="mt-4">
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-2">
{{ __('Vorschau:') }}
</div>
<img src="{{ $logo->temporaryUrl() }}" width="128" height="128"
class="h-32 max-h-32 w-32 max-w-32 rounded-[6px] border border-[color:var(--color-bg-rule)] object-contain bg-[color:var(--color-bg-elev)]">
</div>
@endif
</flux:field>
<div class="flex gap-6 pt-2 border-t border-[color:var(--color-bg-rule)]">
<flux:checkbox wire:model="is_verified" label="{{ __('Firma ist verifiziert') }}" />
<flux:checkbox wire:model="is_active" label="{{ __('Firma ist aktiv') }}" />
</div>
</div>
</article>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Aktionen') }}</span>
</div>
<div class="p-5 flex justify-end gap-3">
<flux:button variant="ghost" href="{{ route('admin.companies.index') }}" wire:navigate>
{{ __('Abbrechen') }}
</flux:button>
<flux:button type="submit" variant="primary">
{{ __('Firma erstellen') }}
</flux:button>
</div>
</article>
</form>
</div>

View file

@ -195,198 +195,233 @@ new #[Layout('components.layouts.app'), Title('Firma bearbeiten')] class extends
}
}; ?>
<div class="space-y-6">
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Stammdaten · Firma bearbeiten') }}</span>
<span class="badge hub">ID {{ $companyId }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Firma bearbeiten') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Stammdaten, Adresse, Logo und Rechtsangaben der Firma aktualisieren.') }}
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.companies.show', $companyId) }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</header>
<form wire:submit="update" class="space-y-6">
{{-- Basisinformationen --}}
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Basisinformationen') }}</flux:heading>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Basisinformationen') }}</span>
</div>
<div class="p-5 space-y-4">
<div class="grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Portal') }}</flux:label>
<flux:select wire:model="portal">
@foreach ($portalOptions as $portalOption)
<option value="{{ $portalOption->value }}">{{ $portalOption->label() }}</option>
@endforeach
</flux:select>
</flux:field>
<flux:field>
<flux:label>{{ __('Typ') }}</flux:label>
<flux:select wire:model="type">
@foreach ($typeOptions as $typeOption)
<option value="{{ $typeOption->value }}">{{ $typeOption->label() }}</option>
@endforeach
</flux:select>
</flux:field>
</div>
<div class="space-y-4">
<div class="grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Portal') }}</flux:label>
<flux:select wire:model="portal">
@foreach($portalOptions as $portalOption)
<option value="{{ $portalOption->value }}">{{ $portalOption->label() }}</option>
@endforeach
</flux:select>
<flux:label>{{ __('Firmenname') }} <span class="text-[color:var(--color-err)]">*</span></flux:label>
<flux:input wire:model="company_name" placeholder="{{ __('Vollständiger Firmenname...') }}" />
<flux:error name="company_name" />
</flux:field>
<flux:field>
<flux:label>{{ __('Typ') }}</flux:label>
<flux:select wire:model="type">
@foreach($typeOptions as $typeOption)
<option value="{{ $typeOption->value }}">{{ $typeOption->label() }}</option>
@endforeach
</flux:select>
<flux:label>{{ __('Beschreibung') }}</flux:label>
<flux:textarea wire:model="description" rows="4" placeholder="{{ __('Kurze Beschreibung der Firma (optional)...') }}" />
<flux:error name="description" />
</flux:field>
<div class="grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('E-Mail') }} <span class="text-[color:var(--color-err)]">*</span></flux:label>
<flux:input wire:model="email" type="email" placeholder="{{ __('kontakt@firma.de') }}" icon="envelope" />
<flux:error name="email" />
</flux:field>
<flux:field>
<flux:label>{{ __('Telefon') }}</flux:label>
<flux:input wire:model="phone" type="tel" placeholder="{{ __('+49 123 456789') }}" icon="phone" />
<flux:error name="phone" />
</flux:field>
</div>
<flux:field>
<flux:label>{{ __('Website') }}</flux:label>
<flux:input wire:model="website" type="url" placeholder="{{ __('https://www.firma.de') }}" icon="globe-alt" />
<flux:error name="website" />
</flux:field>
</div>
</article>
<flux:field>
<flux:label>{{ __('Firmenname') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="company_name" placeholder="{{ __('Vollständiger Firmenname...') }}" />
<flux:error name="company_name" />
</flux:field>
<flux:field>
<flux:label>{{ __('Beschreibung') }}</flux:label>
<flux:textarea wire:model="description" rows="4" placeholder="{{ __('Kurze Beschreibung der Firma (optional)...') }}" />
<flux:error name="description" />
</flux:field>
<div class="grid gap-4 sm:grid-cols-2">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Adresse') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:field>
<flux:label>{{ __('E-Mail') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="email" type="email" placeholder="{{ __('kontakt@firma.de') }}" icon="envelope" />
<flux:error name="email" />
<flux:label>{{ __('Straße & Hausnummer') }}</flux:label>
<flux:input wire:model="street" placeholder="{{ __('Musterstraße 123') }}" />
<flux:error name="street" />
</flux:field>
<div class="grid gap-4 sm:grid-cols-3">
<flux:field>
<flux:label>{{ __('PLZ') }}</flux:label>
<flux:input wire:model="zip" placeholder="{{ __('12345') }}" />
<flux:error name="zip" />
</flux:field>
<flux:field class="sm:col-span-2">
<flux:label>{{ __('Stadt') }}</flux:label>
<flux:input wire:model="city" placeholder="{{ __('Berlin') }}" />
<flux:error name="city" />
</flux:field>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Bundesland / Region') }}</flux:label>
<flux:input wire:model="state" placeholder="{{ __('Bayern') }}" />
<flux:error name="state" />
</flux:field>
<flux:field>
<flux:label>{{ __('Land') }} <span class="text-[color:var(--color-err)]">*</span></flux:label>
<flux:select wire:model="country">
@foreach ($countries as $country)
<option value="{{ $country['code'] }}">{{ $country['name'] }}</option>
@endforeach
</flux:select>
<flux:error name="country" />
</flux:field>
</div>
</div>
</article>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Rechtliche Daten') }}</span>
</div>
<div class="p-5 grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Steuernummer / USt-IdNr.') }}</flux:label>
<flux:input wire:model="tax_id" placeholder="{{ __('DE123456789') }}" />
<flux:error name="tax_id" />
</flux:field>
<flux:field>
<flux:label>{{ __('Telefon') }}</flux:label>
<flux:input wire:model="phone" type="tel" placeholder="{{ __('+49 123 456789') }}" icon="phone" />
<flux:error name="phone" />
<flux:label>{{ __('Handelsregisternummer') }}</flux:label>
<flux:input wire:model="registration_number" placeholder="{{ __('HRB 12345') }}" />
<flux:error name="registration_number" />
</flux:field>
</div>
</article>
<flux:field>
<flux:label>{{ __('Website') }}</flux:label>
<flux:input wire:model="website" type="url" placeholder="{{ __('https://www.firma.de') }}" icon="globe-alt" />
<flux:error name="website" />
</flux:field>
</div>
</flux:card>
{{-- Adresse --}}
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Adresse') }}</flux:heading>
<div class="space-y-4">
<flux:field>
<flux:label>{{ __('Straße & Hausnummer') }}</flux:label>
<flux:input wire:model="street" placeholder="{{ __('Musterstraße 123') }}" />
<flux:error name="street" />
</flux:field>
<div class="grid gap-4 sm:grid-cols-3">
<flux:field>
<flux:label>{{ __('PLZ') }}</flux:label>
<flux:input wire:model="zip" placeholder="{{ __('12345') }}" />
<flux:error name="zip" />
</flux:field>
<flux:field class="sm:col-span-2">
<flux:label>{{ __('Stadt') }}</flux:label>
<flux:input wire:model="city" placeholder="{{ __('Berlin') }}" />
<flux:error name="city" />
</flux:field>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Logo & Status') }}</span>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<div class="p-5 space-y-4">
<flux:field>
<flux:label>{{ __('Bundesland / Region') }}</flux:label>
<flux:input wire:model="state" placeholder="{{ __('Bayern') }}" />
<flux:error name="state" />
</flux:field>
<flux:label>{{ __('Firmenlogo') }}</flux:label>
<flux:input type="file" wire:model="logo" accept="image/jpeg,image/png,image/webp,image/gif" />
<flux:description>{{ __('Maximal 4 MB. Varianten (sq/wide) werden automatisch generiert.') }}</flux:description>
<flux:error name="logo" />
<flux:field>
<flux:label>{{ __('Land') }} <span class="text-red-500">*</span></flux:label>
<flux:select wire:model="country">
@foreach($countries as $country)
<option value="{{ $country['code'] }}">{{ $country['name'] }}</option>
@endforeach
</flux:select>
<flux:error name="country" />
</flux:field>
</div>
</div>
</flux:card>
{{-- Rechtliche Daten --}}
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Rechtliche Daten') }}</flux:heading>
<div class="grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Steuernummer / USt-IdNr.') }}</flux:label>
<flux:input wire:model="tax_id" placeholder="{{ __('DE123456789') }}" />
<flux:error name="tax_id" />
</flux:field>
<flux:field>
<flux:label>{{ __('Handelsregisternummer') }}</flux:label>
<flux:input wire:model="registration_number" placeholder="{{ __('HRB 12345') }}" />
<flux:error name="registration_number" />
</flux:field>
</div>
</flux:card>
{{-- Logo & Status --}}
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Logo & Status') }}</flux:heading>
<div class="space-y-4">
<flux:field>
<flux:label>{{ __('Firmenlogo') }}</flux:label>
<flux:input type="file" wire:model="logo" accept="image/jpeg,image/png,image/webp,image/gif" />
<flux:description>{{ __('Maximal 4 MB. Varianten (sq/wide) werden automatisch generiert.') }}</flux:description>
<flux:error name="logo" />
@if ($logo)
<div class="mt-4">
<flux:text class="text-sm text-zinc-500 mb-2">{{ __('Neues Logo (Vorschau):') }}</flux:text>
<img src="{{ $logo->temporaryUrl() }}" width="128" height="128" class="h-32 max-h-32 w-32 max-w-32 rounded-lg border border-zinc-200 object-contain dark:border-zinc-700">
</div>
@elseif($current_logo_url && ! $remove_logo)
<div class="mt-4 flex items-center gap-3">
<div>
<flux:text class="text-sm text-zinc-500 mb-2">{{ __('Aktuelles Logo:') }}</flux:text>
<img src="{{ $current_logo_url }}" width="128" height="128" class="h-32 max-h-32 w-32 max-w-32 rounded-lg border border-zinc-200 object-contain dark:border-zinc-700">
@if ($logo)
<div class="mt-4">
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-2">
{{ __('Neues Logo (Vorschau):') }}
</div>
<img src="{{ $logo->temporaryUrl() }}" width="128" height="128"
class="h-32 max-h-32 w-32 max-w-32 rounded-[6px] border border-[color:var(--color-bg-rule)] object-contain bg-[color:var(--color-bg-elev)]">
</div>
<flux:button type="button" size="sm" variant="ghost" wire:click="$set('remove_logo', true)">
{{ __('Logo entfernen') }}
</flux:button>
</div>
@elseif($remove_logo)
<flux:callout color="amber" icon="exclamation-triangle" class="mt-4">
{{ __('Logo wird beim Speichern entfernt.') }}
<flux:button type="button" size="sm" variant="ghost" wire:click="$set('remove_logo', false)">
{{ __('Rückgängig') }}
</flux:button>
</flux:callout>
@endif
</flux:field>
@elseif ($current_logo_url && ! $remove_logo)
<div class="mt-4 flex items-center gap-4">
<div>
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-2">
{{ __('Aktuelles Logo:') }}
</div>
<img src="{{ $current_logo_url }}" width="128" height="128"
class="h-32 max-h-32 w-32 max-w-32 rounded-[6px] border border-[color:var(--color-bg-rule)] object-contain bg-[color:var(--color-bg-elev)]">
</div>
<flux:button type="button" size="sm" variant="ghost" wire:click="$set('remove_logo', true)">
{{ __('Logo entfernen') }}
</flux:button>
</div>
@elseif ($remove_logo)
<div class="mt-4 px-4 py-3 rounded-[5px] border text-[12.5px] flex items-start gap-3
bg-[color:var(--color-warn-soft)] border-[color:var(--color-warn)]/30 text-[color:var(--color-accent-deep)]">
<flux:icon.exclamation-triangle class="size-[16px] flex-shrink-0 mt-0.5" />
<div class="flex-1">
{{ __('Logo wird beim Speichern entfernt.') }}
</div>
<flux:button type="button" size="sm" variant="ghost" wire:click="$set('remove_logo', false)">
{{ __('Rückgängig') }}
</flux:button>
</div>
@endif
</flux:field>
<div class="flex gap-6">
<flux:checkbox wire:model="is_verified" label="{{ __('Firma ist verifiziert') }}" />
<flux:checkbox wire:model="is_active" label="{{ __('Firma ist aktiv') }}" />
<div class="flex gap-6 pt-2 border-t border-[color:var(--color-bg-rule)]">
<flux:checkbox wire:model="is_verified" label="{{ __('Firma ist verifiziert') }}" />
<flux:checkbox wire:model="is_active" label="{{ __('Firma ist aktiv') }}" />
</div>
</div>
</div>
</flux:card>
</article>
{{-- Aktionen --}}
<flux:card>
<div class="flex justify-between">
<flux:modal.trigger name="confirm-company-deletion">
<flux:button
variant="danger"
icon="trash"
type="button"
x-data=""
x-on:click.prevent="$dispatch('open-modal', 'confirm-company-deletion')"
>
{{ __('Löschen') }}
</flux:button>
</flux:modal.trigger>
<div class="flex gap-3">
<flux:button variant="ghost" href="{{ route('admin.companies.index') }}" wire:navigate>
{{ __('Abbrechen') }}
</flux:button>
<flux:button type="submit" variant="primary">
{{ __('Änderungen speichern') }}
</flux:button>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Aktionen') }}</span>
</div>
</div>
</flux:card>
<div class="p-5 flex justify-between items-center gap-3 flex-wrap">
<flux:modal.trigger name="confirm-company-deletion">
<flux:button
variant="danger"
icon="trash"
type="button"
x-data=""
x-on:click.prevent="$dispatch('open-modal', 'confirm-company-deletion')"
>
{{ __('Löschen') }}
</flux:button>
</flux:modal.trigger>
<div class="flex gap-3">
<flux:button variant="ghost" href="{{ route('admin.companies.index') }}" wire:navigate>
{{ __('Abbrechen') }}
</flux:button>
<flux:button type="submit" variant="primary">
{{ __('Änderungen speichern') }}
</flux:button>
</div>
</div>
</article>
</form>
<flux:modal name="confirm-company-deletion" class="max-w-lg">

View file

@ -296,43 +296,51 @@ new #[Layout('components.layouts.app'), Title('Firmen')] class extends Component
}
}; ?>
<div class="space-y-6">
{{-- Statistiken --}}
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Gesamt') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['total'] }}</flux:text>
</div>
<flux:icon.building-office class="size-8 text-blue-500" />
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Stammdaten · Firmen') }}</span>
</div>
</flux:card>
<h1 class="text-[34px] font-bold tracking-[-0.7px] leading-[1.1] m-0 text-[color:var(--color-ink)]">
{{ __('Firmen') }}
</h1>
<p class="text-[13.5px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Übersicht aller Firmen beider Portale, mit Filter, Datenqualität und Schnellaktionen.') }}
</p>
</div>
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Aktiv') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['active'] }}</flux:text>
</div>
<flux:icon.check-circle class="size-8 text-green-500" />
</div>
</flux:card>
<div class="flex items-center gap-3 flex-shrink-0">
<flux:button icon="plus" variant="primary" href="{{ route('admin.companies.create') }}" wire:navigate>
{{ __('Neue Firma') }}
</flux:button>
</div>
</header>
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Inaktiv') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['inactive'] }}</flux:text>
</div>
<flux:icon.x-circle class="size-8 text-red-500" />
</div>
</flux:card>
</div>
{{-- ============== KPI-Reihe ============== --}}
<section class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<x-portal.stat-card variant="primary" :label="__('Gesamt')" :value="number_format($stats['total'])">
<x-slot:meta>{{ __('alle Portale') }}</x-slot:meta>
<x-slot:trend>{{ __('Stammdaten-Basis') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="ok" :label="__('Aktiv')" :value="number_format($stats['active'])">
<x-slot:meta>{{ __('produktiv') }}</x-slot:meta>
<x-slot:trend>{{ __('für PMs nutzbar') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Inaktiv')" :value="number_format($stats['inactive'])">
<x-slot:meta>{{ __('pausiert') }}</x-slot:meta>
<x-slot:trend>{{ __('archiviert oder geblockt') }}</x-slot:trend>
</x-portal.stat-card>
</section>
{{-- Filter & Actions --}}
<flux:card>
<div class="flex flex-col gap-4">
{{-- ============== FILTER-PANEL ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Filter & Suche') }}</span>
</div>
<div class="p-5 flex flex-col gap-4">
<div class="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-7">
<flux:input wire:model.live.debounce.300ms="search" placeholder="{{ __('Suchen...') }}"
icon="magnifying-glass" class="xl:col-span-2" />
@ -436,17 +444,17 @@ new #[Layout('components.layouts.app'), Title('Firmen')] class extends Component
/>
</div>
</div>
<div class="flex justify-end">
<flux:button icon="plus" href="{{ route('admin.companies.create') }}" wire:navigate>
{{ __('Neue Firma') }}
</flux:button>
</div>
</div>
</flux:card>
</article>
{{-- Tabelle --}}
<flux:card class="overflow-hidden">
{{-- ============== TABELLE-PANEL ============== --}}
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Alle Firmen') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __(':count Einträge', ['count' => $companies->count()]) }}
</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Aktionen') }}</flux:table.column>
@ -481,40 +489,43 @@ new #[Layout('components.layouts.app'), Title('Firmen')] class extends Component
</flux:table.cell>
<flux:table.cell>
<div class="flex items-center gap-3">
@if($logoUrl)
<img src="{{ $logoUrl }}" width="36" height="36" class="h-9 max-h-9 w-9 max-w-9 rounded-md border border-zinc-200 object-contain dark:border-zinc-700" alt="{{ $company->name }}">
@if ($logoUrl)
<img src="{{ $logoUrl }}" width="36" height="36"
class="h-9 max-h-9 w-9 max-w-9 rounded-[4px] border border-[color:var(--color-bg-rule)] object-contain bg-[color:var(--color-bg-elev)]"
alt="{{ $company->name }}">
@else
<div class="flex h-9 w-9 items-center justify-center rounded-md border border-zinc-200 bg-zinc-100 dark:border-zinc-700 dark:bg-zinc-800">
<flux:icon.building-office class="size-5 text-zinc-400" />
<div class="flex h-9 w-9 items-center justify-center rounded-[4px]
border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)]">
<flux:icon.building-office class="size-5 text-[color:var(--color-ink-3)]" />
</div>
@endif
<flux:text weight="semibold"> {{ \Illuminate\Support\Str::limit($company->name, 60) }}
</flux:text>
<span class="text-[13px] font-semibold text-[color:var(--color-ink)]">
{{ \Illuminate\Support\Str::limit($company->name, 60) }}
</span>
</div>
</flux:table.cell>
<flux:table.cell>
<div class="space-y-1">
<flux:text class="text-sm">{{ $company->email ?: __('Keine E-Mail') }}</flux:text>
<flux:text class="text-sm text-zinc-500">{{ $company->phone ?: __('Kein Telefon') }}
</flux:text>
<div class="space-y-0.5">
<div class="text-[12.5px] text-[color:var(--color-ink)]">
{{ $company->email ?: __('Keine E-Mail') }}
</div>
<div class="text-[12px] text-[color:var(--color-ink-3)]">
{{ $company->phone ?: __('Kein Telefon') }}
</div>
</div>
</flux:table.cell>
<flux:table.cell>
@if ($company->is_active)
<flux:badge color="green" size="sm" icon="check">{{ __('Aktiv') }}
</flux:badge>
<span class="badge ok dot">{{ __('Aktiv') }}</span>
@else
<flux:badge color="red" size="sm" icon="x-mark">{{ __('Inaktiv') }}
</flux:badge>
<span class="badge err dot">{{ __('Inaktiv') }}</span>
@endif
</flux:table.cell>
<flux:table.cell>
<flux:badge color="{{ $this->portalBadgeColor($company->portal) }}" size="sm">
{{ $company->portal?->label() ?? __('Unbekannt') }}
</flux:badge>
<span class="badge hub">{{ $company->portal?->label() ?? __('Unbekannt') }}</span>
</flux:table.cell>
<flux:table.cell>
@ -528,7 +539,7 @@ new #[Layout('components.layouts.app'), Title('Firmen')] class extends Component
{{ $company->press_releases_count }} {{ __('PMs') }}
</flux:button>
@else
<flux:badge color="zinc" size="sm">0</flux:badge>
<span class="badge hub">0</span>
@endif
</flux:table.cell>
@ -543,31 +554,39 @@ new #[Layout('components.layouts.app'), Title('Firmen')] class extends Component
{{ $company->contacts_count }} {{ __('Kontakte') }}
</flux:button>
@else
<flux:badge color="zinc" size="sm">0</flux:badge>
<span class="badge hub">0</span>
@endif
</flux:table.cell>
<flux:table.cell>
<flux:text class="text-sm text-zinc-500">
<span class="text-[12px] text-[color:var(--color-ink-3)]">
{{ $company->created_at?->format('d.m.Y H:i') ?? '' }}
</flux:text>
</span>
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="8">
<div class="flex flex-col items-center justify-center py-12">
<flux:icon.building-office class="size-12 text-zinc-400 dark:text-zinc-600" />
<flux:text class="mt-4 text-zinc-500">{{ __('Keine Firmen gefunden') }}</flux:text>
<div class="flex flex-col items-center justify-center py-12 text-center">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-4
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.building-office class="size-6" />
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Keine Firmen gefunden') }}
</div>
<p class="text-[12px] text-[color:var(--color-ink-3)] max-w-md m-0">
{{ __('Passen Sie die Filter an oder legen Sie eine neue Firma an.') }}
</p>
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
<div class="border-t border-zinc-200 p-4 dark:border-zinc-700">
<div class="border-t border-[color:var(--color-bg-rule)] p-4">
{{ $companies->links() }}
</div>
</flux:card>
</article>
</div>

View file

@ -192,129 +192,198 @@ new #[Layout('components.layouts.app'), Title('Firma anzeigen')] class extends C
}
}; ?>
<div class="space-y-6">
<flux:card>
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div class="flex gap-4">
@php($logoUrl = $company->logoUrl())
<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
@if (session('info'))
<div class="px-4 py-3 rounded-[5px] border text-[12.5px]
bg-[color:var(--color-hub-soft)] border-[color:var(--color-hub-soft-2)] text-[color:var(--color-ink-2)]">
{{ session('info') }}
</div>
@endif
@if($logoUrl)
<img src="{{ $logoUrl }}" width="80" height="80" class="h-20 max-h-20 w-20 max-w-20 rounded-lg border border-zinc-200 object-contain dark:border-zinc-700" alt="{{ $company->name }}">
{{-- ============== PAGE HEADER ============== --}}
@php($logoUrl = $company->logoUrl())
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Stammdaten · Firma') }}</span>
@if ($company->is_active)
<span class="badge ok">{{ __('Aktiv') }}</span>
@else
<div class="flex h-20 w-20 items-center justify-center rounded-lg border border-zinc-200 bg-zinc-100 dark:border-zinc-700 dark:bg-zinc-800">
<flux:icon.building-office class="size-10 text-zinc-400" />
<span class="badge err">{{ __('Inaktiv') }}</span>
@endif
<span class="badge hub">{{ $company->portal?->label() ?? __('Unbekannt') }}</span>
<span class="badge hub">ID {{ $company->id }}</span>
</div>
<div class="flex items-start gap-4">
@if ($logoUrl)
<img src="{{ $logoUrl }}" width="64" height="64"
class="h-16 max-h-16 w-16 max-w-16 rounded-[6px] border border-[color:var(--color-bg-rule)] object-contain bg-[color:var(--color-bg-elev)] flex-shrink-0"
alt="{{ $company->name }}">
@else
<div class="flex h-16 w-16 items-center justify-center rounded-[6px]
border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] flex-shrink-0">
<flux:icon.building-office class="size-7 text-[color:var(--color-ink-3)]" />
</div>
@endif
<div>
<flux:heading size="xl" class="mb-2">{{ $company->name }}</flux:heading>
<div class="flex flex-wrap gap-2">
@if($company->is_active)
<flux:badge color="green" size="sm">{{ __('Aktiv') }}</flux:badge>
@else
<flux:badge color="red" size="sm">{{ __('Inaktiv') }}</flux:badge>
@endif
<flux:badge color="{{ $this->portalBadgeColor($company->portal) }}" size="sm">{{ $company->portal?->label() ?? __('Unbekannt') }}</flux:badge>
<flux:text class="ml-2 text-sm text-zinc-500">ID: {{ $company->id }}</flux:text>
</div>
<div class="min-w-0">
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)] break-words">
{{ $company->name }}
</h1>
@if ($company->website)
<a href="{{ $company->website }}" target="_blank" rel="noopener"
class="text-[12.5px] text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)] mt-1 inline-block">
{{ $company->website }}
</a>
@endif
</div>
</div>
</div>
<div class="flex gap-2">
<flux:button icon="pencil" href="{{ route('admin.companies.edit', $company->id) }}" wire:navigate>
{{ __('Bearbeiten') }}
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.companies.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
@if (\Illuminate\Support\Facades\Route::has('admin.companies.contacts.create'))
<flux:button variant="ghost" icon="user-plus" href="{{ route('admin.companies.contacts.create', ['companyId' => $company->id]) }}" wire:navigate>
{{ __('Kontakt hinzufügen') }}
</flux:button>
@if (\Illuminate\Support\Facades\Route::has('admin.companies.contacts.create'))
<flux:button variant="ghost" icon="user-plus" href="{{ route('admin.companies.contacts.create', ['companyId' => $company->id]) }}" wire:navigate>
{{ __('Kontakt hinzufügen') }}
</flux:button>
@endif
</div>
</div>
</flux:card>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<flux:card>
<flux:text class="text-sm text-zinc-500">{{ __('Pressemitteilungen') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $company->press_releases_count }}</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-sm text-zinc-500">{{ __('Kontakte') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $company->contacts_count }}</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-sm text-zinc-500">{{ __('Verknüpfte Benutzer') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $company->users_count }}</flux:text>
</flux:card>
</div>
<flux:card>
<div class="flex gap-2">
<flux:button
:variant="$activeTab === 'overview' ? 'primary' : 'ghost'"
wire:click="setTab('overview')"
>
{{ __('Überblick') }}
</flux:button>
<flux:button
:variant="$activeTab === 'contacts' ? 'primary' : 'ghost'"
wire:click="setTab('contacts')"
>
{{ __('Kontakte') }}
@endif
<flux:button variant="primary" icon="pencil" href="{{ route('admin.companies.edit', $company->id) }}" wire:navigate>
{{ __('Bearbeiten') }}
</flux:button>
</div>
</flux:card>
</header>
@if($activeTab === 'overview')
{{-- ============== KPI-Reihe ============== --}}
<section class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<x-portal.stat-card variant="primary" :label="__('Pressemitteilungen')" :value="number_format($company->press_releases_count)">
<x-slot:meta>{{ __('insgesamt') }}</x-slot:meta>
<x-slot:trend>{{ __('Content-Output dieser Firma') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="ok" :label="__('Kontakte')" :value="number_format($company->contacts_count)">
<x-slot:meta>{{ __('Ansprechpartner') }}</x-slot:meta>
<x-slot:trend>{{ __('für PMs verfügbar') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Verknüpfte Benutzer')" :value="number_format($company->users_count)">
<x-slot:meta>{{ __('Owner & Co-Editors') }}</x-slot:meta>
<x-slot:trend>{{ __('Backend-Zugriff') }}</x-slot:trend>
</x-portal.stat-card>
</section>
{{-- ============== TABS ============== --}}
<nav class="flex items-center gap-2 border-b border-[color:var(--color-bg-rule)]">
<button type="button" wire:click="setTab('overview')"
@class([
'px-4 py-2.5 text-[12.5px] font-semibold border-b-2 transition-colors',
'border-[color:var(--color-hub)] text-[color:var(--color-ink)]' => $activeTab === 'overview',
'border-transparent text-[color:var(--color-ink-3)] hover:text-[color:var(--color-ink)]' => $activeTab !== 'overview',
])>
{{ __('Überblick') }}
</button>
<button type="button" wire:click="setTab('contacts')"
@class([
'px-4 py-2.5 text-[12.5px] font-semibold border-b-2 transition-colors',
'border-[color:var(--color-hub)] text-[color:var(--color-ink)]' => $activeTab === 'contacts',
'border-transparent text-[color:var(--color-ink-3)] hover:text-[color:var(--color-ink)]' => $activeTab !== 'contacts',
])>
{{ __('Kontakte') }}
</button>
</nav>
@if ($activeTab === 'overview')
<div class="grid gap-6 lg:grid-cols-2">
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Kontaktinformationen') }}</flux:heading>
<div class="space-y-2">
<flux:text>{{ $company->email ?: __('Keine E-Mail hinterlegt') }}</flux:text>
<flux:text>{{ $company->phone ?: __('Kein Telefon hinterlegt') }}</flux:text>
<flux:text>{{ $company->website ?: __('Keine Website hinterlegt') }}</flux:text>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Kontaktinformationen') }}</span>
</div>
</flux:card>
<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)]">{{ __('E-Mail') }}</dt>
<dd class="text-[color:var(--color-ink)] text-right break-all">{{ $company->email ?: __('Keine E-Mail hinterlegt') }}</dd>
</div>
<div class="flex justify-between gap-2">
<dt class="text-[color:var(--color-ink-3)]">{{ __('Telefon') }}</dt>
<dd class="text-[color:var(--color-ink)]">{{ $company->phone ?: __('Kein Telefon hinterlegt') }}</dd>
</div>
<div class="flex justify-between gap-2">
<dt class="text-[color:var(--color-ink-3)]">{{ __('Website') }}</dt>
<dd class="text-[color:var(--color-ink)] text-right break-all">{{ $company->website ?: __('Keine Website hinterlegt') }}</dd>
</div>
</dl>
</article>
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Adresse') }}</flux:heading>
<div class="space-y-2">
<flux:text>{{ $company->address ?: __('Keine Adresse hinterlegt') }}</flux:text>
<flux:text>{{ $company->country_code ?: __('Kein Land hinterlegt') }}</flux:text>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Adresse') }}</span>
</div>
</flux:card>
<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)]">{{ __('Anschrift') }}</dt>
<dd class="text-[color:var(--color-ink)] text-right whitespace-pre-line">{{ $company->address ?: __('Keine Adresse hinterlegt') }}</dd>
</div>
<div class="flex justify-between gap-2">
<dt class="text-[color:var(--color-ink-3)]">{{ __('Land') }}</dt>
<dd class="text-[color:var(--color-ink)]">{{ $company->country_code ?: __('Kein Land hinterlegt') }}</dd>
</div>
</dl>
</article>
<flux:card class="lg:col-span-2">
<div class="mb-4 flex items-center justify-between">
<flux:heading size="lg">{{ __('Aktuelle Pressemitteilungen') }}</flux:heading>
<article class="panel lg:col-span-2">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Aktuelle Pressemitteilungen') }}</span>
<flux:button size="sm" variant="ghost" href="{{ route('admin.press-releases.index', ['company' => $company->id]) }}" wire:navigate>
{{ __('Alle anzeigen') }}
</flux:button>
</div>
<div class="space-y-2">
@forelse($recentPressReleases as $pressRelease)
<a href="{{ route('admin.press-releases.show', $pressRelease->id) }}" wire:navigate class="block rounded-lg p-3 transition-colors hover:bg-zinc-50 dark:hover:bg-zinc-900">
<flux:text weight="medium">{{ $pressRelease->title ?? __('Ohne Titel') }}</flux:text>
<flux:text class="text-sm text-zinc-500">{{ $pressRelease->created_at?->format('d.m.Y') ?? '-' }}</flux:text>
<div class="divide-y divide-[color:var(--color-bg-rule)]">
@forelse ($recentPressReleases as $pressRelease)
<a href="{{ route('admin.press-releases.show', $pressRelease->id) }}" wire:navigate
class="flex items-center justify-between gap-3 px-5 py-3 hover:bg-[color:var(--color-bg-elev)] transition-colors">
<span class="text-[13px] font-semibold text-[color:var(--color-ink)] truncate">
{{ $pressRelease->title ?? __('Ohne Titel') }}
</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)] flex-shrink-0">
{{ $pressRelease->created_at?->format('d.m.Y') ?? '-' }}
</span>
</a>
@empty
<flux:text class="text-sm text-zinc-500">{{ __('Keine Pressemitteilungen vorhanden') }}</flux:text>
<div class="px-5 py-6 text-[12.5px] text-[color:var(--color-ink-3)]">
{{ __('Keine Pressemitteilungen vorhanden') }}
</div>
@endforelse
</div>
</flux:card>
</article>
</div>
@endif
@if($activeTab === 'contacts')
<flux:card>
<div class="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<flux:heading size="lg">{{ __('Ansprechpartner') }} ({{ $filteredContactsTotal }})</flux:heading>
<div class="flex w-full gap-2 sm:w-auto">
@if ($activeTab === 'contacts')
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Ansprechpartner') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ $filteredContactsTotal }} {{ __('Einträge') }}
</span>
</div>
<div class="p-5 space-y-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center">
<flux:input
wire:model.live.debounce.300ms="contactSearch"
placeholder="{{ __('Kontakte durchsuchen...') }}"
icon="magnifying-glass"
class="flex-1"
/>
@if (\Illuminate\Support\Facades\Route::has('admin.companies.contacts.create'))
<flux:button size="sm" icon="plus" href="{{ route('admin.companies.contacts.create', ['companyId' => $company->id]) }}" wire:navigate>
@ -322,79 +391,86 @@ new #[Layout('components.layouts.app'), Title('Firma anzeigen')] class extends C
</flux:button>
@endif
</div>
</div>
<div class="mb-4 rounded-lg border border-zinc-200 p-3 dark:border-zinc-700">
<flux:heading size="sm" class="mb-2">{{ __('Bestehenden Kontakt zuordnen') }}</flux:heading>
<flux:select
wire:model.live="selectedExistingContactId"
variant="combobox"
:filter="false"
placeholder="{{ __('Kontakt suchen und auswählen…') }}"
>
<x-slot name="input">
<flux:select.input
wire:model.live.debounce.300ms="contactLookup"
placeholder="{{ __('Name oder E-Mail…') }}"
/>
</x-slot>
@foreach($contactLookupResults as $lookupContact)
@php($lookupName = trim(($lookupContact->first_name ?? '').' '.($lookupContact->last_name ?? '')) ?: __('Kontakt ohne Name'))
<flux:select.option :value="$lookupContact->id" wire:key="lc-{{ $lookupContact->id }}">
{{ $lookupName }}
<span class="text-zinc-400">
@if($lookupContact->email)
· {{ $lookupContact->email }}
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-4">
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-2">
{{ __('Bestehenden Kontakt zuordnen') }}
</div>
<flux:select
wire:model.live="selectedExistingContactId"
variant="combobox"
:filter="false"
placeholder="{{ __('Kontakt suchen und auswählen…') }}"
>
<x-slot name="input">
<flux:select.input
wire:model.live.debounce.300ms="contactLookup"
placeholder="{{ __('Name oder E-Mail…') }}"
/>
</x-slot>
@foreach ($contactLookupResults as $lookupContact)
@php($lookupName = trim(($lookupContact->first_name ?? '').' '.($lookupContact->last_name ?? '')) ?: __('Kontakt ohne Name'))
<flux:select.option :value="$lookupContact->id" wire:key="lc-{{ $lookupContact->id }}">
{{ $lookupName }}
<span class="text-[color:var(--color-ink-3)]">
@if ($lookupContact->email)
· {{ $lookupContact->email }}
@endif
· {{ $lookupContact->company?->name ?? __('Unbekannte Firma') }}
</span>
</flux:select.option>
@endforeach
<x-slot name="empty">
<flux:select.option.empty>
@if (blank(trim($contactLookup)))
{{ __('Mindestens 1 Zeichen eingeben…') }}
@else
{{ __('Kein Kontakt gefunden.') }}
@endif
· {{ $lookupContact->company?->name ?? __('Unbekannte Firma') }}
</span>
</flux:select.option>
@endforeach
<x-slot name="empty">
<flux:select.option.empty>
@if(blank(trim($contactLookup)))
{{ __('Mindestens 1 Zeichen eingeben…') }}
@else
{{ __('Kein Kontakt gefunden.') }}
@endif
</flux:select.option.empty>
</x-slot>
</flux:select>
</div>
</flux:select.option.empty>
</x-slot>
</flux:select>
</div>
<div class="space-y-3">
@forelse($filteredContacts as $contact)
<div class="rounded-lg border border-zinc-200 p-3 dark:border-zinc-700">
<div class="flex items-start justify-between gap-3">
<div>
<div class="flex flex-wrap items-center gap-2">
<flux:text weight="semibold">
{{ trim(($contact->first_name ?? '').' '.($contact->last_name ?? '')) ?: __('Kontakt ohne Name') }}
</flux:text>
<flux:badge color="{{ $this->portalBadgeColor($contact->portal) }}" size="sm">
{{ $contact->portal?->label() ?? __('Unbekannt') }}
</flux:badge>
<div class="space-y-2">
@forelse ($filteredContacts as $contact)
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-2">
<span class="text-[13px] font-semibold text-[color:var(--color-ink)]">
{{ trim(($contact->first_name ?? '').' '.($contact->last_name ?? '')) ?: __('Kontakt ohne Name') }}
</span>
<span class="badge hub">{{ $contact->portal?->label() ?? __('Unbekannt') }}</span>
</div>
<div class="text-[12px] text-[color:var(--color-ink-3)] mt-0.5">
{{ $contact->responsibility ?: __('Keine Rolle hinterlegt') }}
</div>
@if ($contact->email)
<a href="mailto:{{ $contact->email }}"
class="text-[12px] text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)] mt-0.5 inline-block">
{{ $contact->email }}
</a>
@endif
</div>
<flux:text class="text-sm text-zinc-500">{{ $contact->responsibility ?: __('Keine Rolle hinterlegt') }}</flux:text>
@if($contact->email)
<flux:text class="text-sm text-blue-600 dark:text-blue-400">{{ $contact->email }}</flux:text>
@if (\Illuminate\Support\Facades\Route::has('admin.contacts.edit'))
<flux:button size="sm" variant="ghost" icon="pencil" href="{{ route('admin.contacts.edit', $contact->id) }}" wire:navigate />
@endif
</div>
@if (\Illuminate\Support\Facades\Route::has('admin.contacts.edit'))
<flux:button size="sm" variant="ghost" icon="pencil" href="{{ route('admin.contacts.edit', $contact->id) }}" wire:navigate />
@endif
</div>
</div>
@empty
<flux:text class="text-sm text-zinc-500">{{ __('Keine Kontakte gefunden') }}</flux:text>
@endforelse
</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)]">
{{ __('Keine Kontakte gefunden') }}
</div>
@endforelse
</div>
@if($filteredContactsTotal > $filteredContacts->count())
<flux:text class="mt-3 block text-xs text-zinc-500">
{{ __('Es werden die ersten :count Kontakte angezeigt. Bitte Suche eingrenzen, um weitere Treffer zu finden.', ['count' => $filteredContacts->count()]) }}
</flux:text>
@endif
</flux:card>
@if ($filteredContactsTotal > $filteredContacts->count())
<p class="text-[11.5px] text-[color:var(--color-ink-3)] m-0">
{{ __('Es werden die ersten :count Kontakte angezeigt. Bitte Suche eingrenzen, um weitere Treffer zu finden.', ['count' => $filteredContacts->count()]) }}
</p>
@endif
</div>
</article>
@endif
</div>

View file

@ -143,19 +143,37 @@ new #[Layout('components.layouts.app'), Title('Kontakt anlegen')] class extends
}
}; ?>
<form wire:submit="save" class="space-y-6">
<flux:card>
<flux:heading size="lg">{{ __('Kontakt anlegen') }}</flux:heading>
<flux:subheading>{{ __('Kontakt einer Firma zuordnen und Stammdaten erfassen.') }}</flux:subheading>
@if($isCompanyPrefilled && $companyId)
<flux:text class="mt-2 text-sm text-zinc-500">
{{ __('Firma wurde vorausgewählt. Du kannst sie bei Bedarf trotzdem ändern.') }}
</flux:text>
@endif
</flux:card>
<form wire:submit="save" class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Pressekontakte') }}</span>
@if ($isCompanyPrefilled && $companyId)
<span class="badge hub">{{ __('Firma vorausgewählt') }}</span>
@endif
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Kontakt anlegen') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Kontakt einer Firma zuordnen und Stammdaten erfassen.') }}
</p>
</div>
<flux:card>
<div class="grid gap-4 sm:grid-cols-2">
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.contacts.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</header>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Zuordnung') }}</span>
</div>
<div class="p-5 grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Firma') }}</flux:label>
@ -173,7 +191,7 @@ new #[Layout('components.layouts.app'), Title('Kontakt anlegen')] class extends
/>
</x-slot>
@foreach($companies as $company)
@foreach ($companies as $company)
<flux:select.option :value="$company->id" wire:key="{{ $company->id }}">
{{ $company->name }}
</flux:select.option>
@ -181,7 +199,7 @@ new #[Layout('components.layouts.app'), Title('Kontakt anlegen')] class extends
<x-slot name="empty">
<flux:select.option.empty>
@if(blank(trim($companySearch)))
@if (blank(trim($companySearch)))
{{ __('Mindestens 1 Zeichen eingeben…') }}
@else
{{ __('Keine Firma gefunden.') }}
@ -196,24 +214,25 @@ new #[Layout('components.layouts.app'), Title('Kontakt anlegen')] class extends
<flux:field>
<flux:label>{{ __('Portal') }}</flux:label>
<flux:select wire:model="portal">
@foreach($portalOptions as $portalOption)
@foreach ($portalOptions as $portalOption)
<option value="{{ $portalOption->value }}">{{ $portalOption->label() }}</option>
@endforeach
</flux:select>
<flux:error name="portal" />
</flux:field>
</div>
</flux:card>
</article>
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Kontaktdaten') }}</flux:heading>
<div class="grid gap-4 sm:grid-cols-2">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Kontaktdaten') }}</span>
</div>
<div class="p-5 grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Anrede') }}</flux:label>
<flux:select wire:model="salutationKey">
<option value="">{{ __('Bitte wählen') }}</option>
@foreach($salutations as $key => $labels)
@foreach ($salutations as $key => $labels)
<option value="{{ $key }}">{{ $labels['de'] ?? $key }}</option>
@endforeach
</flux:select>
@ -260,10 +279,10 @@ new #[Layout('components.layouts.app'), Title('Kontakt anlegen')] class extends
<flux:error name="fax" />
</flux:field>
</div>
</flux:card>
</article>
<flux:card>
<div class="flex justify-end gap-3">
<article class="panel">
<div class="p-5 flex justify-end gap-3">
<flux:button variant="ghost" href="{{ route('admin.contacts.index') }}" wire:navigate>
{{ __('Abbrechen') }}
</flux:button>
@ -271,5 +290,5 @@ new #[Layout('components.layouts.app'), Title('Kontakt anlegen')] class extends
{{ __('Kontakt anlegen') }}
</flux:button>
</div>
</flux:card>
</article>
</form>

View file

@ -178,22 +178,34 @@ new #[Layout('components.layouts.app'), Title('Kontakt bearbeiten')] class exten
}
}; ?>
<div class="space-y-6">
<form wire:submit="save" class="space-y-6">
<flux:card>
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<flux:heading size="lg">{{ __('Kontakt bearbeiten') }}</flux:heading>
<flux:subheading>ID: {{ $id }}</flux:subheading>
<div class="space-y-8">
<form wire:submit="save" class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Pressekontakte') }}</span>
<span class="badge hub">ID #{{ $id }}</span>
<span class="badge hub">{{ $this->currentPortalLabel() }}</span>
</div>
<flux:badge color="{{ $this->currentPortalBadgeColor() }}" size="sm">
{{ $this->currentPortalLabel() }}
</flux:badge>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Kontakt bearbeiten') }}
</h1>
</div>
</flux:card>
<flux:card>
<div class="grid gap-4 sm:grid-cols-2">
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.contacts.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</header>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Zuordnung') }}</span>
</div>
<div class="p-5 grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Firma') }}</flux:label>
@ -211,7 +223,7 @@ new #[Layout('components.layouts.app'), Title('Kontakt bearbeiten')] class exten
/>
</x-slot>
@foreach($companies as $company)
@foreach ($companies as $company)
<flux:select.option :value="$company->id" wire:key="{{ $company->id }}">
{{ $company->name }}
</flux:select.option>
@ -219,7 +231,7 @@ new #[Layout('components.layouts.app'), Title('Kontakt bearbeiten')] class exten
<x-slot name="empty">
<flux:select.option.empty>
@if(blank(trim($companySearch)))
@if (blank(trim($companySearch)))
{{ __('Mindestens 1 Zeichen eingeben…') }}
@else
{{ __('Keine Firma gefunden.') }}
@ -234,24 +246,25 @@ new #[Layout('components.layouts.app'), Title('Kontakt bearbeiten')] class exten
<flux:field>
<flux:label>{{ __('Portal') }}</flux:label>
<flux:select wire:model="portal">
@foreach($portalOptions as $portalOption)
@foreach ($portalOptions as $portalOption)
<option value="{{ $portalOption->value }}">{{ $portalOption->label() }}</option>
@endforeach
</flux:select>
<flux:error name="portal" />
</flux:field>
</div>
</flux:card>
</article>
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Kontaktdaten') }}</flux:heading>
<div class="grid gap-4 sm:grid-cols-2">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Kontaktdaten') }}</span>
</div>
<div class="p-5 grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Anrede') }}</flux:label>
<flux:select wire:model="salutationKey">
<option value="">{{ __('Bitte wählen') }}</option>
@foreach($salutations as $key => $labels)
@foreach ($salutations as $key => $labels)
<option value="{{ $key }}">{{ $labels['de'] ?? $key }}</option>
@endforeach
</flux:select>
@ -298,10 +311,13 @@ new #[Layout('components.layouts.app'), Title('Kontakt bearbeiten')] class exten
<flux:error name="fax" />
</flux:field>
</div>
</flux:card>
</article>
<flux:card>
<div class="flex justify-between">
<article class="panel" style="border-left:3px solid var(--color-err);">
<div class="panel-head">
<span class="section-eyebrow" style="color:var(--color-err);">{{ __('Danger Zone & Aktionen') }}</span>
</div>
<div class="p-5 flex justify-between flex-wrap gap-3">
<flux:modal.trigger name="confirm-contact-deletion">
<flux:button
variant="danger"
@ -322,7 +338,7 @@ new #[Layout('components.layouts.app'), Title('Kontakt bearbeiten')] class exten
</flux:button>
</div>
</div>
</flux:card>
</article>
</form>
<flux:modal name="confirm-contact-deletion" class="max-w-lg">

View file

@ -391,55 +391,73 @@ new #[Layout('components.layouts.app'), Title('Kontakte')] class extends Compone
}
}; ?>
<div class="space-y-6">
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Pressekontakte') }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Kontakte') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Pressekontakte aller Firmen über alle Portale hinweg.') }}
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
@if (\Illuminate\Support\Facades\Route::has('admin.contacts.create'))
<flux:button variant="primary" icon="plus" href="{{ route('admin.contacts.create') }}" wire:navigate>
{{ __('Neuer Kontakt') }}
</flux:button>
@else
<flux:button variant="primary" icon="plus" disabled>
{{ __('Neuer Kontakt') }}
</flux:button>
@endif
</div>
</header>
@if ($notification)
<div x-data="{ show: true }" x-init="setTimeout(() => show = false, 3000)" x-show="show" x-transition
class="rounded-md px-4 py-3 text-sm border
{{ $notificationType === 'error'
? 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 text-red-800 dark:text-red-300'
: 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800 text-green-800 dark:text-green-300' }}">
@class([
'px-4 py-3 rounded-[5px] border text-[12.5px] flex items-center gap-2',
'bg-[color:var(--color-err-soft)] border-[color:var(--color-err)]/30 text-[color:var(--color-ink-2)]' => $notificationType === 'error',
'bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]' => $notificationType !== 'error',
])>
@if ($notificationType === 'error')
<flux:icon.exclamation-triangle class="size-[16px] flex-shrink-0" />
@else
<flux:icon.check-circle class="size-[16px] flex-shrink-0" />
@endif
{{ $notification }}
</div>
@endif
{{-- Statistiken --}}
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Gesamt') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['total'] }}</flux:text>
</div>
<flux:icon.user-group class="size-8 text-blue-500" />
</div>
</flux:card>
{{-- ============== KPI-Reihe ============== --}}
<section class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<x-portal.stat-card variant="primary" :label="__('Gesamt')" :value="number_format($stats['total'])">
<x-slot:meta>{{ __('Pressekontakte') }}</x-slot:meta>
<x-slot:trend>{{ __('alle Portale') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="ok" :label="__('Firmen mit Kontakten')" :value="number_format($stats['companies_with_contacts'])">
<x-slot:meta>{{ __('aktiv versorgt') }}</x-slot:meta>
<x-slot:trend>{{ __('mind. ein Kontakt') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Ø pro Firma')" :value="number_format($stats['avg_per_company'], 1)">
<x-slot:meta>{{ __('Pflegegrad') }}</x-slot:meta>
<x-slot:trend>{{ __('Kontakte / Firma') }}</x-slot:trend>
</x-portal.stat-card>
</section>
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Firmen mit Kontakten') }}
</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['companies_with_contacts'] }}</flux:text>
</div>
<flux:icon.building-office class="size-8 text-green-500" />
</div>
</flux:card>
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Ø pro Firma') }}</flux:text>
<flux:text size="xl" weight="bold">{{ number_format($stats['avg_per_company'], 1) }}
</flux:text>
</div>
<flux:icon.chart-bar class="size-8 text-purple-500" />
</div>
</flux:card>
</div>
{{-- Filter & Actions --}}
<flux:card>
<div class="flex flex-col gap-4">
{{-- ============== FILTER-PANEL ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Filter & Suche') }}</span>
</div>
<div class="p-5 flex flex-col gap-4">
<div class="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
<flux:input wire:model.live.debounce.300ms="search"
placeholder="{{ __('Name, Email oder Firma suchen...') }}" icon="magnifying-glass" class="flex-1" />
@ -527,49 +545,51 @@ new #[Layout('components.layouts.app'), Title('Kontakte')] class extends Compone
<option value="{{ $portalOption->value }}">{{ $portalOption->label() }}</option>
@endforeach
</flux:select>
@if (\Illuminate\Support\Facades\Route::has('admin.contacts.create'))
<flux:button icon="plus" href="{{ route('admin.contacts.create') }}" wire:navigate>
{{ __('Neuer Kontakt') }}
</flux:button>
@else
<flux:button icon="plus" disabled>
{{ __('Neuer Kontakt') }}
</flux:button>
@endif
</div>
</div>
</flux:card>
</article>
<flux:card>
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div class="flex flex-1 gap-3">
<flux:input wire:model="presetName" placeholder="{{ __('Neues Preset speichern...') }}"
class="flex-1" />
<flux:button wire:click="savePreset" variant="subtle" icon="bookmark">
{{ __('Preset speichern') }}
</flux:button>
</div>
<div class="flex gap-3">
<flux:select wire:model="selectedPresetId" class="w-64">
<option value="">{{ __('Preset auswählen') }}</option>
@foreach ($presets as $preset)
<option value="{{ $preset->id }}">
{{ $preset->name }}{{ $preset->is_default ? ' (Standard)' : '' }}
</option>
@endforeach
</flux:select>
<flux:button wire:click="applyPreset" variant="ghost">{{ __('Anwenden') }}</flux:button>
<flux:button wire:click="setDefaultPreset" variant="ghost">{{ __('Als Standard') }}</flux:button>
<flux:button wire:click="deletePreset" variant="danger">{{ __('Löschen') }}</flux:button>
</div>
{{-- ============== PRESET-PANEL ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Filter-Presets') }}</span>
</div>
<flux:error name="presetName" class="mt-3" />
</flux:card>
<div class="p-5 space-y-3">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div class="flex flex-1 gap-3">
<flux:input wire:model="presetName" placeholder="{{ __('Neues Preset speichern...') }}"
class="flex-1" />
<flux:button wire:click="savePreset" variant="ghost" icon="bookmark">
{{ __('Preset speichern') }}
</flux:button>
</div>
{{-- Tabelle --}}
<flux:card class="overflow-hidden">
<div class="flex gap-2 flex-wrap">
<flux:select wire:model="selectedPresetId" class="w-64">
<option value="">{{ __('Preset auswählen') }}</option>
@foreach ($presets as $preset)
<option value="{{ $preset->id }}">
{{ $preset->name }}{{ $preset->is_default ? ' (Standard)' : '' }}
</option>
@endforeach
</flux:select>
<flux:button wire:click="applyPreset" variant="ghost">{{ __('Anwenden') }}</flux:button>
<flux:button wire:click="setDefaultPreset" variant="ghost">{{ __('Als Standard') }}</flux:button>
<flux:button wire:click="deletePreset" variant="danger">{{ __('Löschen') }}</flux:button>
</div>
</div>
<flux:error name="presetName" />
</div>
</article>
{{-- ============== TABELLE ============== --}}
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Alle Kontakte') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __(':count Einträge', ['count' => $contacts->count()]) }}
</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Aktionen') }}</flux:table.column>
@ -614,43 +634,40 @@ new #[Layout('components.layouts.app'), Title('Kontakte')] class extends Compone
</div>
</flux:table.cell>
<flux:table.cell>
<div>
<flux:text weight="semibold truncate">
{{ $contactDisplayName }}
</flux:text>
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] truncate">
{{ $contactDisplayName }}
</div>
</flux:table.cell>
<flux:table.cell>
<div class="space-y-1">
<flux:text class="text-sm">
<div class="space-y-0.5">
<div class="text-[12.5px]">
<a href="mailto:{{ $contact->email }}"
class="text-blue-600 hover:underline dark:text-blue-400">
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
{{ $contact->email ?: __('Keine E-Mail') }}
</a>
</flux:text>
<flux:text class="text-sm text-zinc-500">{{ $contact->phone ?: __('Kein Telefon') }}
</flux:text>
</div>
<div class="text-[12px] text-[color:var(--color-ink-3)]">
{{ $contact->phone ?: __('Kein Telefon') }}
</div>
</div>
</flux:table.cell>
<flux:table.cell>
@if ($contact->company && \Illuminate\Support\Facades\Route::has('admin.companies.show'))
<a href="{{ route('admin.companies.show', $contact->company_id) }}" wire:navigate
class="text-blue-600 hover:underline dark:text-blue-400">
class="text-[12.5px] text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
{{ \Illuminate\Support\Str::limit($contact->company->name, 60) }}
</a>
@else
<flux:text>
<span class="text-[12.5px] text-[color:var(--color-ink)]">
{{ \Illuminate\Support\Str::limit($contact->company?->name ?? __('Unbekannte Firma'), 80) }}
</flux:text>
</span>
@endif
</flux:table.cell>
<flux:table.cell>
<flux:badge color="{{ $this->portalBadgeColor($contact->portal) }}" size="sm">
{{ $contact->portal?->label() ?? __('Unbekannt') }}
</flux:badge>
<span class="badge hub">{{ $contact->portal?->label() ?? __('Unbekannt') }}</span>
</flux:table.cell>
<flux:table.cell>
@ -664,14 +681,14 @@ new #[Layout('components.layouts.app'), Title('Kontakte')] class extends Compone
{{ $contact->press_releases_count }} {{ __('PMs') }}
</flux:button>
@else
<flux:badge color="zinc" size="sm">0</flux:badge>
<span class="badge dot">0</span>
@endif
</flux:table.cell>
<flux:table.cell>
<flux:text class="text-sm text-zinc-500">
<span class="text-[12px] text-[color:var(--color-ink-3)]">
{{ $contact->created_at?->format('d.m.Y H:i') ?? '-' }}
</flux:text>
</span>
</flux:table.cell>
<flux:table.cell>
@ -712,9 +729,14 @@ new #[Layout('components.layouts.app'), Title('Kontakte')] class extends Compone
@empty
<flux:table.row>
<flux:table.cell colspan="8">
<div class="flex flex-col items-center justify-center py-12">
<flux:icon.user-group class="size-12 text-zinc-400 dark:text-zinc-600" />
<flux:text class="mt-4 text-zinc-500">{{ __('Keine Kontakte gefunden') }}</flux:text>
<div class="flex flex-col items-center justify-center px-4 py-10 text-center">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-3
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.user-group class="size-6" />
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Keine Kontakte gefunden') }}
</div>
</div>
</flux:table.cell>
</flux:table.row>
@ -722,8 +744,8 @@ new #[Layout('components.layouts.app'), Title('Kontakte')] class extends Compone
</flux:table.rows>
</flux:table>
<div class="border-t border-zinc-200 p-4 dark:border-zinc-700">
<div class="border-t border-[color:var(--color-bg-rule)] p-4">
{{ $contacts->links() }}
</div>
</flux:card>
</article>
</div>

View file

@ -12,32 +12,37 @@ new #[Layout('components.layouts.app'), Title('Gutscheine')] class extends Compo
}
}; ?>
<div class="space-y-6">
<flux:card>
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div class="space-y-2">
<flux:heading size="lg">{{ __('Gutscheine') }}</flux:heading>
<flux:subheading>
{{ __('Coupons sind in der Initialmigration vertagt (Entscheidung D-16). Eine Wiedereinführung wird später separat evaluiert ggf. direkt über Stripe-Coupons.') }}
</flux:subheading>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Finanzen') }}</span>
<span class="badge warn">{{ __('Vertagt') }}</span>
</div>
<flux:badge color="zinc" icon="pause" size="lg">
{{ __('Vertagt') }}
</flux:badge>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Gutscheine') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[720px] text-[color:var(--color-ink-2)]">
{{ __('Coupons sind in der Initialmigration vertagt (Entscheidung D-16). Eine Wiedereinführung wird später separat evaluiert ggf. direkt über Stripe-Coupons.') }}
</p>
</div>
</flux:card>
</header>
<flux:card>
<flux:heading size="sm" class="mb-3">{{ __('Hinweise') }}</flux:heading>
<ul class="space-y-2 text-sm text-zinc-600 dark:text-zinc-300">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Hinweise') }}</span>
</div>
<ul class="p-5 space-y-3 text-[12.5px] text-[color:var(--color-ink-2)] list-none m-0">
<li class="flex gap-2">
<flux:icon.information-circle class="size-5 shrink-0 text-zinc-400" />
<flux:icon.information-circle class="size-[16px] shrink-0 mt-0.5 text-[color:var(--color-hub)]" />
<span>{{ __('Im neuen Stack sind keine eigenen Coupon-Tabellen vorgesehen. Sobald wieder benötigt, werden Coupons als Stripe-Coupons abgebildet.') }}</span>
</li>
<li class="flex gap-2">
<flux:icon.information-circle class="size-5 shrink-0 text-zinc-400" />
<flux:icon.information-circle class="size-[16px] shrink-0 mt-0.5 text-[color:var(--color-hub)]" />
<span>{{ __('Bestehende Legacy-Gutscheine werden nicht migriert Bestandskunden behalten ihre Konditionen über das Grandfathering-Modell (P8.8).') }}</span>
</li>
</ul>
</flux:card>
</article>
</div>

View file

@ -75,31 +75,35 @@ new #[Layout('components.layouts.app'), Title('Footer-Code anlegen')] class exte
}
}; ?>
<div class="space-y-6">
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl">{{ __('Footer-Code anlegen') }}</flux:heading>
<flux:subheading>
{{ __('Snippet, das unter Pressemitteilungen ausgespielt wird.') }}
</flux:subheading>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Footer-Codes') }}</span>
</div>
<flux:button
variant="ghost"
icon="arrow-left"
:href="route('admin.footer-codes.index')"
wire:navigate
>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Footer-Code anlegen') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Snippet, das unter Pressemitteilungen ausgespielt wird.') }}
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" :href="route('admin.footer-codes.index')" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</flux:card>
</header>
<form wire:submit="save" class="space-y-6">
<flux:card>
<flux:heading size="lg">{{ __('Stammdaten') }}</flux:heading>
<div class="mt-4 space-y-4">
<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')"
@ -115,7 +119,7 @@ new #[Layout('components.layouts.app'), Title('Footer-Code anlegen')] class exte
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<flux:select wire:model="portal" :label="__('Portal')">
@foreach($portalOptions as $option)
@foreach ($portalOptions as $option)
<option value="{{ $option->value }}">{{ $option->label() }}</option>
@endforeach
</flux:select>
@ -136,12 +140,13 @@ new #[Layout('components.layouts.app'), Title('Footer-Code anlegen')] class exte
/>
</div>
</div>
</flux:card>
</article>
<flux:card>
<flux:heading size="lg">{{ __('Sichtbarkeit') }}</flux:heading>
<div class="mt-4 space-y-4">
<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')"
@ -154,48 +159,48 @@ new #[Layout('components.layouts.app'), Title('Footer-Code anlegen')] class exte
:description="__('Inaktive Codes werden niemals ausgespielt.')"
/>
</div>
</flux:card>
</article>
@if(! $isGlobal)
<flux:card>
<flux:heading size="lg">{{ __('Kategorie-Zuordnung') }}</flux:heading>
<flux:subheading>
{{ __('Nur Pressemitteilungen in diesen Kategorien zeigen den Footer-Code.') }}
</flux:subheading>
<div class="mt-4 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 border border-zinc-200 px-3 py-2 dark:border-zinc-700">
<input
type="checkbox"
wire:model="categoryIds"
value="{{ $option['id'] }}"
class="rounded border-zinc-300"
/>
<span class="text-sm">{{ $option['name'] }}</span>
</label>
@empty
<flux:text class="text-zinc-500">
{{ __('Keine Kategorien vorhanden.') }}
</flux:text>
@endforelse
@if (! $isGlobal)
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Kategorie-Zuordnung') }}</span>
</div>
</flux:card>
<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
<flux:card>
<div class="flex items-center justify-end gap-2">
<flux:button
variant="ghost"
:href="route('admin.footer-codes.index')"
wire:navigate
>
<article class="panel">
<div class="p-5 flex items-center justify-end gap-2">
<flux:button variant="ghost" :href="route('admin.footer-codes.index')" wire:navigate>
{{ __('Abbrechen') }}
</flux:button>
<flux:button type="submit" variant="primary" icon="check">
{{ __('Speichern') }}
</flux:button>
</div>
</flux:card>
</article>
</form>
</div>

View file

@ -106,29 +106,42 @@ new #[Layout('components.layouts.app'), Title('Footer-Code bearbeiten')] class e
}
}; ?>
<div class="space-y-6">
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl">{{ __('Footer-Code bearbeiten') }}</flux:heading>
<flux:subheading>#{{ $id }} {{ $title }}</flux:subheading>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Footer-Codes') }}</span>
<span class="badge hub">ID #{{ $id }}</span>
@if ($isGlobal)
<span class="badge hub dot">{{ __('Global') }}</span>
@endif
@if ($isActive)
<span class="badge ok dot">{{ __('Aktiv') }}</span>
@else
<span class="badge dot">{{ __('Inaktiv') }}</span>
@endif
</div>
<flux:button
variant="ghost"
icon="arrow-left"
:href="route('admin.footer-codes.index')"
wire:navigate
>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)] break-words">
{{ __('Footer-Code bearbeiten') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">{{ $title }}</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" :href="route('admin.footer-codes.index')" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</flux:card>
</header>
<form wire:submit="save" class="space-y-6">
<flux:card>
<flux:heading size="lg">{{ __('Stammdaten') }}</flux:heading>
<div class="mt-4 space-y-4">
<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')" />
<flux:textarea
@ -139,7 +152,7 @@ new #[Layout('components.layouts.app'), Title('Footer-Code bearbeiten')] class e
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<flux:select wire:model="portal" :label="__('Portal')">
@foreach($portalOptions as $option)
@foreach ($portalOptions as $option)
<option value="{{ $option->value }}">{{ $option->label() }}</option>
@endforeach
</flux:select>
@ -159,12 +172,13 @@ new #[Layout('components.layouts.app'), Title('Footer-Code bearbeiten')] class e
/>
</div>
</div>
</flux:card>
</article>
<flux:card>
<flux:heading size="lg">{{ __('Sichtbarkeit') }}</flux:heading>
<div class="mt-4 space-y-4">
<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')"
@ -176,34 +190,38 @@ new #[Layout('components.layouts.app'), Title('Footer-Code bearbeiten')] class e
:label="__('Aktiv')"
/>
</div>
</flux:card>
</article>
@if(! $isGlobal)
<flux:card>
<flux:heading size="lg">{{ __('Kategorie-Zuordnung') }}</flux:heading>
<div class="mt-4 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 border border-zinc-200 px-3 py-2 dark:border-zinc-700">
@if (! $isGlobal)
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Kategorie-Zuordnung') }}</span>
</div>
<div class="p-5 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-zinc-300"
class="rounded border-[color:var(--color-bg-rule)]"
/>
<span class="text-sm">{{ $option['name'] }}</span>
<span class="text-[12.5px] text-[color:var(--color-ink)]">{{ $option['name'] }}</span>
</label>
@empty
<flux:text class="text-zinc-500">
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">
{{ __('Keine Kategorien vorhanden.') }}
</flux:text>
</p>
@endforelse
</div>
</flux:card>
</article>
@endif
<flux:card>
<div class="flex items-center justify-between gap-2">
<article class="panel" style="border-left:3px solid var(--color-err);">
<div class="panel-head">
<span class="section-eyebrow" style="color:var(--color-err);">{{ __('Danger Zone & Aktionen') }}</span>
</div>
<div class="p-5 flex items-center justify-between gap-2 flex-wrap">
<flux:button
type="button"
variant="danger"
@ -215,11 +233,7 @@ new #[Layout('components.layouts.app'), Title('Footer-Code bearbeiten')] class e
</flux:button>
<div class="flex items-center gap-2">
<flux:button
variant="ghost"
:href="route('admin.footer-codes.index')"
wire:navigate
>
<flux:button variant="ghost" :href="route('admin.footer-codes.index')" wire:navigate>
{{ __('Abbrechen') }}
</flux:button>
<flux:button type="submit" variant="primary" icon="check">
@ -227,6 +241,6 @@ new #[Layout('components.layouts.app'), Title('Footer-Code bearbeiten')] class e
</flux:button>
</div>
</div>
</flux:card>
</article>
</form>
</div>

View file

@ -76,80 +76,77 @@ new #[Layout('components.layouts.app'), Title('Footer-Codes')] class extends Com
}
}; ?>
<div class="space-y-6">
@if(session('success'))
<flux:callout color="green" icon="check-circle">{{ session('success') }}</flux:callout>
@endif
@if(session('error'))
<flux:callout color="red" icon="exclamation-triangle">{{ session('error') }}</flux:callout>
@endif
<flux:card>
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<flux:heading size="xl">{{ __('Footer-Codes') }}</flux:heading>
<flux:subheading>
{{ __('Snippets, die unter Pressemitteilungen ausgespielt werden.') }}
</flux:subheading>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Operations') }}</span>
</div>
<flux:button
variant="primary"
icon="plus"
:href="route('admin.footer-codes.create')"
wire:navigate
>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Footer-Codes') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Snippets, die unter Pressemitteilungen ausgespielt werden.') }}
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="primary" icon="plus" :href="route('admin.footer-codes.create')" wire:navigate>
{{ __('Footer-Code anlegen') }}
</flux:button>
</div>
</flux:card>
</header>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500">{{ __('Gesamt') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $totals['total'] }}</flux:text>
</div>
<flux:icon.code-bracket-square class="size-8 text-blue-500" />
</div>
</flux:card>
@if (session('success'))
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-center gap-2
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
<flux:icon.check-circle class="size-[16px] flex-shrink-0" />
{{ session('success') }}
</div>
@endif
@if (session('error'))
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-start gap-3
bg-[color:var(--color-err-soft)] border-[color:var(--color-err)]/30 text-[color:var(--color-ink-2)]">
<flux:icon.exclamation-triangle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-err)]" />
<div class="flex-1">{{ session('error') }}</div>
</div>
@endif
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500">{{ __('Aktiv') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $totals['active'] }}</flux:text>
</div>
<flux:icon.bolt class="size-8 text-green-500" />
</div>
</flux:card>
{{-- ============== KPI-Reihe ============== --}}
<section class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<x-portal.stat-card variant="primary" :label="__('Gesamt')" :value="number_format($totals['total'])">
<x-slot:meta>{{ __('Snippets') }}</x-slot:meta>
<x-slot:trend>{{ __('alle Portale') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="ok" :label="__('Aktiv')" :value="number_format($totals['active'])">
<x-slot:meta>{{ __('live ausgespielt') }}</x-slot:meta>
<x-slot:trend>{{ __('in PMs sichtbar') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Global')" :value="number_format($totals['global'])">
<x-slot:meta>{{ __('portalübergreifend') }}</x-slot:meta>
<x-slot:trend>{{ __('ohne Kategorie-Bindung') }}</x-slot:trend>
</x-portal.stat-card>
</section>
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500">{{ __('Global') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $totals['global'] }}</flux:text>
</div>
<flux:icon.globe-alt class="size-8 text-purple-500" />
</div>
</flux:card>
</div>
<flux:card>
<div class="grid gap-3 md:grid-cols-[1fr,auto,auto]">
{{-- ============== FILTER ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Filter & Suche') }}</span>
</div>
<div class="p-5 grid gap-3 md:grid-cols-[1fr,auto,auto]">
<flux:input
wire:model.live.debounce.300ms="search"
placeholder="{{ __('Titel oder Inhalt suchen…') }}"
icon="magnifying-glass"
/>
<flux:select wire:model.live="portalFilter">
<option value="all">{{ __('Alle Portale') }}</option>
@foreach($portalOptions as $option)
@foreach ($portalOptions as $option)
<option value="{{ $option->value }}">{{ $option->label() }}</option>
@endforeach
</flux:select>
<flux:select wire:model.live="statusFilter">
<option value="all">{{ __('Alle Status') }}</option>
<option value="active">{{ __('Aktiv') }}</option>
@ -157,9 +154,16 @@ new #[Layout('components.layouts.app'), Title('Footer-Codes')] class extends Com
<option value="global">{{ __('Global') }}</option>
</flux:select>
</div>
</flux:card>
</article>
<flux:card>
{{-- ============== TABELLE ============== --}}
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Alle Footer-Codes') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __(':count Einträge', ['count' => $codes->total()]) }}
</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Titel') }}</flux:table.column>
@ -172,34 +176,40 @@ new #[Layout('components.layouts.app'), Title('Footer-Codes')] class extends Com
</flux:table.columns>
<flux:table.rows>
@forelse($codes as $code)
@forelse ($codes as $code)
<flux:table.row :key="'fc-'.$code->id">
<flux:table.cell>
<div class="flex items-center gap-2">
@if($code->is_global)
<flux:badge color="purple" size="xs" icon="globe-alt">{{ __('Global') }}</flux:badge>
@if ($code->is_global)
<span class="badge hub dot">{{ __('Global') }}</span>
@endif
<span class="font-medium">{{ $code->title }}</span>
<span class="text-[13px] font-semibold text-[color:var(--color-ink)]">{{ $code->title }}</span>
</div>
</flux:table.cell>
<flux:table.cell>
<flux:badge color="zinc" size="sm">{{ $code->portal->label() }}</flux:badge>
<span class="badge hub">{{ $code->portal->label() }}</span>
</flux:table.cell>
<flux:table.cell>
{{ $code->language ? strtoupper($code->language) : '' }}
<span class="text-[12px] text-[color:var(--color-ink-2)] font-mono">
{{ $code->language ? strtoupper($code->language) : '' }}
</span>
</flux:table.cell>
<flux:table.cell>
@if($code->is_global)
<span class="text-zinc-500">{{ __('alle') }}</span>
@if ($code->is_global)
<span class="text-[12px] text-[color:var(--color-ink-3)]">{{ __('alle') }}</span>
@else
{{ $code->categories_count }}
<span class="text-[12.5px] text-[color:var(--color-ink)] tabular-nums">{{ $code->categories_count }}</span>
@endif
</flux:table.cell>
<flux:table.cell>{{ $code->priority }}</flux:table.cell>
<flux:table.cell>
<flux:badge :color="$code->is_active ? 'green' : 'zinc'" size="sm">
{{ $code->is_active ? __('Aktiv') : __('Inaktiv') }}
</flux:badge>
<span class="text-[12.5px] text-[color:var(--color-ink)] tabular-nums">{{ $code->priority }}</span>
</flux:table.cell>
<flux:table.cell>
@if ($code->is_active)
<span class="badge ok dot">{{ __('Aktiv') }}</span>
@else
<span class="badge dot">{{ __('Inaktiv') }}</span>
@endif
</flux:table.cell>
<flux:table.cell align="end">
<div class="flex items-center justify-end gap-1">
@ -224,8 +234,14 @@ new #[Layout('components.layouts.app'), Title('Footer-Codes')] class extends Com
@empty
<flux:table.row>
<flux:table.cell :colspan="7">
<div class="py-8 text-center text-zinc-500">
{{ __('Keine Footer-Codes gefunden.') }}
<div class="flex flex-col items-center justify-center px-4 py-10 text-center">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-3
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.code-bracket-square class="size-6" />
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Keine Footer-Codes gefunden.') }}
</div>
</div>
</flux:table.cell>
</flux:table.row>
@ -233,8 +249,8 @@ new #[Layout('components.layouts.app'), Title('Footer-Codes')] class extends Com
</flux:table.rows>
</flux:table>
<div class="mt-4">
<div class="border-t border-[color:var(--color-bg-rule)] p-4">
{{ $codes->links() }}
</div>
</flux:card>
</article>
</div>

View file

@ -120,108 +120,120 @@ new #[Layout('components.layouts.app'), Title('Legacy Rechnungen')] class extend
}
}; ?>
<div class="space-y-6">
<flux:card>
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div class="space-y-2">
<flux:heading size="lg">{{ __('Legacy Rechnungen') }}</flux:heading>
<flux:subheading>
{{ __('Legacy-Rechnungsarchiv mit read-only Übersicht, Filtern und PDF-Download. Der neue Stripe-Rechnungslauf folgt separat in Phase 8.') }}
</flux:subheading>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Finanzen') }}</span>
<span class="badge hub">{{ __('Legacy-Rechnungsarchiv') }}</span>
</div>
<flux:badge color="zinc" icon="archive-box" size="lg">
{{ __('Legacy-Archiv') }}
</flux:badge>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Legacy Rechnungen') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[720px] text-[color:var(--color-ink-2)]">
{{ __('Legacy-Rechnungsarchiv mit read-only Übersicht, Filtern und PDF-Download. Der neue Stripe-Rechnungslauf folgt separat in Phase 8.') }}
</p>
</div>
</flux:card>
</header>
@if($stats['unmapped_count'] > 0)
<flux:callout color="yellow" icon="exclamation-triangle">
{{ __(':count Legacy-Rechnungen konnten keinem neuen User zugeordnet werden. Sie bleiben im Archiv sichtbar und sollten im Rehearsal-Report fachlich geprüft werden.', ['count' => number_format($stats['unmapped_count'], 0, ',', '.')]) }}
</flux:callout>
@if ($stats['unmapped_count'] > 0)
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-start gap-3
bg-[color:var(--color-warn-soft)] border-[color:var(--color-warn)]/30 text-[color:var(--color-ink-2)]">
<flux:icon.exclamation-triangle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-accent-deep)]" />
<div class="flex-1">
{{ __(':count Legacy-Rechnungen konnten keinem neuen User zugeordnet werden. Sie bleiben im Archiv sichtbar und sollten im Rehearsal-Report fachlich geprüft werden.', ['count' => number_format($stats['unmapped_count'], 0, ',', '.')]) }}
</div>
</div>
@endif
<div class="grid grid-cols-2 gap-4 lg:grid-cols-5">
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Rechnungen') }}</flux:text>
<flux:text size="xl" weight="bold">{{ number_format($stats['count'], 0, ',', '.') }}</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Archivsumme') }}</flux:text>
<flux:text size="xl" weight="bold">{{ number_format($stats['total_cents'] / 100, 2, ',', '.') }} </flux:text>
</flux:card>
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Bezahlt') }}</flux:text>
<flux:text size="xl" weight="bold">{{ number_format($stats['paid_count'], 0, ',', '.') }}</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Ohne User') }}</flux:text>
<flux:text size="xl" weight="bold">{{ number_format($stats['unmapped_count'], 0, ',', '.') }}</flux:text>
</flux:card>
@if($supportsPdfGeneratedAt)
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('PDF erzeugt') }}</flux:text>
<flux:text size="xl" weight="bold">{{ number_format($stats['generated_pdf_count'], 0, ',', '.') }}</flux:text>
</flux:card>
{{-- ============== KPI-Reihe ============== --}}
<section class="grid grid-cols-2 gap-4 lg:grid-cols-5">
<x-portal.stat-card variant="primary" :label="__('Rechnungen')" :value="number_format($stats['count'], 0, ',', '.')">
<x-slot:meta>{{ __('Archivdatensätze') }}</x-slot:meta>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Archivsumme')" :value="number_format($stats['total_cents'] / 100, 2, ',', '.').' €'">
<x-slot:meta>{{ __('historisch') }}</x-slot:meta>
</x-portal.stat-card>
<x-portal.stat-card variant="ok" :label="__('Bezahlt')" :value="number_format($stats['paid_count'], 0, ',', '.')">
<x-slot:meta>{{ __('mit Zahldatum') }}</x-slot:meta>
</x-portal.stat-card>
<x-portal.stat-card variant="warn" :label="__('Ohne User')" :value="number_format($stats['unmapped_count'], 0, ',', '.')">
<x-slot:meta>{{ __('zu mappen') }}</x-slot:meta>
</x-portal.stat-card>
@if ($supportsPdfGeneratedAt)
<x-portal.stat-card variant="ok" :label="__('PDF erzeugt')" :value="number_format($stats['generated_pdf_count'], 0, ',', '.')">
<x-slot:meta>{{ __('aus Archiv') }}</x-slot:meta>
</x-portal.stat-card>
@else
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('PDF-Status') }}</flux:text>
<flux:text size="xl" weight="bold">{{ __('Migration offen') }}</flux:text>
</flux:card>
<x-portal.stat-card variant="muted" :label="__('PDF-Status')" :value="__('Migration offen')">
<x-slot:meta>{{ __('Schema-Update fehlt') }}</x-slot:meta>
</x-portal.stat-card>
@endif
</div>
</section>
<flux:card>
<div class="grid gap-3 lg:grid-cols-6">
<flux:input
wire:model.live.debounce.300ms="search"
placeholder="{{ __('Rechnungsnr., Legacy-ID, User oder E-Mail suchen...') }}"
icon="magnifying-glass"
class="lg:col-span-2"
/>
<flux:select wire:model.live="portalFilter">
<option value="all">{{ __('Alle Portale') }}</option>
@foreach($portalOptions as $portal)
<option value="{{ $portal->value }}">{{ $portal->label() }}</option>
@endforeach
</flux:select>
<flux:select wire:model.live="statusFilter">
<option value="all">{{ __('Alle Status') }}</option>
@foreach($statusOptions as $status)
<option value="{{ $status }}">{{ $status }}</option>
@endforeach
</flux:select>
<flux:select wire:model.live="mappingFilter">
<option value="all">{{ __('Alle Zuordnungen') }}</option>
<option value="mapped">{{ __('Mit User') }}</option>
<option value="unmapped">{{ __('Ohne User') }}</option>
</flux:select>
<flux:select wire:model.live="pdfFilter">
<option value="all">{{ __('Alle PDFs') }}</option>
@if($supportsPdfGeneratedAt)
<option value="generated">{{ __('PDF erzeugt') }}</option>
<option value="pending">{{ __('Noch nicht erzeugt') }}</option>
@endif
</flux:select>
</div>
<div class="mt-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<flux:text class="text-sm text-zinc-500">
{{ __(':count Treffer für die aktuelle Filterung. PDF-Dateien werden bei Bedarf aus den archivierten Legacy-Daten erzeugt.', ['count' => number_format($stats['filtered_count'], 0, ',', '.')]) }}
</flux:text>
{{-- ============== FILTER-PANEL ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Filter & Suche') }}</span>
<flux:button size="sm" variant="ghost" icon="arrow-path" wire:click="resetFilters">
{{ __('Filter zurücksetzen') }}
</flux:button>
</div>
</flux:card>
<div class="p-5 space-y-4">
<div class="grid gap-3 lg:grid-cols-6">
<flux:input
wire:model.live.debounce.300ms="search"
placeholder="{{ __('Rechnungsnr., Legacy-ID, User oder E-Mail suchen...') }}"
icon="magnifying-glass"
class="lg:col-span-2"
/>
<flux:card class="p-0">
<div class="p-4">
<flux:table>
<flux:select wire:model.live="portalFilter">
<option value="all">{{ __('Alle Portale') }}</option>
@foreach ($portalOptions as $portal)
<option value="{{ $portal->value }}">{{ $portal->label() }}</option>
@endforeach
</flux:select>
<flux:select wire:model.live="statusFilter">
<option value="all">{{ __('Alle Status') }}</option>
@foreach ($statusOptions as $status)
<option value="{{ $status }}">{{ $status }}</option>
@endforeach
</flux:select>
<flux:select wire:model.live="mappingFilter">
<option value="all">{{ __('Alle Zuordnungen') }}</option>
<option value="mapped">{{ __('Mit User') }}</option>
<option value="unmapped">{{ __('Ohne User') }}</option>
</flux:select>
<flux:select wire:model.live="pdfFilter">
<option value="all">{{ __('Alle PDFs') }}</option>
@if ($supportsPdfGeneratedAt)
<option value="generated">{{ __('PDF erzeugt') }}</option>
<option value="pending">{{ __('Noch nicht erzeugt') }}</option>
@endif
</flux:select>
</div>
<p class="text-[12px] text-[color:var(--color-ink-3)] m-0">
{{ __(':count Treffer für die aktuelle Filterung. PDF-Dateien werden bei Bedarf aus den archivierten Legacy-Daten erzeugt.', ['count' => number_format($stats['filtered_count'], 0, ',', '.')]) }}
</p>
</div>
</article>
{{-- ============== TABELLE ============== --}}
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Legacy-Rechnungen') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __(':count Treffer', ['count' => number_format($stats['filtered_count'], 0, ',', '.')]) }}
</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Rechnungsnr.') }}</flux:table.column>
<flux:table.column>{{ __('Portal') }}</flux:table.column>
@ -232,20 +244,20 @@ new #[Layout('components.layouts.app'), Title('Legacy Rechnungen')] class extend
<flux:table.column>{{ __('PDF') }}</flux:table.column>
</flux:table.columns>
@forelse($invoices as $invoice)
@forelse ($invoices as $invoice)
<flux:table.row wire:key="admin-legacy-invoice-{{ $invoice->id }}">
<flux:table.cell>
<div class="space-y-1">
<flux:text weight="semibold">{{ $invoice->number ?? ('#'.$invoice->legacy_id) }}</flux:text>
<flux:text class="text-xs text-zinc-500">Legacy-ID: {{ $invoice->legacy_id }}</flux:text>
<div class="space-y-0.5">
<div class="text-[13px] font-semibold text-[color:var(--color-ink)]">{{ $invoice->number ?? ('#'.$invoice->legacy_id) }}</div>
<div class="text-[11px] text-[color:var(--color-ink-3)] font-mono">Legacy-ID: {{ $invoice->legacy_id }}</div>
</div>
</flux:table.cell>
<flux:table.cell>
<flux:badge size="sm" color="zinc">{{ $invoice->legacy_portal?->label() }}</flux:badge>
<span class="badge hub">{{ $invoice->legacy_portal?->label() }}</span>
</flux:table.cell>
<flux:table.cell>
@if($invoice->user)
<div class="space-y-1">
@if ($invoice->user)
<div class="space-y-0.5">
<flux:button
size="xs"
variant="ghost"
@ -254,28 +266,30 @@ new #[Layout('components.layouts.app'), Title('Legacy Rechnungen')] class extend
>
{{ $invoice->user->name }}
</flux:button>
<flux:text class="text-xs text-zinc-500">{{ $invoice->user->email }}</flux:text>
<div class="text-[11px] text-[color:var(--color-ink-3)]">{{ $invoice->user->email }}</div>
</div>
@else
<div class="space-y-1">
<flux:badge size="sm" color="yellow">{{ __('Ohne Zuordnung') }}</flux:badge>
<flux:text class="text-xs text-zinc-500">Legacy-User: {{ $invoice->legacy_user_id ?? 'n/a' }}</flux:text>
<span class="badge warn dot">{{ __('Ohne Zuordnung') }}</span>
<div class="text-[11px] text-[color:var(--color-ink-3)] font-mono">Legacy-User: {{ $invoice->legacy_user_id ?? 'n/a' }}</div>
</div>
@endif
</flux:table.cell>
<flux:table.cell>
<flux:text weight="semibold">{{ number_format($invoice->total_cents / 100, 2, ',', '.') }} </flux:text>
<span class="text-[13px] font-semibold text-[color:var(--color-ink)] tabular-nums">{{ number_format($invoice->total_cents / 100, 2, ',', '.') }} </span>
</flux:table.cell>
<flux:table.cell>
<flux:badge size="sm" color="{{ $invoice->paid_at ? 'green' : 'yellow' }}">
{{ $invoice->status ?? ($invoice->paid_at ? __('Bezahlt') : __('Offen')) }}
</flux:badge>
@if ($invoice->paid_at)
<span class="badge ok dot">{{ $invoice->status ?? __('Bezahlt') }}</span>
@else
<span class="badge warn dot">{{ $invoice->status ?? __('Offen') }}</span>
@endif
</flux:table.cell>
<flux:table.cell>
<div class="space-y-1">
<flux:text class="text-sm text-zinc-500">{{ $invoice->invoice_date?->format('d.m.Y') ?? '' }}</flux:text>
@if($invoice->paid_at)
<flux:text class="text-xs text-zinc-500">{{ __('bezahlt: :date', ['date' => $invoice->paid_at->format('d.m.Y')]) }}</flux:text>
<div class="space-y-0.5">
<div class="text-[12px] text-[color:var(--color-ink-2)]">{{ $invoice->invoice_date?->format('d.m.Y') ?? '' }}</div>
@if ($invoice->paid_at)
<div class="text-[11px] text-[color:var(--color-ink-3)]">{{ __('bezahlt: :date', ['date' => $invoice->paid_at->format('d.m.Y')]) }}</div>
@endif
</div>
</flux:table.cell>
@ -290,8 +304,8 @@ new #[Layout('components.layouts.app'), Title('Legacy Rechnungen')] class extend
>
{{ __('Öffnen') }}
</flux:button>
@if($supportsPdfGeneratedAt && $invoice->pdf_generated_at)
<flux:badge size="sm" color="green">{{ __('erzeugt') }}</flux:badge>
@if ($supportsPdfGeneratedAt && $invoice->pdf_generated_at)
<span class="badge ok">{{ __('erzeugt') }}</span>
@endif
</div>
</flux:table.cell>
@ -299,16 +313,22 @@ new #[Layout('components.layouts.app'), Title('Legacy Rechnungen')] class extend
@empty
<flux:table.row>
<flux:table.cell colspan="7">
<div class="flex flex-col items-center justify-center py-10">
<flux:icon.document-text class="size-10 text-zinc-300" />
<flux:text class="mt-3 text-zinc-500">{{ __('Keine Legacy-Rechnungen für diese Filter gefunden.') }}</flux:text>
<div class="flex flex-col items-center justify-center px-4 py-10 text-center">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-3
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.document-text class="size-6" />
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Keine Legacy-Rechnungen für diese Filter gefunden.') }}
</div>
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table>
</div>
</flux:card>
</flux:table>
{{ $invoices->links() }}
<div class="border-t border-[color:var(--color-bg-rule)] p-4">
{{ $invoices->links() }}
</div>
</article>
</div>

View file

@ -86,86 +86,86 @@ new #[Layout('components.layouts.app'), Title('Newsletter Sync')] class extends
}
}; ?>
<div class="space-y-6">
<flux:card>
<div class="flex items-start justify-between gap-4">
<div class="space-y-2">
<flux:heading size="lg">{{ __('Newsletter Synchronisierung') }}</flux:heading>
<flux:text class="text-zinc-500 dark:text-zinc-400">
{{ __('Vorbereitung fuer die kuenftige externe API-Anbindung. Aktuell ist nur das technische Grundgeruest aktiv.') }}
</flux:text>
</div>
<div class="flex flex-col items-end gap-2">
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Newsletter') }}</span>
@if ($syncConfig['enabled'])
<flux:badge color="green" icon="check" size="sm">{{ __('Sync aktiv') }}</flux:badge>
<span class="badge ok dot">{{ __('Sync aktiv') }}</span>
@else
<flux:badge color="zinc" icon="pause" size="sm">{{ __('Sync deaktiviert') }}</flux:badge>
<span class="badge dot">{{ __('Sync deaktiviert') }}</span>
@endif
<div class="flex gap-2">
<flux:button size="sm" variant="ghost" icon="eye" wire:click="triggerDryRun">
{{ __('Dry Run') }}
</flux:button>
<flux:button size="sm" icon="play" wire:click="triggerTestSync">
{{ __('Test-Sync ausfuehren') }}
</flux:button>
</div>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Newsletter Synchronisierung') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Vorbereitung fuer die kuenftige externe API-Anbindung. Aktuell ist nur das technische Grundgeruest aktiv.') }}
</p>
</div>
</flux:card>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button size="sm" variant="ghost" icon="eye" wire:click="triggerDryRun">
{{ __('Dry Run') }}
</flux:button>
<flux:button size="sm" variant="primary" icon="play" wire:click="triggerTestSync">
{{ __('Test-Sync ausfuehren') }}
</flux:button>
</div>
</header>
@if ($dryRunMessage)
<flux:card>
<flux:text class="text-sm text-zinc-600 dark:text-zinc-300">{{ $dryRunMessage }}</flux:text>
</flux:card>
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-start gap-3
bg-[color:var(--color-hub-soft)] border-[color:var(--color-hub-soft-2)] text-[color:var(--color-ink-2)]">
<flux:icon.eye class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-hub)]" />
<div class="flex-1">{{ $dryRunMessage }}</div>
</div>
@endif
@if ($syncMessage)
<flux:card>
<flux:text class="text-sm">{{ $syncMessage }}</flux:text>
</flux:card>
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-center gap-2
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
<flux:icon.check-circle class="size-[16px] flex-shrink-0" />
{{ $syncMessage }}
</div>
@endif
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<flux:card>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Gesamt') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['total'] }}</flux:text>
</flux:card>
{{-- ============== KPI-Reihe ============== --}}
<section class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<x-portal.stat-card variant="primary" :label="__('Gesamt')" :value="number_format($stats['total'])">
<x-slot:meta>{{ __('Subscriptions') }}</x-slot:meta>
</x-portal.stat-card>
<x-portal.stat-card variant="ok" :label="__('Bestaetigt')" :value="number_format($stats['confirmed'])">
<x-slot:meta>{{ __('Double Opt-in') }}</x-slot:meta>
</x-portal.stat-card>
<x-portal.stat-card variant="warn" :label="__('Unbestaetigt')" :value="number_format($stats['pending'])">
<x-slot:meta>{{ __('warten auf Opt-in') }}</x-slot:meta>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Abgemeldet')" :value="number_format($stats['unsubscribed'])">
<x-slot:meta>{{ __('Unsubscribed') }}</x-slot:meta>
</x-portal.stat-card>
</section>
<flux:card>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Bestaetigt') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['confirmed'] }}</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Unbestaetigt') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['pending'] }}</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Abgemeldet') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['unsubscribed'] }}</flux:text>
</flux:card>
</div>
<flux:card>
<flux:heading size="sm">{{ __('Konfiguration') }}</flux:heading>
<div class="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Konfiguration') }}</span>
</div>
<dl class="p-5 grid grid-cols-1 gap-4 sm:grid-cols-2 text-[12.5px]">
<div>
<flux:text class="text-xs uppercase tracking-wide text-zinc-500 dark:text-zinc-400">{{ __('Provider') }}</flux:text>
<flux:text class="mt-1">{{ $syncConfig['provider'] }}</flux:text>
<dt class="text-[10.5px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-1">{{ __('Provider') }}</dt>
<dd class="text-[color:var(--color-ink)] font-mono">{{ $syncConfig['provider'] }}</dd>
</div>
<div>
<flux:text class="text-xs uppercase tracking-wide text-zinc-500 dark:text-zinc-400">{{ __('Timeout') }}</flux:text>
<flux:text class="mt-1">{{ $syncConfig['timeout'] }}s</flux:text>
<dt class="text-[10.5px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-1">{{ __('Timeout') }}</dt>
<dd class="text-[color:var(--color-ink)] tabular-nums">{{ $syncConfig['timeout'] }}s</dd>
</div>
<div class="sm:col-span-2">
<flux:text class="text-xs uppercase tracking-wide text-zinc-500 dark:text-zinc-400">{{ __('Endpoint') }}</flux:text>
<flux:text class="mt-1 break-all">{{ $syncConfig['endpoint'] }}</flux:text>
<dt class="text-[10.5px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-1">{{ __('Endpoint') }}</dt>
<dd class="text-[color:var(--color-ink)] font-mono break-all">{{ $syncConfig['endpoint'] }}</dd>
</div>
</div>
</flux:card>
</dl>
</article>
</div>

View file

@ -12,42 +12,47 @@ new #[Layout('components.layouts.app'), Title('Zahlungen')] class extends Compon
}
}; ?>
<div class="space-y-6">
<flux:card>
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div class="space-y-2">
<flux:heading size="lg">{{ __('Zahlungen') }}</flux:heading>
<flux:subheading>
{{ __('Zahlungsabwicklung läuft in Phase 8 ausschließlich über Stripe alte Zahlungsarten (Rechnung, PayPal, SPK Berlin, Cortal Consors, Bar/Post) entfallen komplett.') }}
</flux:subheading>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Finanzen') }}</span>
<span class="badge warn">{{ __('In Vorbereitung') }}</span>
</div>
<flux:badge color="amber" icon="clock" size="lg">
{{ __('In Vorbereitung') }}
</flux:badge>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Zahlungen') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[720px] text-[color:var(--color-ink-2)]">
{{ __('Zahlungsabwicklung läuft in Phase 8 ausschließlich über Stripe alte Zahlungsarten (Rechnung, PayPal, SPK Berlin, Cortal Consors, Bar/Post) entfallen komplett.') }}
</p>
</div>
</flux:card>
</header>
<flux:card>
<flux:heading size="sm" class="mb-3">{{ __('Geplant für P8') }}</flux:heading>
<ul class="space-y-2 text-sm text-zinc-600 dark:text-zinc-300">
<li class="flex gap-2">
<flux:icon.check-circle class="size-5 shrink-0 text-zinc-400" />
<span>{{ __('Live-Anzeige aller Stripe-Zahlungen mit Filtern nach Status, Methode und Zeitraum.') }}</span>
</li>
<li class="flex gap-2">
<flux:icon.check-circle class="size-5 shrink-0 text-zinc-400" />
<span>{{ __('Detail-Ansicht mit Stripe-Transaktions-ID, Webhook-Trail und zugeordneter Rechnung.') }}</span>
</li>
<li class="flex gap-2">
<flux:icon.check-circle class="size-5 shrink-0 text-zinc-400" />
<span>{{ __('Refund-Workflow direkt aus dem Admin (sofern Stripe-Berechtigung gegeben).') }}</span>
</li>
</ul>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Geplant für P8') }}</span>
</div>
<div class="p-5 space-y-4">
<ul class="space-y-3 text-[12.5px] text-[color:var(--color-ink-2)] list-none m-0">
<li class="flex gap-2">
<flux:icon.check-circle class="size-[16px] shrink-0 mt-0.5 text-[color:var(--color-ok)]" />
<span>{{ __('Live-Anzeige aller Stripe-Zahlungen mit Filtern nach Status, Methode und Zeitraum.') }}</span>
</li>
<li class="flex gap-2">
<flux:icon.check-circle class="size-[16px] shrink-0 mt-0.5 text-[color:var(--color-ok)]" />
<span>{{ __('Detail-Ansicht mit Stripe-Transaktions-ID, Webhook-Trail und zugeordneter Rechnung.') }}</span>
</li>
<li class="flex gap-2">
<flux:icon.check-circle class="size-[16px] shrink-0 mt-0.5 text-[color:var(--color-ok)]" />
<span>{{ __('Refund-Workflow direkt aus dem Admin (sofern Stripe-Berechtigung gegeben).') }}</span>
</li>
</ul>
<flux:separator class="my-5" />
<flux:text class="text-sm text-zinc-500">
{{ __('Datenmodell (user_payments, user_payment_options) ist bereits angelegt; die Anbindung folgt mit Stripe-Webhooks.') }}
</flux:text>
</flux:card>
<p class="pt-4 border-t border-[color:var(--color-bg-rule)] text-[12px] text-[color:var(--color-ink-3)] m-0">
{{ __('Datenmodell (user_payments, user_payment_options) ist bereits angelegt; die Anbindung folgt mit Stripe-Webhooks.') }}
</p>
</div>
</article>
</div>

View file

@ -42,24 +42,36 @@ new class extends Component
}; ?>
<div class="px-1 py-2">
<div class="mb-1 text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider px-2">
<div class="mb-2 px-2 text-[10.5px] uppercase tracking-[0.6px] font-semibold text-[color:var(--color-ink-3)]">
{{ __('Portal-Filter') }}
</div>
<div class="flex flex-col gap-1">
<div class="flex flex-col gap-0.5">
<button
wire:click="switchPortal('')"
class="flex items-center gap-2 rounded px-2 py-1.5 text-sm transition-colors {{ $activePortal === '' ? 'bg-zinc-200 dark:bg-zinc-700 font-medium' : 'hover:bg-zinc-100 dark:hover:bg-zinc-800' }}"
@class([
'flex items-center gap-2 rounded-[4px] px-2 py-1.5 text-[12.5px] transition-colors',
'bg-[color:var(--color-bg-elev)] font-semibold text-[color:var(--color-ink)] border border-[color:var(--color-bg-rule)]' => $activePortal === '',
'text-[color:var(--color-ink-2)] hover:bg-[color:var(--color-bg-elev)]/60' => $activePortal !== '',
])
>
<span class="h-2 w-2 rounded-full bg-zinc-400"></span>
<span class="h-2 w-2 rounded-full bg-[color:var(--color-ink-3)]"></span>
{{ __('Alle Portale') }}
</button>
@foreach($portals as $portal)
@if($portal !== \App\Enums\Portal::Both)
@foreach ($portals as $portal)
@if ($portal !== \App\Enums\Portal::Both)
<button
wire:click="switchPortal('{{ $portal->value }}')"
class="flex items-center gap-2 rounded px-2 py-1.5 text-sm transition-colors {{ $activePortal === $portal->value ? 'bg-zinc-200 dark:bg-zinc-700 font-medium' : 'hover:bg-zinc-100 dark:hover:bg-zinc-800' }}"
@class([
'flex items-center gap-2 rounded-[4px] px-2 py-1.5 text-[12.5px] transition-colors',
'bg-[color:var(--color-bg-elev)] font-semibold text-[color:var(--color-ink)] border border-[color:var(--color-bg-rule)]' => $activePortal === $portal->value,
'text-[color:var(--color-ink-2)] hover:bg-[color:var(--color-bg-elev)]/60' => $activePortal !== $portal->value,
])
>
<span class="h-2 w-2 rounded-full {{ $portal === \App\Enums\Portal::Presseecho ? 'bg-green-500' : 'bg-red-500' }}"></span>
<span @class([
'h-2 w-2 rounded-full',
'bg-[color:var(--color-ok)]' => $portal === \App\Enums\Portal::Presseecho,
'bg-[color:var(--color-err)]' => $portal !== \App\Enums\Portal::Presseecho,
])></span>
{{ $portal->label() }}
</button>
@endif

View file

@ -50,23 +50,33 @@ new #[Layout('components.layouts.app'), Title('Neue Voreinstellung')] class exte
}
}; ?>
<form wire:submit="save" class="space-y-6">
<flux:card>
<div class="flex items-center justify-between gap-4">
<div>
<flux:heading size="lg">{{ __('Neue Voreinstellung') }}</flux:heading>
<flux:subheading>{{ __('Texte, Zahlen oder JSON-Werte zentral fuer Admin-Funktionen pflegen.') }}</flux:subheading>
<form wire:submit="save" class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Voreinstellungen') }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Neue Voreinstellung') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Texte, Zahlen oder JSON-Werte zentral fuer Admin-Funktionen pflegen.') }}
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.presets.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</flux:card>
</header>
@include('livewire.admin.presets.partials.form-fields')
<flux:card>
<div class="flex justify-end gap-3">
<article class="panel">
<div class="p-5 flex justify-end gap-3">
<flux:button variant="ghost" href="{{ route('admin.presets.index') }}" wire:navigate>
{{ __('Abbrechen') }}
</flux:button>
@ -74,5 +84,5 @@ new #[Layout('components.layouts.app'), Title('Neue Voreinstellung')] class exte
{{ __('Voreinstellung erstellen') }}
</flux:button>
</div>
</flux:card>
</article>
</form>

View file

@ -71,23 +71,34 @@ new #[Layout('components.layouts.app'), Title('Voreinstellung bearbeiten')] clas
}
}; ?>
<form wire:submit="save" class="space-y-6">
<flux:card>
<div class="flex items-center justify-between gap-4">
<div>
<flux:heading size="lg">{{ __('Voreinstellung bearbeiten') }}</flux:heading>
<flux:subheading>{{ __('ID') }}: {{ $id }}</flux:subheading>
<form wire:submit="save" class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Voreinstellungen') }}</span>
<span class="badge hub">ID #{{ $id }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Voreinstellung bearbeiten') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Texte, Zahlen oder JSON-Werte zentral fuer Admin-Funktionen pflegen.') }}
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.presets.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</flux:card>
</header>
@include('livewire.admin.presets.partials.form-fields')
<flux:card>
<div class="flex justify-end gap-3">
<article class="panel">
<div class="p-5 flex justify-end gap-3">
<flux:button variant="ghost" href="{{ route('admin.presets.index') }}" wire:navigate>
{{ __('Abbrechen') }}
</flux:button>
@ -95,5 +106,5 @@ new #[Layout('components.layouts.app'), Title('Voreinstellung bearbeiten')] clas
{{ __('Änderungen speichern') }}
</flux:button>
</div>
</flux:card>
</article>
</form>

View file

@ -76,28 +76,43 @@ new #[Layout('components.layouts.app'), Title('Voreinstellungen')] class extends
}
}; ?>
<div class="space-y-6">
@if(session('success'))
<div class="rounded-md border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800 dark:border-green-800 dark:bg-green-900/20 dark:text-green-300">
{{ session('success') }}
</div>
@endif
<flux:card>
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<flux:heading size="lg">{{ __('Voreinstellungen') }}</flux:heading>
<flux:subheading>{{ __('Zentrale Admin-Presets fuer Texte, Zahlen und weitere Werte.') }}</flux:subheading>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Stammdaten') }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Voreinstellungen') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Zentrale Admin-Presets fuer Texte, Zahlen und weitere Werte.') }}
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button icon="plus" variant="primary" href="{{ route('admin.presets.create') }}" wire:navigate>
{{ __('Neue Voreinstellung') }}
</flux:button>
</div>
</flux:card>
</header>
<flux:card>
<div class="flex flex-col gap-3 sm:flex-row sm:items-center">
@if (session('success'))
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-center gap-2
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
<flux:icon.check-circle class="size-[16px] flex-shrink-0" />
{{ session('success') }}
</div>
@endif
{{-- ============== FILTER-PANEL ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Filter & Suche') }}</span>
</div>
<div class="p-5 flex flex-col gap-3 sm:flex-row sm:items-center">
<flux:input
wire:model.live.debounce.300ms="search"
placeholder="{{ __('Key, Bezeichnung oder Wert suchen...') }}"
@ -107,21 +122,28 @@ new #[Layout('components.layouts.app'), Title('Voreinstellungen')] class extends
<flux:select wire:model.live="areaFilter" class="sm:w-48">
<option value="all">{{ __('Alle Bereiche') }}</option>
@foreach($areas as $area)
@foreach ($areas as $area)
<option value="{{ $area }}">{{ $area }}</option>
@endforeach
</flux:select>
<flux:select wire:model.live="typeFilter" class="sm:w-40">
<option value="all">{{ __('Alle Typen') }}</option>
@foreach($types as $type)
@foreach ($types as $type)
<option value="{{ $type }}">{{ $type }}</option>
@endforeach
</flux:select>
</div>
</flux:card>
</article>
<flux:card class="overflow-hidden">
{{-- ============== TABELLE ============== --}}
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Alle Voreinstellungen') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __(':count Einträge', ['count' => $presets->total()]) }}
</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Key') }}</flux:table.column>
@ -133,33 +155,35 @@ new #[Layout('components.layouts.app'), Title('Voreinstellungen')] class extends
</flux:table.columns>
<flux:table.rows>
@forelse($presets as $preset)
@forelse ($presets as $preset)
<flux:table.row wire:key="{{ $preset->id }}">
<flux:table.cell>
<div class="max-w-xs">
<flux:text weight="semibold" class="truncate">{{ $preset->label }}</flux:text>
<flux:text class="truncate text-xs text-zinc-500">{{ $preset->key }}</flux:text>
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] truncate">{{ $preset->label }}</div>
<div class="text-[11.5px] text-[color:var(--color-ink-3)] truncate font-mono">{{ $preset->key }}</div>
</div>
</flux:table.cell>
<flux:table.cell>
<flux:badge color="blue" size="sm">{{ $preset->area }}</flux:badge>
<span class="badge hub">{{ $preset->area }}</span>
</flux:table.cell>
<flux:table.cell>
<flux:badge color="zinc" size="sm">{{ $preset->type }}</flux:badge>
<span class="badge hub">{{ $preset->type }}</span>
</flux:table.cell>
<flux:table.cell>
<flux:text class="line-clamp-2 max-w-sm text-sm text-zinc-600 dark:text-zinc-300">
<div class="line-clamp-2 max-w-sm text-[12.5px] text-[color:var(--color-ink-2)]">
{{ \Illuminate\Support\Str::limit($preset->value ?? '-', 140) }}
</flux:text>
</div>
</flux:table.cell>
<flux:table.cell>
<flux:badge color="{{ $preset->is_active ? 'green' : 'zinc' }}" size="sm">
{{ $preset->is_active ? __('Aktiv') : __('Inaktiv') }}
</flux:badge>
@if ($preset->is_active)
<span class="badge ok dot">{{ __('Aktiv') }}</span>
@else
<span class="badge dot">{{ __('Inaktiv') }}</span>
@endif
</flux:table.cell>
<flux:table.cell>
@ -169,16 +193,22 @@ new #[Layout('components.layouts.app'), Title('Voreinstellungen')] class extends
@empty
<flux:table.row>
<flux:table.cell colspan="6">
<div class="flex flex-col items-center justify-center py-10">
<flux:icon.cog class="size-10 text-zinc-400" />
<flux:text class="mt-3 text-zinc-500">{{ __('Keine Voreinstellungen gefunden.') }}</flux:text>
<div class="flex flex-col items-center justify-center px-4 py-10 text-center">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-3
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.cog class="size-6" />
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Keine Voreinstellungen gefunden.') }}
</div>
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
</flux:card>
{{ $presets->links() }}
<div class="border-t border-[color:var(--color-bg-rule)] p-4">
{{ $presets->links() }}
</div>
</article>
</div>

View file

@ -1,11 +1,15 @@
<div class="grid gap-6 lg:grid-cols-[1fr,320px]">
<div class="space-y-6">
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Basisdaten') }}</flux:heading>
<div class="space-y-4">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Basisdaten') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:field>
<flux:label>{{ __('Key') }} <span class="text-red-500">*</span></flux:label>
<flux:label>
{{ __('Key') }}
<span class="text-[color:var(--color-err)]">*</span>
</flux:label>
<flux:input wire:model="key" placeholder="press_releases.deleted_published_text" />
<flux:description>{{ __('Technischer Schlüssel. Erlaubt sind Kleinbuchstaben, Zahlen, Punkt, Unterstrich und Bindestrich.') }}</flux:description>
<flux:error name="key" />
@ -13,13 +17,19 @@
<div class="grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Bereich') }} <span class="text-red-500">*</span></flux:label>
<flux:label>
{{ __('Bereich') }}
<span class="text-[color:var(--color-err)]">*</span>
</flux:label>
<flux:input wire:model="area" placeholder="press_releases" />
<flux:error name="area" />
</flux:field>
<flux:field>
<flux:label>{{ __('Typ') }} <span class="text-red-500">*</span></flux:label>
<flux:label>
{{ __('Typ') }}
<span class="text-[color:var(--color-err)]">*</span>
</flux:label>
<flux:select wire:model="type">
<option value="text">{{ __('Text') }}</option>
<option value="number">{{ __('Zahl') }}</option>
@ -31,17 +41,21 @@
</div>
<flux:field>
<flux:label>{{ __('Bezeichnung') }} <span class="text-red-500">*</span></flux:label>
<flux:label>
{{ __('Bezeichnung') }}
<span class="text-[color:var(--color-err)]">*</span>
</flux:label>
<flux:input wire:model="label" placeholder="{{ __('Ersatztext für gelöschte veröffentlichte Pressemitteilungen') }}" />
<flux:error name="label" />
</flux:field>
</div>
</flux:card>
</article>
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Werte') }}</flux:heading>
<div class="space-y-4">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Werte') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:field>
<flux:label>{{ __('Wert') }}</flux:label>
<flux:textarea wire:model="value" rows="10" />
@ -56,18 +70,20 @@
<flux:error name="payload" />
</flux:field>
</div>
</flux:card>
</article>
</div>
<div class="space-y-6">
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Status') }}</flux:heading>
<flux:checkbox wire:model="isActive" label="{{ __('Aktiv') }}" />
<flux:text class="mt-3 text-sm text-zinc-500">
{{ __('Nur aktive Presets werden von der Anwendung automatisch verwendet.') }}
</flux:text>
</flux:card>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Status') }}</span>
</div>
<div class="p-5">
<flux:checkbox wire:model="isActive" label="{{ __('Aktiv') }}" />
<p class="mt-3 text-[12px] text-[color:var(--color-ink-3)] m-0">
{{ __('Nur aktive Presets werden von der Anwendung automatisch verwendet.') }}
</p>
</div>
</article>
</div>
</div>

View file

@ -131,6 +131,25 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
$this->clearContactFilter();
}
public function setView(string $view): void
{
$this->statusFilter = $view;
$this->resetPage();
}
public function resetFilters(): void
{
$this->search = '';
$this->statusFilter = 'all';
$this->portalFilter = 'all';
$this->languageFilter = 'all';
$this->categoryFilter = 'all';
$this->clearUserFilter();
$this->clearCompanyFilter();
$this->clearContactFilter();
$this->resetPage();
}
public function publish(int $id): void
{
$pr = PressRelease::withoutGlobalScopes()->findOrFail($id);
@ -222,7 +241,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
}
/**
* @return array{total: int, published: int, review: int, draft: int}
* @return array{total: int, published: int, review: int, draft: int, rejected: int, archived: int}
*/
private function pressReleaseStats(): array
{
@ -233,6 +252,8 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
->selectRaw('SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as published', [PressReleaseStatus::Published->value])
->selectRaw('SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as review', [PressReleaseStatus::Review->value])
->selectRaw('SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as draft', [PressReleaseStatus::Draft->value])
->selectRaw('SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as rejected', [PressReleaseStatus::Rejected->value])
->selectRaw('SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as archived', [PressReleaseStatus::Archived->value])
->first();
return [
@ -240,6 +261,8 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
'published' => (int) ($stats->published ?? 0),
'review' => (int) ($stats->review ?? 0),
'draft' => (int) ($stats->draft ?? 0),
'rejected' => (int) ($stats->rejected ?? 0),
'archived' => (int) ($stats->archived ?? 0),
];
});
}
@ -405,6 +428,34 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
</x-portal.stat-card>
</section>
{{-- ============== SAVED-VIEWS-TABS ============== --}}
<nav class="view-tabs" aria-label="{{ __('Gespeicherte Ansichten') }}">
<button type="button" wire:click="setView('all')"
@class(['view-tab', 'is-active' => $statusFilter === 'all'])>
{{ __('Alle') }} <span class="cnt">{{ number_format($stats['total']) }}</span>
</button>
<button type="button" wire:click="setView('{{ \App\Enums\PressReleaseStatus::Review->value }}')"
@class(['view-tab', 'is-active' => $statusFilter === \App\Enums\PressReleaseStatus::Review->value])>
{{ __('In Prüfung') }} <span class="cnt">{{ number_format($stats['review']) }}</span>
</button>
<button type="button" wire:click="setView('{{ \App\Enums\PressReleaseStatus::Published->value }}')"
@class(['view-tab', 'is-active' => $statusFilter === \App\Enums\PressReleaseStatus::Published->value])>
{{ __('Veröffentlicht') }} <span class="cnt">{{ number_format($stats['published']) }}</span>
</button>
<button type="button" wire:click="setView('{{ \App\Enums\PressReleaseStatus::Draft->value }}')"
@class(['view-tab', 'is-active' => $statusFilter === \App\Enums\PressReleaseStatus::Draft->value])>
{{ __('Entwürfe') }} <span class="cnt">{{ number_format($stats['draft']) }}</span>
</button>
<button type="button" wire:click="setView('{{ \App\Enums\PressReleaseStatus::Rejected->value }}')"
@class(['view-tab', 'is-active' => $statusFilter === \App\Enums\PressReleaseStatus::Rejected->value])>
{{ __('Abgelehnt') }} <span class="cnt">{{ number_format($stats['rejected'] ?? 0) }}</span>
</button>
<button type="button" wire:click="setView('{{ \App\Enums\PressReleaseStatus::Archived->value }}')"
@class(['view-tab', 'is-active' => $statusFilter === \App\Enums\PressReleaseStatus::Archived->value])>
{{ __('Archiv') }} <span class="cnt">{{ number_format($stats['archived'] ?? 0) }}</span>
</button>
</nav>
{{-- ============== FILTER-PANEL ============== --}}
<article class="panel">
<div class="panel-head">
@ -444,7 +495,9 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
<flux:select wire:model.live="categoryFilter" class="w-full">
<option value="all">{{ __('Alle Kategorien') }}</option>
@foreach ($categoryOptions as $categoryOption)
@php($categoryName = $categoryOption->translations->firstWhere('locale', 'de')?->name ?? '#' . $categoryOption->id)
@php
$categoryName = $categoryOption->translations->firstWhere('locale', 'de')?->name ?? '#'.$categoryOption->id;
@endphp
<option value="{{ $categoryOption->id }}">{{ $categoryName }}</option>
@endforeach
</flux:select>
@ -548,7 +601,9 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
<flux:select.option value="all">{{ __('Alle Kontakte') }}</flux:select.option>
@foreach($contactLookupResults as $contactOption)
@php($contactName = trim(($contactOption->first_name ?? '').' '.($contactOption->last_name ?? '')) ?: __('Kontakt ohne Name'))
@php
$contactName = trim(($contactOption->first_name ?? '').' '.($contactOption->last_name ?? '')) ?: __('Kontakt ohne Name');
@endphp
<flux:select.option :value="$contactOption->id" wire:key="pm-contact-{{ $contactOption->id }}">
{{ $contactName }}
<span class="ml-1 text-zinc-400">
@ -575,6 +630,154 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
/>
</div>
</div>
{{-- Active-Chips --}}
@php
$hasAnyFilter = $search !== ''
|| $statusFilter !== 'all'
|| $portalFilter !== 'all'
|| $languageFilter !== 'all'
|| $categoryFilter !== 'all'
|| $userFilter !== 'all'
|| $companyFilter !== 'all'
|| $contactFilter !== 'all';
@endphp
@if ($hasAnyFilter)
<div class="flex items-center gap-2 flex-wrap text-[11.5px] pt-1">
<span class="eyebrow muted" style="margin-right:2px;">{{ __('Aktiv') }}</span>
@if ($search !== '')
<span class="active-chip">
<span>{{ __('Suche') }}:
<strong>{{ \Illuminate\Support\Str::limit($search, 40) }}"</strong></span>
<button type="button" class="x" wire:click="$set('search', '')"
aria-label="{{ __('Filter entfernen') }}">
<svg width="8" height="8" viewBox="0 0 12 12" fill="none">
<path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.6"
stroke-linecap="round" />
</svg>
</button>
</span>
@endif
@if ($statusFilter !== 'all')
@php $statusEnum = \App\Enums\PressReleaseStatus::tryFrom($statusFilter); @endphp
<span class="active-chip">
<span>{{ __('Status') }}:
<strong>{{ $statusEnum?->label() ?? $statusFilter }}</strong></span>
<button type="button" class="x" wire:click="setView('all')"
aria-label="{{ __('Filter entfernen') }}">
<svg width="8" height="8" viewBox="0 0 12 12" fill="none">
<path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.6"
stroke-linecap="round" />
</svg>
</button>
</span>
@endif
@if ($portalFilter !== 'all')
@php $portalEnum = \App\Enums\Portal::tryFrom($portalFilter); @endphp
<span class="active-chip">
<span>{{ __('Portal') }}:
<strong>{{ $portalEnum?->label() ?? $portalFilter }}</strong></span>
<button type="button" class="x" wire:click="$set('portalFilter', 'all')"
aria-label="{{ __('Filter entfernen') }}">
<svg width="8" height="8" viewBox="0 0 12 12" fill="none">
<path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.6"
stroke-linecap="round" />
</svg>
</button>
</span>
@endif
@if ($languageFilter !== 'all')
<span class="active-chip">
<span>{{ __('Sprache') }}:
<strong>{{ strtoupper($languageFilter) }}</strong></span>
<button type="button" class="x" wire:click="$set('languageFilter', 'all')"
aria-label="{{ __('Filter entfernen') }}">
<svg width="8" height="8" viewBox="0 0 12 12" fill="none">
<path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.6"
stroke-linecap="round" />
</svg>
</button>
</span>
@endif
@if ($categoryFilter !== 'all')
@php
$activeCat = $categoryOptions->firstWhere('id', (int) $categoryFilter);
$activeCatName = $activeCat?->translations->firstWhere('locale', 'de')?->name
?? '#'.$categoryFilter;
@endphp
<span class="active-chip">
<span>{{ __('Kategorie') }}: <strong>{{ $activeCatName }}</strong></span>
<button type="button" class="x" wire:click="$set('categoryFilter', 'all')"
aria-label="{{ __('Filter entfernen') }}">
<svg width="8" height="8" viewBox="0 0 12 12" fill="none">
<path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.6"
stroke-linecap="round" />
</svg>
</button>
</span>
@endif
@if ($userFilter !== 'all')
@php $activeUser = $userLookupResults->firstWhere('id', (int) $userFilter); @endphp
<span class="active-chip">
<span>{{ __('User') }}:
<strong>{{ $activeUser?->name ?? '#'.$userFilter }}</strong></span>
<button type="button" class="x" wire:click="clearUserFilter"
aria-label="{{ __('Filter entfernen') }}">
<svg width="8" height="8" viewBox="0 0 12 12" fill="none">
<path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.6"
stroke-linecap="round" />
</svg>
</button>
</span>
@endif
@if ($companyFilter !== 'all')
@php $activeCompany = $companyLookupResults->firstWhere('id', (int) $companyFilter); @endphp
<span class="active-chip">
<span>{{ __('Firma') }}:
<strong>{{ $activeCompany?->name ?? '#'.$companyFilter }}</strong></span>
<button type="button" class="x" wire:click="clearCompanyFilter"
aria-label="{{ __('Filter entfernen') }}">
<svg width="8" height="8" viewBox="0 0 12 12" fill="none">
<path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.6"
stroke-linecap="round" />
</svg>
</button>
</span>
@endif
@if ($contactFilter !== 'all')
@php
$activeContact = $contactLookupResults->firstWhere('id', (int) $contactFilter);
$contactName = $activeContact
? trim(($activeContact->first_name ?? '').' '.($activeContact->last_name ?? ''))
: '';
$contactName = $contactName !== '' ? $contactName : '#'.$contactFilter;
@endphp
<span class="active-chip">
<span>{{ __('Kontakt') }}: <strong>{{ $contactName }}</strong></span>
<button type="button" class="x" wire:click="clearContactFilter"
aria-label="{{ __('Filter entfernen') }}">
<svg width="8" height="8" viewBox="0 0 12 12" fill="none">
<path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.6"
stroke-linecap="round" />
</svg>
</button>
</span>
@endif
<button type="button" wire:click="resetFilters"
class="text-[11.5px] font-semibold text-[color:var(--color-ink-3)] hover:text-[color:var(--color-hub)] underline-offset-[3px] hover:underline">
{{ __('Alle zurücksetzen') }}
</button>
</div>
@endif
</div>
</article>
@ -588,86 +791,135 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Aktionen') }}</flux:table.column>
<flux:table.column class="w-[200px]" sortable :sorted="$sortBy === 'status'" :direction="$sortDir"
wire:click="sort('status')">{{ __('Status') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'title'" :direction="$sortDir"
wire:click="sort('title')">{{ __('Titel') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'created_at'" :direction="$sortDir"
wire:click="sort('created_at')">{{ __('Erstellt') }}</flux:table.column>
<flux:table.column>{{ __('Kategorie') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'status'" :direction="$sortDir"
wire:click="sort('status')">{{ __('Status') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'portal'" :direction="$sortDir"
wire:click="sort('portal')">{{ __('Portal') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'hits'" :direction="$sortDir"
wire:click="sort('hits')">
{{ __('Hits') }}</flux:table.column>
<flux:table.column>{{ __('Aktionen') }}</flux:table.column>
<flux:table.column class="w-[200px]" sortable :sorted="$sortBy === 'portal'"
:direction="$sortDir" wire:click="sort('portal')">{{ __('Portal') }}</flux:table.column>
<flux:table.column class="w-[180px]">{{ __('Kategorie') }}</flux:table.column>
<flux:table.column class="w-[130px]" sortable :sorted="$sortBy === 'created_at'"
:direction="$sortDir" wire:click="sort('created_at')">{{ __('Datum') }}</flux:table.column>
<flux:table.column class="w-[80px]" sortable :sorted="$sortBy === 'hits'" :direction="$sortDir"
wire:click="sort('hits')">{{ __('Hits') }}</flux:table.column>
<flux:table.column class="w-[90px]">{{ __('Aktionen') }}</flux:table.column>
</flux:table.columns>
@forelse($pressReleases as $pr)
<flux:table.row wire:key="{{ $pr->id }}">
@php
$status = $pr->status->value;
$rowClass = match ($status) {
'review' => 'is-row-warn',
'rejected' => 'is-row-err',
default => '',
};
$badgeClass = match ($status) {
'published' => 'ok',
'review' => 'warn',
'rejected' => 'err',
'archived', 'draft' => 'muted',
default => 'hub',
};
$portal = $pr->portal?->value ?? 'both';
$showPe = in_array($portal, ['presseecho', 'both'], true);
$showBp = in_array($portal, ['businessportal24', 'both'], true);
$dateSubLabel = match ($status) {
'published' => __('veröffentlicht'),
'review' => __('eingereicht'),
'rejected' => __('abgelehnt'),
'draft' => __('erstellt'),
'archived' => __('archiviert'),
default => __('erstellt'),
};
$primaryDate = $status === 'published' && $pr->published_at
? $pr->published_at
: $pr->created_at;
@endphp
<flux:table.row wire:key="{{ $pr->id }}" class="{{ $rowClass }}">
<flux:table.cell>
<div class="flex items-center gap-1.5 flex-wrap">
<span class="badge {{ $badgeClass }} dot">{{ $pr->status->label() }}</span>
@if ($pr->status === \App\Enums\PressReleaseStatus::Review)
<flux:modal.trigger name="confirm-index-publish-{{ $pr->id }}">
<button type="button" class="inline-action"
title="{{ __('Pressemitteilung veröffentlichen') }}">
{{ __('Freigeben →') }}
</button>
</flux:modal.trigger>
<flux:modal.trigger name="confirm-index-reject-{{ $pr->id }}">
<button type="button" class="inline-action err"
title="{{ __('Pressemitteilung ablehnen') }}">
{{ __('Ablehnen →') }}
</button>
</flux:modal.trigger>
@elseif ($pr->status === \App\Enums\PressReleaseStatus::Published)
<flux:modal.trigger name="confirm-index-archive-{{ $pr->id }}">
<button type="button" class="inline-action warn"
title="{{ __('Pressemitteilung archivieren') }}">
{{ __('Archivieren →') }}
</button>
</flux:modal.trigger>
@endif
</div>
</flux:table.cell>
<flux:table.cell>
<a href="{{ route('admin.press-releases.show', $pr->id) }}" wire:navigate
class="block font-semibold text-[13.5px] leading-[1.35] text-[color:var(--color-ink)] hover:text-[color:var(--color-hub)] hover:underline underline-offset-[3px] decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)] truncate">
{{ $pr->title ?? '—' }}
</a>
<div class="text-[11.5px] text-[color:var(--color-ink-3)] mt-0.5 leading-[1.4]">
PM-{{ $pr->id }}
@if ($pr->company)
· {{ $pr->company->name }}
@endif
· {{ strtoupper($pr->language) }}
</div>
</flux:table.cell>
<flux:table.cell>
<div class="flex flex-wrap gap-1">
@if ($showPe)
<span class="portal-pill pe"><span class="pdot"></span>presseecho</span>
@endif
@if ($showBp)
<span class="portal-pill bp"><span class="pdot"></span>businessportal24</span>
@endif
</div>
</flux:table.cell>
<flux:table.cell>
@php
$categoryName = $pr->category?->translations->firstWhere('locale', 'de')?->name ?? '';
@endphp
<span class="text-[12px] text-[color:var(--color-ink-2)] truncate block"
title="{{ $categoryName }}">
{{ $categoryName }}
</span>
</flux:table.cell>
<flux:table.cell>
<div class="font-mono text-[12px] text-[color:var(--color-ink-2)] tabular-nums">
{{ $primaryDate?->format('d.m.Y') }}
</div>
<div class="text-[10.5px] text-[color:var(--color-ink-4)] mt-0.5">
{{ $dateSubLabel }} · {{ $primaryDate?->format('H:i') }}
</div>
</flux:table.cell>
<flux:table.cell>
<span
class="font-mono text-[12px] text-[color:var(--color-ink-2)] tabular-nums">{{ number_format($pr->hits) }}</span>
</flux:table.cell>
<flux:table.cell>
<div class="flex items-center gap-1">
<flux:button size="sm" variant="ghost" icon="eye"
href="{{ route('admin.press-releases.show', $pr->id) }}" wire:navigate />
href="{{ route('admin.press-releases.show', $pr->id) }}" wire:navigate
title="{{ __('Ansehen') }}" />
<flux:button size="sm" variant="ghost" icon="pencil"
href="{{ route('admin.press-releases.edit', $pr->id) }}" wire:navigate />
</div>
</flux:table.cell>
<flux:table.cell>
<div class="max-w-xs">
<p class="truncate font-medium">{{ $pr->title ?? '' }}</p>
<p class="text-sm truncate text-zinc-400">
{{ $pr->company?->name ?? '' . ' | ' . strtoupper($pr->language) }}
</p>
</div>
</flux:table.cell>
<flux:table.cell>
<flux:text class="text-sm text-zinc-500">{{ $pr->created_at->format('d.m.Y H:i') }}</flux:text>
</flux:table.cell>
<flux:table.cell>
@php($categoryName = $pr->category?->translations->firstWhere('locale', 'de')?->name ?? '')
<div class="max-w-48">
<flux:text class="truncate text-sm" title="{{ $categoryName }}">{{ $categoryName }}
</flux:text>
</div>
</flux:table.cell>
<flux:table.cell>
<span @class([
'badge',
'ok' => $pr->status->value === 'published',
'warn' => $pr->status->value === 'review',
'err' => $pr->status->value === 'rejected',
'hub' => in_array($pr->status->value, ['archived', 'draft'], true),
])>
{{ $pr->status->label() }}
</span>
</flux:table.cell>
<flux:table.cell>
<flux:text class="text-sm">{{ $pr->portal->label() }}</flux:text>
</flux:table.cell>
<flux:table.cell>
<flux:text class="text-sm">{{ number_format($pr->hits) }}</flux:text>
</flux:table.cell>
<flux:table.cell>
<div class="flex items-center gap-1">
@if ($pr->status === \App\Enums\PressReleaseStatus::Review)
<flux:modal.trigger name="confirm-index-publish-{{ $pr->id }}">
<flux:button size="sm" variant="ghost" icon="check-circle"
class="text-green-600" />
</flux:modal.trigger>
<flux:modal.trigger name="confirm-index-reject-{{ $pr->id }}">
<flux:button size="sm" variant="ghost" icon="x-circle" class="text-red-600" />
</flux:modal.trigger>
@endif
@if ($pr->status === \App\Enums\PressReleaseStatus::Published)
<flux:modal.trigger name="confirm-index-archive-{{ $pr->id }}">
<flux:button size="sm" variant="ghost" icon="archive-box"
class="text-zinc-500" />
</flux:modal.trigger>
@endif
href="{{ route('admin.press-releases.edit', $pr->id) }}" wire:navigate
title="{{ __('Bearbeiten') }}" />
</div>
@if ($pr->status === \App\Enums\PressReleaseStatus::Review)
@ -733,19 +985,44 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="8">
<div class="flex flex-col items-center justify-center py-12 text-center">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-4
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.newspaper class="size-6" />
<flux:table.cell colspan="7">
@if ($hasAnyFilter)
<div class="empty-stage">
<div class="empty-ico warm">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
<path d="M4 6h16M7 12h10M10 18h4" stroke="currentColor" stroke-width="1.8"
stroke-linecap="round" />
</svg>
</div>
<h3 class="empty-title">
{{ __('Keine Pressemitteilungen mit diesen Filtern') }}
</h3>
<p class="empty-sub">
{{ __('Passen Sie die Filter an oder setzen Sie alle Filter zurück, um wieder alle PMs zu sehen.') }}
</p>
<div class="flex items-center gap-2.5 mt-6">
<flux:button variant="primary" wire:click="resetFilters">
{{ __('Filter zurücksetzen') }}
</flux:button>
</div>
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Keine Pressemitteilungen gefunden') }}
@else
<div class="empty-stage">
<div class="empty-ico">
<flux:icon.newspaper class="size-7" />
</div>
<h3 class="empty-title">{{ __('Noch keine Pressemitteilungen') }}</h3>
<p class="empty-sub">
{{ __('Sobald Kunden PMs anlegen oder einreichen, erscheinen sie hier zur Bearbeitung.') }}
</p>
<div class="flex items-center gap-2.5 mt-6">
<flux:button variant="primary" icon="plus"
href="{{ route('admin.press-releases.create') }}" wire:navigate>
{{ __('Neue PM anlegen') }}
</flux:button>
</div>
</div>
<p class="text-[12px] text-[color:var(--color-ink-3)] max-w-md m-0">
{{ __('Passen Sie die Filter an oder erstellen Sie eine neue Pressemitteilung.') }}
</p>
</div>
@endif
</flux:table.cell>
</flux:table.row>
@endforelse

View file

@ -8,19 +8,19 @@
<flux:table.column>{{ __('Queries') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@forelse($rows as $row)
@forelse ($rows as $row)
<flux:table.row :key="$row['value']">
<flux:table.cell><flux:text class="font-mono text-xs">{{ $row['value'] }}</flux:text></flux:table.cell>
<flux:table.cell>{{ number_format($row['requests']) }}</flux:table.cell>
<flux:table.cell>{{ number_format($row['average_duration_ms'], 2, ',', '.') }} ms</flux:table.cell>
<flux:table.cell>{{ number_format($row['max_duration_ms']) }} ms</flux:table.cell>
<flux:table.cell>{{ number_format($row['average_database_time_ms'], 2, ',', '.') }} ms</flux:table.cell>
<flux:table.cell>{{ number_format($row['total_queries']) }}</flux:table.cell>
<flux:table.cell><span class="text-[12px] text-[color:var(--color-ink)] font-mono">{{ $row['value'] }}</span></flux:table.cell>
<flux:table.cell><span class="text-[12.5px] text-[color:var(--color-ink)] tabular-nums">{{ number_format($row['requests']) }}</span></flux:table.cell>
<flux:table.cell><span class="text-[12px] text-[color:var(--color-ink-2)] tabular-nums">{{ number_format($row['average_duration_ms'], 2, ',', '.') }} ms</span></flux:table.cell>
<flux:table.cell><span class="text-[12px] text-[color:var(--color-ink-2)] tabular-nums">{{ number_format($row['max_duration_ms']) }} ms</span></flux:table.cell>
<flux:table.cell><span class="text-[12px] text-[color:var(--color-ink-2)] tabular-nums">{{ number_format($row['average_database_time_ms'], 2, ',', '.') }} ms</span></flux:table.cell>
<flux:table.cell><span class="text-[12px] text-[color:var(--color-ink-2)] tabular-nums">{{ number_format($row['total_queries']) }}</span></flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="6">
<div class="py-8 text-center text-sm text-zinc-500">{{ __('Keine Daten gefunden.') }}</div>
<div class="py-8 text-center text-[12px] text-[color:var(--color-ink-3)]">{{ __('Keine Daten gefunden.') }}</div>
</flux:table.cell>
</flux:table.row>
@endforelse

View file

@ -51,112 +51,129 @@ new #[Layout('components.layouts.app'), Title('Performance Reports')] class exte
}
}; ?>
<div class="space-y-6">
<flux:card>
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div>
<flux:heading size="lg">{{ __('Performance Reports') }}</flux:heading>
<flux:subheading>
{{ __('Auswertung der Slow-Admin-Request-Logs aus dem admin_slow Log-Kanal.') }}
</flux:subheading>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Reports') }}</span>
</div>
<div class="text-sm text-zinc-500 dark:text-zinc-400">
{{ __('Logdateien') }}: {{ $report['summary']['files'] }}
</div>
</div>
</flux:card>
<flux:card>
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-6">
<flux:field>
<flux:label>{{ __('Von') }}</flux:label>
<flux:input type="datetime-local" wire:model.live.debounce.500ms="from" />
</flux:field>
<flux:field>
<flux:label>{{ __('Bis') }}</flux:label>
<flux:input type="datetime-local" wire:model.live.debounce.500ms="to" />
</flux:field>
<flux:field>
<flux:label>{{ __('Route') }}</flux:label>
<flux:input wire:model.live.debounce.500ms="routeFilter" placeholder="admin.users" />
</flux:field>
<flux:field>
<flux:label>{{ __('Pfad') }}</flux:label>
<flux:input wire:model.live.debounce.500ms="pathFilter" placeholder="/admin/users" />
</flux:field>
<flux:field>
<flux:label>{{ __('Status') }}</flux:label>
<flux:select wire:model.live="statusFilter">
<option value="">{{ __('Alle') }}</option>
<option value="200">200</option>
<option value="302">302</option>
<option value="403">403</option>
<option value="422">422</option>
<option value="500">500</option>
</flux:select>
</flux:field>
<flux:field>
<flux:label>{{ __('Min. Dauer') }}</flux:label>
<flux:input type="number" min="0" wire:model.live.debounce.500ms="minDurationMs" placeholder="ms" />
</flux:field>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Performance Reports') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Auswertung der Slow-Admin-Request-Logs aus dem admin_slow Log-Kanal.') }}
</p>
</div>
<div class="mt-4 flex items-center justify-between gap-3">
<flux:field class="max-w-36">
<flux:label>{{ __('Detailzeilen') }}</flux:label>
<flux:select wire:model.live="limit">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</flux:select>
</flux:field>
<div class="flex items-center gap-2 flex-shrink-0">
<span class="badge hub">{{ __('Logdateien') }}: {{ $report['summary']['files'] }}</span>
</div>
</header>
<flux:button variant="ghost" icon="arrow-path" wire:click="resetFilters">
{{-- ============== FILTER-PANEL ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Filter') }}</span>
<flux:button size="sm" variant="ghost" icon="arrow-path" wire:click="resetFilters">
{{ __('Filter zurücksetzen') }}
</flux:button>
</div>
</flux:card>
<div class="p-5 space-y-4">
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-6">
<flux:field>
<flux:label>{{ __('Von') }}</flux:label>
<flux:input type="datetime-local" wire:model.live.debounce.500ms="from" />
</flux:field>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
<flux:card>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Requests') }}</flux:text>
<flux:text size="xl" weight="bold">{{ number_format($report['summary']['total_requests']) }}</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Ø Dauer') }}</flux:text>
<flux:text size="xl" weight="bold">{{ number_format($report['summary']['average_duration_ms'], 2, ',', '.') }} ms</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Max. Dauer') }}</flux:text>
<flux:text size="xl" weight="bold">{{ number_format($report['summary']['max_duration_ms']) }} ms</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Max. Queries') }}</flux:text>
<flux:text size="xl" weight="bold">{{ number_format($report['summary']['max_query_count']) }}</flux:text>
</flux:card>
</div>
<flux:field>
<flux:label>{{ __('Bis') }}</flux:label>
<flux:input type="datetime-local" wire:model.live.debounce.500ms="to" />
</flux:field>
<flux:field>
<flux:label>{{ __('Route') }}</flux:label>
<flux:input wire:model.live.debounce.500ms="routeFilter" placeholder="admin.users" />
</flux:field>
<flux:field>
<flux:label>{{ __('Pfad') }}</flux:label>
<flux:input wire:model.live.debounce.500ms="pathFilter" placeholder="/admin/users" />
</flux:field>
<flux:field>
<flux:label>{{ __('Status') }}</flux:label>
<flux:select wire:model.live="statusFilter">
<option value="">{{ __('Alle') }}</option>
<option value="200">200</option>
<option value="302">302</option>
<option value="403">403</option>
<option value="422">422</option>
<option value="500">500</option>
</flux:select>
</flux:field>
<flux:field>
<flux:label>{{ __('Min. Dauer') }}</flux:label>
<flux:input type="number" min="0" wire:model.live.debounce.500ms="minDurationMs" placeholder="ms" />
</flux:field>
</div>
<div class="flex items-center justify-end gap-3 pt-2 border-t border-[color:var(--color-bg-rule)]">
<flux:field class="max-w-36">
<flux:label>{{ __('Detailzeilen') }}</flux:label>
<flux:select wire:model.live="limit">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</flux:select>
</flux:field>
</div>
</div>
</article>
{{-- ============== KPI-Reihe ============== --}}
<section class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
<x-portal.stat-card variant="primary" :label="__('Requests')" :value="number_format($report['summary']['total_requests'])">
<x-slot:meta>{{ __('im Sample') }}</x-slot:meta>
<x-slot:trend>{{ __('Slow-Log') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Ø Dauer')" :value="number_format($report['summary']['average_duration_ms'], 2, ',', '.').' ms'">
<x-slot:meta>{{ __('Mittelwert') }}</x-slot:meta>
<x-slot:trend>{{ __('alle Requests') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="warn" :label="__('Max. Dauer')" :value="number_format($report['summary']['max_duration_ms']).' ms'">
<x-slot:meta>{{ __('Spitze') }}</x-slot:meta>
<x-slot:trend>{{ __('schlechtester Wert') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="warn" :label="__('Max. Queries')" :value="number_format($report['summary']['max_query_count'])">
<x-slot:meta>{{ __('je Request') }}</x-slot:meta>
<x-slot:trend>{{ __('N+1-Indikator') }}</x-slot:trend>
</x-portal.stat-card>
</section>
<div class="grid gap-6 xl:grid-cols-2">
<flux:card class="overflow-hidden">
<flux:heading size="md" class="mb-4">{{ __('Top Routen') }}</flux:heading>
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Top Routen') }}</span>
</div>
@include('livewire.admin.reports.slow-requests-table', ['rows' => $report['top_routes'], 'label' => __('Route')])
</flux:card>
</article>
<flux:card class="overflow-hidden">
<flux:heading size="md" class="mb-4">{{ __('Top Pfade') }}</flux:heading>
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Top Pfade') }}</span>
</div>
@include('livewire.admin.reports.slow-requests-table', ['rows' => $report['top_paths'], 'label' => __('Pfad')])
</flux:card>
</article>
</div>
<flux:card class="overflow-hidden">
<flux:heading size="md" class="mb-4">{{ __('Langsamste Requests') }}</flux:heading>
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Langsamste Requests') }}</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Zeit') }}</flux:table.column>
@ -168,29 +185,31 @@ new #[Layout('components.layouts.app'), Title('Performance Reports')] class exte
<flux:table.column>{{ __('Queries') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@forelse($report['slowest_requests'] as $entry)
@forelse ($report['slowest_requests'] as $entry)
<flux:table.row :key="$entry['timestamp'].'-'.$entry['route_name'].'-'.$entry['duration_ms']">
<flux:table.cell>{{ $entry['timestamp'] }}</flux:table.cell>
<flux:table.cell><flux:text class="font-mono text-xs">{{ $entry['route_name'] }}</flux:text></flux:table.cell>
<flux:table.cell><flux:text class="font-mono text-xs">{{ $entry['path'] }}</flux:text></flux:table.cell>
<flux:table.cell><flux:badge color="zinc" size="sm">{{ $entry['status_code'] }}</flux:badge></flux:table.cell>
<flux:table.cell>{{ number_format($entry['duration_ms']) }} ms</flux:table.cell>
<flux:table.cell>{{ number_format($entry['database_time_ms'], 2, ',', '.') }} ms</flux:table.cell>
<flux:table.cell>{{ number_format($entry['query_count']) }}</flux:table.cell>
<flux:table.cell><span class="text-[12px] text-[color:var(--color-ink-3)] font-mono">{{ $entry['timestamp'] }}</span></flux:table.cell>
<flux:table.cell><span class="text-[12px] text-[color:var(--color-ink)] font-mono">{{ $entry['route_name'] }}</span></flux:table.cell>
<flux:table.cell><span class="text-[12px] text-[color:var(--color-ink-2)] font-mono">{{ $entry['path'] }}</span></flux:table.cell>
<flux:table.cell><span class="badge hub">{{ $entry['status_code'] }}</span></flux:table.cell>
<flux:table.cell><span class="text-[12.5px] font-semibold text-[color:var(--color-ink)] tabular-nums">{{ number_format($entry['duration_ms']) }} ms</span></flux:table.cell>
<flux:table.cell><span class="text-[12px] text-[color:var(--color-ink-2)] tabular-nums">{{ number_format($entry['database_time_ms'], 2, ',', '.') }} ms</span></flux:table.cell>
<flux:table.cell><span class="text-[12px] text-[color:var(--color-ink)] tabular-nums">{{ number_format($entry['query_count']) }}</span></flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="7">
<div class="py-8 text-center text-sm text-zinc-500">{{ __('Keine Slow-Admin-Requests gefunden.') }}</div>
<div class="py-8 text-center text-[12px] text-[color:var(--color-ink-3)]">{{ __('Keine Slow-Admin-Requests gefunden.') }}</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
</flux:card>
</article>
<flux:card class="overflow-hidden">
<flux:heading size="md" class="mb-4">{{ __('Häufige Slow Queries') }}</flux:heading>
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Häufige Slow Queries') }}</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('SQL') }}</flux:table.column>
@ -199,51 +218,52 @@ new #[Layout('components.layouts.app'), Title('Performance Reports')] class exte
<flux:table.column>{{ __('Max. Zeit') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@forelse($report['slow_queries'] as $query)
@forelse ($report['slow_queries'] as $query)
<flux:table.row :key="md5($query['sql'])">
<flux:table.cell><flux:text class="font-mono text-xs">{{ str($query['sql'])->limit(160) }}</flux:text></flux:table.cell>
<flux:table.cell>{{ number_format($query['occurrences']) }}</flux:table.cell>
<flux:table.cell>{{ number_format($query['average_time_ms'], 2, ',', '.') }} ms</flux:table.cell>
<flux:table.cell>{{ number_format($query['max_time_ms'], 2, ',', '.') }} ms</flux:table.cell>
<flux:table.cell><span class="text-[12px] text-[color:var(--color-ink-2)] font-mono">{{ str($query['sql'])->limit(160) }}</span></flux:table.cell>
<flux:table.cell><span class="text-[12.5px] text-[color:var(--color-ink)] tabular-nums">{{ number_format($query['occurrences']) }}</span></flux:table.cell>
<flux:table.cell><span class="text-[12px] text-[color:var(--color-ink-2)] tabular-nums">{{ number_format($query['average_time_ms'], 2, ',', '.') }} ms</span></flux:table.cell>
<flux:table.cell><span class="text-[12px] text-[color:var(--color-ink-2)] tabular-nums">{{ number_format($query['max_time_ms'], 2, ',', '.') }} ms</span></flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="4">
<div class="py-8 text-center text-sm text-zinc-500">{{ __('Keine einzelnen Slow Queries im Sample gefunden.') }}</div>
<div class="py-8 text-center text-[12px] text-[color:var(--color-ink-3)]">{{ __('Keine einzelnen Slow Queries im Sample gefunden.') }}</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
</flux:card>
</article>
<flux:card class="overflow-hidden">
<flux:heading size="md" class="mb-4">{{ __('EXPLAIN Top Slow Queries') }}</flux:heading>
<div class="space-y-6">
@forelse($report['explain_plans'] as $explain)
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('EXPLAIN Top Slow Queries') }}</span>
</div>
<div class="p-5 space-y-6">
@forelse ($report['explain_plans'] as $explain)
<div class="space-y-3">
<flux:text class="font-mono text-xs">{{ str($explain['sql'])->limit(180) }}</flux:text>
<div class="text-[12px] text-[color:var(--color-ink-2)] font-mono">{{ str($explain['sql'])->limit(180) }}</div>
@if($explain['error'])
<flux:badge color="amber" size="sm">{{ $explain['error'] }}</flux:badge>
@elseif($explain['plan'] === [])
<flux:text class="text-sm text-zinc-500">{{ __('Kein Explain-Plan zurückgegeben.') }}</flux:text>
@if ($explain['error'])
<span class="badge warn dot">{{ $explain['error'] }}</span>
@elseif ($explain['plan'] === [])
<p class="text-[12px] text-[color:var(--color-ink-3)] m-0">{{ __('Kein Explain-Plan zurückgegeben.') }}</p>
@else
<div class="overflow-x-auto">
<table class="w-full text-left text-xs">
<thead class="border-b border-zinc-200 text-zinc-500 dark:border-zinc-700 dark:text-zinc-400">
<div class="overflow-x-auto rounded-[5px] border border-[color:var(--color-bg-rule)]">
<table class="w-full text-left text-[11.5px]">
<thead class="border-b border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] text-[10.5px] uppercase tracking-[0.5px] text-[color:var(--color-ink-3)]">
<tr>
@foreach(array_keys($explain['plan'][0]) as $column)
<th class="px-3 py-2 font-medium">{{ $column }}</th>
@foreach (array_keys($explain['plan'][0]) as $column)
<th class="px-3 py-2 font-semibold">{{ $column }}</th>
@endforeach
</tr>
</thead>
<tbody>
@foreach($explain['plan'] as $planRow)
<tr class="border-b border-zinc-100 dark:border-zinc-800">
@foreach($planRow as $value)
<td class="px-3 py-2 font-mono">{{ is_scalar($value) || $value === null ? (string) $value : json_encode($value) }}</td>
@foreach ($explain['plan'] as $planRow)
<tr class="border-b border-[color:var(--color-bg-rule)] last:border-b-0">
@foreach ($planRow as $value)
<td class="px-3 py-2 font-mono text-[color:var(--color-ink-2)]">{{ is_scalar($value) || $value === null ? (string) $value : json_encode($value) }}</td>
@endforeach
</tr>
@endforeach
@ -253,8 +273,8 @@ new #[Layout('components.layouts.app'), Title('Performance Reports')] class exte
@endif
</div>
@empty
<div class="py-8 text-center text-sm text-zinc-500">{{ __('Keine Slow Queries für EXPLAIN vorhanden.') }}</div>
<div class="py-8 text-center text-[12px] text-[color:var(--color-ink-3)]">{{ __('Keine Slow Queries für EXPLAIN vorhanden.') }}</div>
@endforelse
</div>
</flux:card>
</article>
</div>

View file

@ -75,36 +75,56 @@ new #[Layout('components.layouts.app'), Title('Neue Rolle')] class extends Compo
}
}; ?>
<form wire:submit="save" class="space-y-6">
<flux:card>
<div class="flex items-center justify-between gap-4">
<div>
<flux:heading size="lg">{{ __('Neue Rolle') }}</flux:heading>
<flux:subheading>{{ __('Guard') }}: {{ $guardName }}</flux:subheading>
<form wire:submit="save" class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Sicherheit · Rollen') }}</span>
<span class="badge hub">{{ __('Guard') }}: {{ $guardName }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Neue Rolle') }}
</h1>
</div>
</flux:card>
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Basis-Informationen') }}</flux:heading>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.roles.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</header>
<flux:field>
<flux:label>{{ __('Technischer Name') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="name" placeholder="{{ __('z.B. editor') }}" />
<flux:description>{{ __('Kleinbuchstaben, keine Leerzeichen. Wird intern verwendet.') }}</flux:description>
<flux:error name="name" />
</flux:field>
</flux:card>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Basis-Informationen') }}</span>
</div>
<div class="p-5">
<flux:field>
<flux:label>
{{ __('Technischer Name') }}
<span class="text-[color:var(--color-err)]">*</span>
</flux:label>
<flux:input wire:model="name" placeholder="{{ __('z.B. editor') }}" />
<flux:description>{{ __('Kleinbuchstaben, keine Leerzeichen. Wird intern verwendet.') }}</flux:description>
<flux:error name="name" />
</flux:field>
</div>
</article>
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Berechtigungen') }}</flux:heading>
<div class="space-y-6">
@forelse($permissionGroups as $groupName => $permissionsInGroup)
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Berechtigungen') }}</span>
</div>
<div class="p-5 space-y-6">
@forelse ($permissionGroups as $groupName => $permissionsInGroup)
<div>
<flux:heading size="md" class="mb-3">{{ $groupName }}</flux:heading>
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-3">
{{ $groupName }}
</div>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
@foreach($permissionsInGroup as $permission)
@foreach ($permissionsInGroup as $permission)
<flux:checkbox
wire:model="permissions"
value="{{ $permission['name'] }}"
@ -114,15 +134,15 @@ new #[Layout('components.layouts.app'), Title('Neue Rolle')] class extends Compo
</div>
</div>
@empty
<flux:text class="text-sm text-zinc-500">{{ __('Keine Berechtigungen fuer diesen Guard vorhanden.') }}</flux:text>
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">{{ __('Keine Berechtigungen fuer diesen Guard vorhanden.') }}</p>
@endforelse
<flux:error name="permissions" />
</div>
</article>
<flux:error name="permissions" class="mt-4" />
</flux:card>
<flux:card>
<div class="flex justify-end gap-3">
<article class="panel">
<div class="p-5 flex justify-end gap-3">
<flux:button variant="ghost" href="{{ route('admin.roles.index') }}" wire:navigate>
{{ __('Abbrechen') }}
</flux:button>
@ -130,5 +150,5 @@ new #[Layout('components.layouts.app'), Title('Neue Rolle')] class extends Compo
{{ __('Rolle erstellen') }}
</flux:button>
</div>
</flux:card>
</article>
</form>

View file

@ -97,47 +97,69 @@ new #[Layout('components.layouts.app'), Title('Rolle bearbeiten')] class extends
}
}; ?>
<form wire:submit="save" class="space-y-6">
<flux:card>
<div class="flex items-center justify-between gap-4">
<div>
<flux:heading size="lg">{{ __('Rolle bearbeiten') }}</flux:heading>
<flux:subheading>{{ __('ID') }}: {{ $id }} | {{ __('Guard') }}: {{ $guardName }}</flux:subheading>
<form wire:submit="save" class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Sicherheit · Rollen') }}</span>
<span class="badge hub">ID #{{ $id }}</span>
<span class="badge hub">{{ __('Guard') }}: {{ $guardName }}</span>
@if ($isSystemRole)
<span class="badge hub dot">{{ __('Systemrolle') }}</span>
@endif
</div>
@if($isSystemRole)
<flux:badge color="purple" size="sm">{{ __('Systemrolle') }}</flux:badge>
@endif
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Rolle bearbeiten') }}
</h1>
</div>
</flux:card>
@if($isSystemRole)
<flux:card>
<flux:text class="text-sm text-zinc-600 dark:text-zinc-300">
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.roles.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</header>
@if ($isSystemRole)
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-start gap-3
bg-[color:var(--color-warn-soft)] border-[color:var(--color-warn)]/30 text-[color:var(--color-ink-2)]">
<flux:icon.exclamation-triangle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-accent-deep)]" />
<div class="flex-1">
{{ __('Hinweis: Diese Rolle ist Teil des Basis-Setups. Aenderungen wirken sich direkt auf den Admin-Zugriff aus.') }}
</flux:text>
</flux:card>
</div>
</div>
@endif
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Basis-Informationen') }}</flux:heading>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Basis-Informationen') }}</span>
</div>
<div class="p-5">
<flux:field>
<flux:label>
{{ __('Technischer Name') }}
<span class="text-[color:var(--color-err)]">*</span>
</flux:label>
<flux:input wire:model="name" />
<flux:error name="name" />
</flux:field>
</div>
</article>
<flux:field>
<flux:label>{{ __('Technischer Name') }} <span class="text-red-500">*</span></flux:label>
<flux:input wire:model="name" />
<flux:error name="name" />
</flux:field>
</flux:card>
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Berechtigungen') }}</flux:heading>
<div class="space-y-6">
@forelse($permissionGroups as $groupName => $permissionsInGroup)
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Berechtigungen') }}</span>
</div>
<div class="p-5 space-y-6">
@forelse ($permissionGroups as $groupName => $permissionsInGroup)
<div>
<flux:heading size="md" class="mb-3">{{ $groupName }}</flux:heading>
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-3">
{{ $groupName }}
</div>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
@foreach($permissionsInGroup as $permission)
@foreach ($permissionsInGroup as $permission)
<flux:checkbox
wire:model="permissions"
value="{{ $permission['name'] }}"
@ -147,15 +169,15 @@ new #[Layout('components.layouts.app'), Title('Rolle bearbeiten')] class extends
</div>
</div>
@empty
<flux:text class="text-sm text-zinc-500">{{ __('Keine Berechtigungen fuer diesen Guard vorhanden.') }}</flux:text>
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">{{ __('Keine Berechtigungen fuer diesen Guard vorhanden.') }}</p>
@endforelse
<flux:error name="permissions" />
</div>
</article>
<flux:error name="permissions" class="mt-4" />
</flux:card>
<flux:card>
<div class="flex justify-end gap-3">
<article class="panel">
<div class="p-5 flex justify-end gap-3">
<flux:button variant="ghost" href="{{ route('admin.roles.index') }}" wire:navigate>
{{ __('Abbrechen') }}
</flux:button>
@ -163,5 +185,5 @@ new #[Layout('components.layouts.app'), Title('Rolle bearbeiten')] class extends
{{ __('Aenderungen speichern') }}
</flux:button>
</div>
</flux:card>
</article>
</form>

View file

@ -18,23 +18,38 @@ new #[Layout('components.layouts.app'), Title('Rollen & Rechte')] class extends
}
}; ?>
<div class="space-y-6">
<flux:card>
<div class="flex items-center justify-between gap-4">
<div>
<flux:heading size="lg">{{ __('Rollen') }}</flux:heading>
<flux:subheading>{{ __('Verwaltung von Rollen und Berechtigungen') }}</flux:subheading>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Sicherheit') }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Rollen') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Verwaltung von Rollen und Berechtigungen') }}
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
@if (\Illuminate\Support\Facades\Route::has('admin.roles.create'))
<flux:button icon="plus" href="{{ route('admin.roles.create') }}" wire:navigate>
<flux:button variant="primary" icon="plus" href="{{ route('admin.roles.create') }}" wire:navigate>
{{ __('Neue Rolle') }}
</flux:button>
@endif
</div>
</flux:card>
</header>
<flux:card class="overflow-hidden">
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Alle Rollen') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __(':count Einträge', ['count' => $roles->count()]) }}
</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Name') }}</flux:table.column>
@ -45,28 +60,28 @@ new #[Layout('components.layouts.app'), Title('Rollen & Rechte')] class extends
</flux:table.columns>
<flux:table.rows>
@forelse($roles as $role)
@forelse ($roles as $role)
<flux:table.row :key="$role->id">
<flux:table.cell>
<div>
<flux:text weight="semibold">{{ str($role->name)->replace('-', ' ')->title() }}</flux:text>
<flux:text class="text-xs text-zinc-500">{{ $role->name }}</flux:text>
<div class="text-[13px] font-semibold text-[color:var(--color-ink)]">{{ str($role->name)->replace('-', ' ')->title() }}</div>
<div class="text-[11.5px] text-[color:var(--color-ink-3)] font-mono">{{ $role->name }}</div>
</div>
</flux:table.cell>
<flux:table.cell>
<flux:badge color="zinc" size="sm">{{ $role->users_count }}</flux:badge>
<span class="badge dot">{{ $role->users_count }}</span>
</flux:table.cell>
<flux:table.cell>
<flux:badge color="blue" size="sm">{{ $role->permissions_count }}</flux:badge>
<span class="badge hub">{{ $role->permissions_count }}</span>
</flux:table.cell>
<flux:table.cell>
@if(in_array($role->name, ['admin', 'editor', 'customer', 'api-only'], true))
<flux:badge color="purple" size="sm">{{ __('System') }}</flux:badge>
@if (in_array($role->name, ['admin', 'editor', 'customer', 'api-only'], true))
<span class="badge hub dot">{{ __('System') }}</span>
@else
<flux:badge color="green" size="sm">{{ __('Custom') }}</flux:badge>
<span class="badge ok dot">{{ __('Custom') }}</span>
@endif
</flux:table.cell>
@ -79,14 +94,19 @@ new #[Layout('components.layouts.app'), Title('Rollen & Rechte')] class extends
@empty
<flux:table.row>
<flux:table.cell colspan="5">
<div class="flex flex-col items-center justify-center py-10">
<flux:icon.shield-check class="size-10 text-zinc-400" />
<flux:text class="mt-3 text-zinc-500">{{ __('Keine Rollen gefunden') }}</flux:text>
<div class="flex flex-col items-center justify-center px-4 py-10 text-center">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-3
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.shield-check class="size-6" />
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Keine Rollen gefunden') }}
</div>
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
</flux:card>
</article>
</div>

View file

@ -302,41 +302,59 @@ new #[Layout('components.layouts.app'), Title('Benutzer')] class extends Compone
}
}; ?>
<div class="space-y-6">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Gesamt') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['total'] }}</flux:text>
</div>
<flux:icon.users class="size-8 text-blue-500" />
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · User-Verwaltung') }}</span>
</div>
</flux:card>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Benutzer') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Alle Konten der Plattform mit Rollen, Berechtigungen und Migrations-/Pflegefällen.') }}
</p>
</div>
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Aktiv') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['active'] }}</flux:text>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="primary" icon="plus" href="{{ route('admin.users.create') }}" wire:navigate>
{{ __('Benutzer anlegen') }}
</flux:button>
</div>
</header>
{{-- ============== KPI-Reihe ============== --}}
<section class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<x-portal.stat-card variant="primary" :label="__('Gesamt')" :value="number_format($stats['total'])">
<x-slot:meta>{{ __('User-Konten') }}</x-slot:meta>
<x-slot:trend>{{ __('alle Portale') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="ok" :label="__('Aktiv')" :value="number_format($stats['active'])">
<x-slot:meta>{{ __('Login möglich') }}</x-slot:meta>
<x-slot:trend>{{ __('Produktiv') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Inaktiv')" :value="number_format($stats['inactive'])">
<x-slot:meta>{{ __('gesperrt / archiviert') }}</x-slot:meta>
<x-slot:trend>{{ __('kein Login') }}</x-slot:trend>
</x-portal.stat-card>
</section>
{{-- ============== FILTER-PANEL ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Filter & Suche') }}</span>
@if ($search || $activeFilter !== 'all' || $portalFilter !== 'all' || $roleFilter !== 'all' || $qualityFilter !== 'all' || $permissionFilter !== 'all')
<div class="flex items-center gap-2">
<span class="badge hub dot">{{ __('Filter aktiv') }}</span>
<flux:button size="sm" variant="ghost" icon="arrow-path" type="button" wire:click="resetFilters">
{{ __('Zurücksetzen') }}
</flux:button>
</div>
<flux:icon.check-circle class="size-8 text-green-500" />
</div>
</flux:card>
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:text class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Inaktiv') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['inactive'] }}</flux:text>
</div>
<flux:icon.x-circle class="size-8 text-red-500" />
</div>
</flux:card>
</div>
<flux:card>
<div class="flex flex-col gap-4">
@endif
</div>
<div class="p-5 space-y-4">
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<flux:input wire:model.live.debounce.300ms="search" placeholder="{{ __('Name oder E-Mail suchen...') }}"
icon="magnifying-glass" class="lg:max-w-sm" />
@ -357,7 +375,7 @@ new #[Layout('components.layouts.app'), Title('Benutzer')] class extends Compone
<flux:select wire:model.live="roleFilter" class="sm:w-44">
<option value="all">{{ __('Alle Rollen') }}</option>
@foreach($availableRoles as $roleName)
@foreach ($availableRoles as $roleName)
<option value="{{ $roleName }}">{{ $roleName }}</option>
@endforeach
</flux:select>
@ -383,28 +401,22 @@ new #[Layout('components.layouts.app'), Title('Benutzer')] class extends Compone
</div>
</div>
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div class="flex flex-wrap gap-2">
@if($search || $activeFilter !== 'all' || $portalFilter !== 'all' || $roleFilter !== 'all' || $qualityFilter !== 'all' || $permissionFilter !== 'all')
<flux:badge color="blue" size="sm">{{ __('Filter aktiv') }}</flux:badge>
<flux:button size="sm" variant="ghost" type="button" wire:click="resetFilters">
{{ __('Zurücksetzen') }}
</flux:button>
@else
<flux:text class="text-sm text-zinc-500">
{{ __('Nutze die Filter, um offene Migrations- und Pflegefälle schnell zu finden.') }}
</flux:text>
@endif
</div>
<flux:button icon="plus" href="{{ route('admin.users.create') }}" wire:navigate>
{{ __('Benutzer anlegen') }}
</flux:button>
</div>
@if (! ($search || $activeFilter !== 'all' || $portalFilter !== 'all' || $roleFilter !== 'all' || $qualityFilter !== 'all' || $permissionFilter !== 'all'))
<p class="text-[12px] text-[color:var(--color-ink-3)] m-0">
{{ __('Nutze die Filter, um offene Migrations- und Pflegefälle schnell zu finden.') }}
</p>
@endif
</div>
</flux:card>
</article>
<flux:card class="overflow-hidden">
{{-- ============== TABELLE ============== --}}
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Alle Benutzer') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __(':count Einträge', ['count' => $users->count()]) }}
</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Aktionen') }}</flux:table.column>
@ -558,9 +570,14 @@ new #[Layout('components.layouts.app'), Title('Benutzer')] class extends Compone
@empty
<flux:table.row>
<flux:table.cell colspan="9">
<div class="flex flex-col items-center justify-center py-12">
<flux:icon.users class="size-12 text-zinc-400 dark:text-zinc-600" />
<flux:text class="mt-4 text-zinc-500">{{ __('Keine Benutzer gefunden') }}</flux:text>
<div class="flex flex-col items-center justify-center px-4 py-10 text-center">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-3
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.users class="size-6" />
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Keine Benutzer gefunden') }}
</div>
</div>
</flux:table.cell>
</flux:table.row>
@ -568,10 +585,10 @@ new #[Layout('components.layouts.app'), Title('Benutzer')] class extends Compone
</flux:table.rows>
</flux:table>
<div class="border-t border-zinc-200 p-4 dark:border-zinc-700">
<div class="border-t border-[color:var(--color-bg-rule)] p-4">
{{ $users->links() }}
</div>
</flux:card>
</article>
<flux:modal name="user-details" class="w-full max-w-5xl" scroll="body">
<div class="space-y-6">

View file

@ -229,14 +229,34 @@ new #[Layout('components.layouts.app'), Title('Benutzer anlegen')] class extends
}
}; ?>
<form wire:submit="save" class="space-y-6">
<flux:card>
<flux:heading size="lg">{{ __('Benutzer anlegen') }}</flux:heading>
<flux:subheading>{{ __('Rollen, Firmen und optional Rechnungsadresse direkt mitsetzen.') }}</flux:subheading>
</flux:card>
<form wire:submit="save" class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Benutzer · Neu') }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Benutzer anlegen') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Rollen, Firmen und optional Rechnungsadresse direkt mitsetzen.') }}
</p>
</div>
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Basisdaten') }}</flux:heading>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.users.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</header>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Basisdaten') }}</span>
</div>
<div class="p-5">
<div class="grid gap-4 sm:grid-cols-2">
<flux:field>
@ -276,20 +296,28 @@ new #[Layout('components.layouts.app'), Title('Benutzer anlegen')] class extends
<flux:checkbox wire:model="isActive" label="{{ __('Aktiv') }}" />
<flux:checkbox wire:model="isSuperAdmin" label="{{ __('Super Admin') }}" />
</div>
</flux:card>
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Rollenzuweisung') }}</flux:heading>
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
@foreach($availableRoles as $role)
<flux:checkbox wire:model="selectedRoles" value="{{ $role->name }}" label="{{ $role->name }}" />
@endforeach
</div>
<flux:error name="selectedRoles" class="mt-4" />
</flux:card>
</article>
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Firmenverknüpfung') }}</flux:heading>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Rollenzuweisung') }}</span>
</div>
<div class="p-5">
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
@foreach ($availableRoles as $role)
<flux:checkbox wire:model="selectedRoles" value="{{ $role->name }}" label="{{ $role->name }}" />
@endforeach
</div>
<flux:error name="selectedRoles" class="mt-4" />
</div>
</article>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Firmenverknüpfung') }}</span>
</div>
<div class="p-5">
<div class="mb-4">
<flux:select
@ -324,11 +352,11 @@ new #[Layout('components.layouts.app'), Title('Benutzer anlegen')] class extends
</div>
<div class="space-y-3">
@forelse($linkedCompanies as $company)
<div class="grid gap-3 rounded-lg border border-zinc-200 p-3 dark:border-zinc-700 sm:grid-cols-[1fr,160px,auto] sm:items-center">
@forelse ($linkedCompanies as $company)
<div class="grid gap-3 rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3 sm:grid-cols-[1fr,160px,auto] sm:items-center">
<div>
<flux:text weight="semibold">{{ $company->name }}</flux:text>
<flux:text class="text-sm text-zinc-500">{{ $company->slug }}</flux:text>
<div class="text-[13px] font-semibold text-[color:var(--color-ink)]">{{ $company->name }}</div>
<div class="text-[11px] text-[color:var(--color-ink-3)] font-mono">{{ $company->slug }}</div>
</div>
<flux:select wire:model="companyRoles.{{ $company->id }}">
@ -337,23 +365,23 @@ new #[Layout('components.layouts.app'), Title('Benutzer anlegen')] class extends
<option value="owner">{{ __('Owner') }}</option>
</flux:select>
<flux:button
size="sm"
variant="ghost"
icon="x-mark"
wire:click="removeLinkedCompany({{ $company->id }})"
>
<flux:button size="sm" variant="ghost" icon="x-mark"
wire:click="removeLinkedCompany({{ $company->id }})">
{{ __('Entfernen') }}
</flux:button>
</div>
@empty
<flux:text class="text-sm text-zinc-500">{{ __('Noch keine Firma verknüpft.') }}</flux:text>
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">{{ __('Noch keine Firma verknüpft.') }}</p>
@endforelse
</div>
</flux:card>
</div>
</article>
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Rechnungsadresse (optional)') }}</flux:heading>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Rechnungsadresse (optional)') }}</span>
</div>
<div class="p-5">
<div class="grid gap-3 sm:grid-cols-2">
<flux:field>
@ -400,22 +428,23 @@ new #[Layout('components.layouts.app'), Title('Benutzer anlegen')] class extends
<flux:label>{{ __('Land') }}</flux:label>
<flux:select wire:model="billing.country_code">
<option value="">{{ __('Bitte wählen') }}</option>
@foreach($countries as $countryCode => $countryName)
@foreach ($countries as $countryCode => $countryName)
<option value="{{ $countryCode }}">{{ $countryName }}</option>
@endforeach
</flux:select>
</flux:field>
</div>
</flux:card>
</div>
</article>
<flux:card>
<div class="flex justify-end gap-3">
<article class="panel">
<div class="p-5 flex justify-end gap-3">
<flux:button variant="ghost" href="{{ route('admin.users.index') }}" wire:navigate>
{{ __('Abbrechen') }}
</flux:button>
<flux:button type="submit" variant="primary">
<flux:button type="submit" variant="primary" icon="check">
{{ __('Benutzer anlegen') }}
</flux:button>
</div>
</flux:card>
</article>
</form>

View file

@ -715,25 +715,40 @@ new #[Layout('components.layouts.app'), Title('Benutzer bearbeiten')] class exte
}
}; ?>
<form wire:submit="save" class="space-y-6">
<flux:card>
<div class="flex items-center justify-between">
<div>
<flux:heading size="lg">{{ __('Benutzer bearbeiten') }}</flux:heading>
<flux:subheading>ID: {{ $id }}</flux:subheading>
<form wire:submit="save" class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Benutzer · Bearbeiten') }}</span>
<span class="badge hub">ID #{{ $id }}</span>
<span class="badge hub">{{ strtoupper($portal) }}</span>
@if ($isActive)
<span class="badge ok dot">{{ __('Aktiv') }}</span>
@else
<span class="badge err dot">{{ __('Inaktiv') }}</span>
@endif
</div>
<flux:badge color="zinc" size="sm">{{ strtoupper($portal) }}</flux:badge>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)] break-words">
{{ $name ?: __('Benutzer bearbeiten') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ $email }}
</p>
</div>
</flux:card>
@if($notification)
<div
x-data="{ show: true }"
x-init="setTimeout(() => show = false, 3000)"
x-show="show"
x-transition
class="rounded-md bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 px-4 py-3 text-sm text-green-800 dark:text-green-300"
>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.users.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</header>
@if ($notification)
<div x-data="{ show: true }" x-init="setTimeout(() => show = false, 4000)" x-show="show" x-transition
class="rounded-[5px] border-l-[3px] px-4 py-3 text-[12.5px]"
style="border-color: var(--color-ok); background: color-mix(in oklab, var(--color-ok) 10%, var(--color-bg)); color: var(--color-ink);">
{{ $notification }}
</div>
@endif
@ -754,70 +769,72 @@ new #[Layout('components.layouts.app'), Title('Benutzer bearbeiten')] class exte
&& filled($billing['country_code']);
@endphp
<flux:card>
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div>
<flux:heading size="lg">{{ __('Bearbeitungsflow') }}</flux:heading>
<flux:text class="mt-1 text-sm text-zinc-500">
{{ __('Die wichtigsten Pflegebereiche sind hier zusammengefasst. Springe direkt in den Abschnitt, der noch Arbeit braucht.') }}
</flux:text>
</div>
<div class="flex flex-wrap gap-2">
<flux:button size="sm" variant="ghost" href="#account">{{ __('Account') }}</flux:button>
<flux:button size="sm" variant="ghost" href="#legacy-profile">{{ __('Profil') }}</flux:button>
<flux:button size="sm" variant="ghost" href="#company-links">{{ __('Firmen') }}</flux:button>
<flux:button size="sm" variant="ghost" href="#contact-links">{{ __('Kontakte') }}</flux:button>
<flux:button size="sm" variant="ghost" href="#billing-address">{{ __('Rechnung') }}</flux:button>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Bearbeitungsflow') }}</span>
<div class="flex flex-wrap gap-1.5">
<a href="#account"
class="px-2.5 py-1 text-[11px] font-medium rounded-[4px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] text-[color:var(--color-ink-2)] hover:text-[color:var(--color-hub)] hover:border-[color:var(--color-hub)]/40 transition">{{ __('Account') }}</a>
<a href="#legacy-profile"
class="px-2.5 py-1 text-[11px] font-medium rounded-[4px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] text-[color:var(--color-ink-2)] hover:text-[color:var(--color-hub)] hover:border-[color:var(--color-hub)]/40 transition">{{ __('Profil') }}</a>
<a href="#company-links"
class="px-2.5 py-1 text-[11px] font-medium rounded-[4px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] text-[color:var(--color-ink-2)] hover:text-[color:var(--color-hub)] hover:border-[color:var(--color-hub)]/40 transition">{{ __('Firmen') }}</a>
<a href="#contact-links"
class="px-2.5 py-1 text-[11px] font-medium rounded-[4px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] text-[color:var(--color-ink-2)] hover:text-[color:var(--color-hub)] hover:border-[color:var(--color-hub)]/40 transition">{{ __('Kontakte') }}</a>
<a href="#billing-address"
class="px-2.5 py-1 text-[11px] font-medium rounded-[4px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] text-[color:var(--color-ink-2)] hover:text-[color:var(--color-hub)] hover:border-[color:var(--color-hub)]/40 transition">{{ __('Rechnung') }}</a>
</div>
</div>
<div class="p-5">
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0 mb-4">
{{ __('Die wichtigsten Pflegebereiche sind hier zusammengefasst. Springe direkt in den Abschnitt, der noch Arbeit braucht.') }}
</p>
<div class="mt-4 grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
<div class="rounded-lg border border-zinc-200 p-3 dark:border-zinc-700">
<flux:text class="text-xs text-zinc-500">{{ __('Account') }}</flux:text>
<div class="mt-2 flex flex-wrap gap-2">
<flux:badge :color="$isActive ? 'green' : 'red'" size="sm">
{{ $isActive ? __('Aktiv') : __('Inaktiv') }}
</flux:badge>
<flux:badge color="zinc" size="sm">{{ strtoupper($portal) }}</flux:badge>
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[11px] text-[color:var(--color-ink-3)] mb-2 uppercase tracking-[0.04em]">{{ __('Account') }}</div>
<div class="flex flex-wrap gap-2">
@if ($isActive)
<span class="badge ok dot">{{ __('Aktiv') }}</span>
@else
<span class="badge err dot">{{ __('Inaktiv') }}</span>
@endif
<span class="badge hub">{{ strtoupper($portal) }}</span>
</div>
</div>
</div>
<div class="rounded-lg border border-zinc-200 p-3 dark:border-zinc-700">
<flux:text class="text-xs text-zinc-500">{{ __('Legacy-Profil') }}</flux:text>
<div class="mt-2">
@if($hasProfileInput)
<flux:badge color="green" size="sm">{{ __('Profil vorhanden') }}</flux:badge>
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[11px] text-[color:var(--color-ink-3)] mb-2 uppercase tracking-[0.04em]">{{ __('Legacy-Profil') }}</div>
@if ($hasProfileInput)
<span class="badge ok dot">{{ __('Profil vorhanden') }}</span>
@else
<flux:badge color="amber" size="sm">{{ __('Profil fehlt') }}</flux:badge>
<span class="badge warn dot">{{ __('Profil fehlt') }}</span>
@endif
</div>
</div>
<div class="rounded-lg border border-zinc-200 p-3 dark:border-zinc-700">
<flux:text class="text-xs text-zinc-500">{{ __('Verknüpfungen') }}</flux:text>
<div class="mt-2 flex flex-wrap gap-2">
<flux:badge :color="count($linkedCompanyIds) > 0 ? 'green' : 'amber'" size="sm">
{{ trans_choice(':count Firma|:count Firmen', count($linkedCompanyIds), ['count' => count($linkedCompanyIds)]) }}
</flux:badge>
<flux:badge :color="count($contactForms) > 0 ? 'blue' : 'zinc'" size="sm">
{{ trans_choice(':count Kontakt|:count Kontakte', count($contactForms), ['count' => count($contactForms)]) }}
</flux:badge>
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[11px] text-[color:var(--color-ink-3)] mb-2 uppercase tracking-[0.04em]">{{ __('Verknüpfungen') }}</div>
<div class="flex flex-wrap gap-2">
<span class="badge {{ count($linkedCompanyIds) > 0 ? 'ok' : 'warn' }} dot">
{{ trans_choice(':count Firma|:count Firmen', count($linkedCompanyIds), ['count' => count($linkedCompanyIds)]) }}
</span>
<span class="badge hub">
{{ trans_choice(':count Kontakt|:count Kontakte', count($contactForms), ['count' => count($contactForms)]) }}
</span>
</div>
</div>
</div>
<div class="rounded-lg border border-zinc-200 p-3 dark:border-zinc-700">
<flux:text class="text-xs text-zinc-500">{{ __('Rechnungsadresse') }}</flux:text>
<div class="mt-2">
@if($billingComplete)
<flux:badge color="green" size="sm">{{ __('Vollständig') }}</flux:badge>
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[11px] text-[color:var(--color-ink-3)] mb-2 uppercase tracking-[0.04em]">{{ __('Rechnungsadresse') }}</div>
@if ($billingComplete)
<span class="badge ok dot">{{ __('Vollständig') }}</span>
@else
<flux:badge color="zinc" size="sm">{{ __('Unvollständig') }}</flux:badge>
<span class="badge warn dot">{{ __('Unvollständig') }}</span>
@endif
</div>
</div>
</div>
</flux:card>
</article>
<flux:card id="account">
<flux:heading size="lg" class="mb-4">{{ __('Basisdaten') }}</flux:heading>
@ -1211,14 +1228,14 @@ new #[Layout('components.layouts.app'), Title('Benutzer bearbeiten')] class exte
</div>
</flux:card>
<flux:card>
<div class="flex justify-end gap-3">
<article class="panel">
<div class="p-5 flex justify-end gap-3">
<flux:button variant="ghost" href="{{ route('admin.users.index') }}" wire:navigate>
{{ __('Abbrechen') }}
</flux:button>
<flux:button type="submit" variant="primary">
<flux:button type="submit" variant="primary" icon="check">
{{ __('Speichern') }}
</flux:button>
</div>
</flux:card>
</article>
</form>

View file

@ -57,112 +57,129 @@ new #[Layout('components.layouts.app'), Title('Benutzer anzeigen')] class extend
}
}; ?>
<div class="space-y-6">
@if (!$user)
<flux:card>
<flux:heading size="lg">{{ __('Benutzer nicht gefunden') }}</flux:heading>
<flux:button class="mt-4" href="{{ route('admin.users.index') }}" wire:navigate>
{{ __('Zurück zur Übersicht') }}
</flux:button>
</flux:card>
@else
<flux:card>
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<flux:heading size="xl">{{ $user->name }}</flux:heading>
<flux:text class="text-sm text-zinc-500">{{ $user->email }}</flux:text>
<flux:text class="text-xs text-zinc-500">ID: {{ $user->id }}</flux:text>
</div>
<div class="flex gap-2">
<flux:button variant="ghost" href="{{ route('admin.users.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
<flux:button icon="pencil" href="{{ route('admin.users.edit', $user->id) }}" wire:navigate>
{{ __('Bearbeiten') }}
</flux:button>
<div class="space-y-8">
@if (! $user)
<article class="panel">
<div class="p-10 flex flex-col items-center justify-center text-center">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-3
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.exclamation-triangle class="size-6" />
</div>
<div class="text-[16px] font-semibold text-[color:var(--color-ink)] mb-2">{{ __('Benutzer nicht gefunden') }}</div>
<flux:button variant="primary" href="{{ route('admin.users.index') }}" wire:navigate>
{{ __('Zurück zur Übersicht') }}
</flux:button>
</div>
</article>
@else
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Administration · Benutzer') }}</span>
<span class="badge hub">ID #{{ $user->id }}</span>
@if ($user->is_active)
<span class="badge ok dot">{{ __('Aktiv') }}</span>
@else
<span class="badge err dot">{{ __('Inaktiv') }}</span>
@endif
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)] break-words">
{{ $user->name }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">{{ $user->email }}</p>
</div>
</flux:card>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-4">
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Portal') }}</flux:text>
<flux:text weight="semibold">{{ $user->portal?->label() ?? '-' }}</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Typ') }}</flux:text>
<flux:text weight="semibold">{{ $user->registration_type?->label() ?? '-' }}</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Status') }}</flux:text>
@if ($user->is_active)
<flux:badge color="green" size="sm">{{ __('Aktiv') }}</flux:badge>
@else
<flux:badge color="red" size="sm">{{ __('Inaktiv') }}</flux:badge>
@endif
</flux:card>
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Letzter Login') }}</flux:text>
<flux:text weight="semibold">{{ $user->last_login_at?->format('d.m.Y H:i') ?? __('Nie') }}</flux:text>
@if ($user->last_login_ip)
<flux:text class="text-xs text-zinc-500">{{ $user->last_login_ip }}</flux:text>
@endif
</flux:card>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.users.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
<flux:button variant="primary" icon="pencil" href="{{ route('admin.users.edit', $user->id) }}" wire:navigate>
{{ __('Bearbeiten') }}
</flux:button>
</div>
</header>
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Rollen') }}</flux:heading>
<div class="flex flex-wrap gap-2">
@forelse($user->roles as $role)
<flux:badge color="zinc" size="sm">{{ $role->name }}</flux:badge>
{{-- ============== KPI-Reihe ============== --}}
<section class="grid grid-cols-1 gap-4 sm:grid-cols-4">
<x-portal.stat-card variant="muted" :label="__('Portal')" :value="$user->portal?->label() ?? '-'">
<x-slot:meta>{{ __('Zuordnung') }}</x-slot:meta>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Typ')" :value="$user->registration_type?->label() ?? '-'">
<x-slot:meta>{{ __('Registrierung') }}</x-slot:meta>
</x-portal.stat-card>
@if ($user->is_active)
<x-portal.stat-card variant="ok" :label="__('Status')" :value="__('Aktiv')">
<x-slot:meta>{{ __('Login möglich') }}</x-slot:meta>
</x-portal.stat-card>
@else
<x-portal.stat-card variant="warn" :label="__('Status')" :value="__('Inaktiv')">
<x-slot:meta>{{ __('gesperrt') }}</x-slot:meta>
</x-portal.stat-card>
@endif
<x-portal.stat-card variant="primary" :label="__('Letzter Login')" :value="$user->last_login_at?->format('d.m.Y H:i') ?? __('Nie')">
<x-slot:meta>{{ $user->last_login_ip ?? __('keine IP') }}</x-slot:meta>
</x-portal.stat-card>
</section>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Rollen') }}</span>
</div>
<div class="p-5 flex flex-wrap gap-2">
@forelse ($user->roles as $role)
<span class="badge hub">{{ $role->name }}</span>
@empty
<flux:text class="text-sm text-zinc-500">{{ __('Keine Rollen hinterlegt') }}</flux:text>
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">{{ __('Keine Rollen hinterlegt') }}</p>
@endforelse
</div>
</flux:card>
</article>
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Rechnungsadresse') }}</flux:heading>
@if ($user->billingAddress)
<div class="space-y-1">
<flux:text>{{ $user->billingAddress->name }}</flux:text>
<flux:text>{{ $user->billingAddress->address1 }}</flux:text>
@if ($user->billingAddress->address2)
<flux:text>{{ $user->billingAddress->address2 }}</flux:text>
@endif
<flux:text>{{ $user->billingAddress->postal_code }} {{ $user->billingAddress->city }}</flux:text>
<flux:text>{{ $user->billingAddress->country_code }}</flux:text>
</div>
@else
<flux:text class="text-sm text-zinc-500">{{ __('Keine Rechnungsadresse hinterlegt') }}</flux:text>
@endif
</flux:card>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Rechnungsadresse') }}</span>
</div>
<div class="p-5">
@if ($user->billingAddress)
<div class="space-y-1 text-[12.5px] text-[color:var(--color-ink)]">
<div class="font-semibold">{{ $user->billingAddress->name }}</div>
<div>{{ $user->billingAddress->address1 }}</div>
@if ($user->billingAddress->address2)
<div>{{ $user->billingAddress->address2 }}</div>
@endif
<div>{{ $user->billingAddress->postal_code }} {{ $user->billingAddress->city }}</div>
<div>{{ $user->billingAddress->country_code }}</div>
</div>
@else
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">{{ __('Keine Rechnungsadresse hinterlegt') }}</p>
@endif
</div>
</article>
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Verknüpfte Firmen und Kontakte') }}</flux:heading>
<flux:subheading class="mb-4">
{{ __('Kontakte sind die Ansprechpartner der verknüpften Firmen (wie in der Bearbeiten-Ansicht).') }}
</flux:subheading>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Verknüpfte Firmen und Kontakte') }}</span>
</div>
<div class="p-5 space-y-4">
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">
{{ __('Kontakte sind die Ansprechpartner der verknüpften Firmen (wie in der Bearbeiten-Ansicht).') }}
</p>
<div class="space-y-4">
@forelse($user->companies as $company)
<div class="rounded-lg border border-zinc-200 p-4 dark:border-zinc-700">
@forelse ($user->companies as $company)
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-4">
<div class="mb-3 flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<div class="min-w-0 flex-1">
@if (\Illuminate\Support\Facades\Route::has('admin.companies.show'))
<a href="{{ route('admin.companies.show', $company->id) }}" wire:navigate
class="block">
<flux:text weight="semibold"
class="text-blue-600 hover:underline dark:text-blue-400">
{{ $company->name }}
</flux:text>
class="text-[13px] font-semibold text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
{{ $company->name }}
</a>
@else
<flux:text weight="semibold">{{ $company->name }}</flux:text>
<div class="text-[13px] font-semibold text-[color:var(--color-ink)]">{{ $company->name }}</div>
@endif
<flux:text class="text-xs text-zinc-500">{{ $company->slug }}</flux:text>
<div class="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-zinc-500">
<div class="text-[11px] text-[color:var(--color-ink-3)] font-mono">{{ $company->slug }}</div>
<div class="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11.5px] text-[color:var(--color-ink-3)]">
@if ($company->email)
<span>{{ $company->email }}</span>
@endif
@ -172,14 +189,10 @@ new #[Layout('components.layouts.app'), Title('Benutzer anzeigen')] class extend
</div>
</div>
<div class="flex flex-wrap items-center gap-2">
<flux:badge color="zinc" size="sm">
{{ $this->companyUserRoleLabel($company->pivot?->role ?? 'member') }}
</flux:badge>
<flux:badge color="zinc" size="sm">
{{ $company->portal?->label() ?? '—' }}
</flux:badge>
@if (!$company->is_active)
<flux:badge color="red" size="sm">{{ __('Inaktiv') }}</flux:badge>
<span class="badge hub">{{ $this->companyUserRoleLabel($company->pivot?->role ?? 'member') }}</span>
<span class="badge hub">{{ $company->portal?->label() ?? '—' }}</span>
@if (! $company->is_active)
<span class="badge err dot">{{ __('Inaktiv') }}</span>
@endif
</div>
</div>
@ -187,19 +200,18 @@ new #[Layout('components.layouts.app'), Title('Benutzer anzeigen')] class extend
@if ($company->contacts->isNotEmpty())
<div class="space-y-2">
@foreach ($company->contacts as $contact)
<div
class="flex flex-col gap-2 rounded-md bg-zinc-50 p-2 dark:bg-zinc-900 sm:flex-row sm:items-center sm:justify-between">
<div class="flex flex-col gap-2 rounded-[4px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-2 sm:flex-row sm:items-center sm:justify-between">
<div class="min-w-0">
<flux:text class="text-sm" weight="medium">
{{ trim(($contact->first_name ?? '') . ' ' . ($contact->last_name ?? '')) ?: __('Kontakt ohne Name') }}
</flux:text>
<flux:text class="text-xs text-zinc-500">
<div class="text-[12.5px] font-medium text-[color:var(--color-ink)]">
{{ trim(($contact->first_name ?? '').' '.($contact->last_name ?? '')) ?: __('Kontakt ohne Name') }}
</div>
<div class="text-[11px] text-[color:var(--color-ink-3)]">
{{ $contact->responsibility ?? __('Keine Rolle hinterlegt') }}
</flux:text>
<div class="mt-0.5 flex flex-wrap items-center gap-2 text-xs text-zinc-500">
</div>
<div class="mt-0.5 flex flex-wrap items-center gap-2 text-[11px] text-[color:var(--color-ink-3)]">
@if ($contact->email)
<a href="mailto:{{ $contact->email }}"
class="text-blue-600 hover:underline dark:text-blue-400">{{ $contact->email }}</a>
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>
@ -207,8 +219,7 @@ new #[Layout('components.layouts.app'), Title('Benutzer anzeigen')] class extend
</div>
</div>
<div class="flex shrink-0 items-center gap-2">
<flux:badge color="zinc" size="sm">
{{ $contact->portal?->label() ?? '—' }}</flux:badge>
<span class="badge hub">{{ $contact->portal?->label() ?? '—' }}</span>
@if (\Illuminate\Support\Facades\Route::has('admin.contacts.edit'))
<flux:button size="sm" variant="ghost" icon="pencil"
href="{{ route('admin.contacts.edit', $contact->id) }}"
@ -220,20 +231,19 @@ new #[Layout('components.layouts.app'), Title('Benutzer anzeigen')] class extend
</div>
@endforeach
@if ($company->contacts_count > $company->contacts->count())
<flux:text class="text-xs text-zinc-500">
<p class="text-[11.5px] text-[color:var(--color-ink-3)] m-0">
{{ __(':count weitere Kontakte werden hier nicht geladen. Öffne die Firma, um alle Kontakte zu sehen.', ['count' => $company->contacts_count - $company->contacts->count()]) }}
</flux:text>
</p>
@endif
</div>
@else
<flux:text class="text-xs text-zinc-500">{{ __('Keine Kontakte bei dieser Firma') }}
</flux:text>
<p class="text-[11.5px] text-[color:var(--color-ink-3)] m-0">{{ __('Keine Kontakte bei dieser Firma') }}</p>
@endif
</div>
@empty
<flux:text class="text-sm text-zinc-500">{{ __('Keine Firmen verknüpft') }}</flux:text>
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">{{ __('Keine Firmen verknüpft') }}</p>
@endforelse
</div>
</flux:card>
</article>
@endif
</div>

View file

@ -1,153 +0,0 @@
<?php
use Illuminate\Auth\Events\Lockout;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Validate;
use Livewire\Volt\Component;
new #[Layout('components.layouts.auth')] class extends Component {
#[Validate('required|string|email')]
public string $email = '';
#[Validate('required|string')]
public string $password = '';
public bool $remember = false;
/**
* Handle an incoming authentication request.
*/
public function login(): void
{
$this->validate();
$this->ensureIsNotRateLimited();
if (! Auth::attempt(['email' => $this->email, 'password' => $this->password], $this->remember)) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => __('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
Session::regenerate();
$this->redirectIntended(default: route('dashboard', absolute: false), navigate: true);
}
/**
* Ensure the authentication request is not rate limited.
*/
protected function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
event(new Lockout(request()));
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'email' => __('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}
/**
* Get the authentication rate limiting throttle key.
*/
protected function throttleKey(): string
{
return Str::transliterate(Str::lower($this->email).'|'.request()->ip());
}
}; ?>
<div class="flex flex-col gap-6 p-8 bg-white rounded-lg shadow-lg">
<div class="text-center">
<h1 class="text-2xl font-bold text-gray-900">Log in to your account</h1>
<p class="text-gray-600 mt-2">Enter your email and password below to log in</p>
</div>
<!-- Session Status -->
@if (session('status'))
<div class="text-center text-sm text-green-600">
{{ session('status') }}
</div>
@endif
<form wire:submit="login" class="flex flex-col gap-6">
<!-- Email Address -->
<div>
<label for="email" class="block text-sm font-medium text-gray-700">Email address</label>
<input
wire:model="email"
id="email"
type="email"
required
autofocus
autocomplete="email"
placeholder="email@example.com"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
/>
@error('email')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<!-- Password -->
<div class="relative">
<label for="password" class="block text-sm font-medium text-gray-700">Password</label>
<input
wire:model="password"
id="password"
type="password"
required
autocomplete="current-password"
placeholder="Password"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
/>
@error('password')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
@if (Route::has('password.request'))
<a href="{{ route('password.request') }}" class="absolute right-0 top-0 text-sm text-blue-600 hover:text-blue-500">
Forgot your password?
</a>
@endif
</div>
<!-- Remember Me -->
<div class="flex items-center">
<input
wire:model="remember"
id="remember"
type="checkbox"
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label for="remember" class="ml-2 block text-sm text-gray-900">Remember me</label>
</div>
<button type="submit" class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
Log in
</button>
</form>
@if (Route::has('register'))
<div class="text-center text-sm text-gray-600">
Don't have an account?
<a href="{{ route('register') }}" class="text-blue-600 hover:text-blue-500">Sign up</a>
</div>
@endif
</div>

View file

@ -8,45 +8,64 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
{
}; ?>
<div class="space-y-6">
<flux:card>
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<flux:heading size="lg">{{ __('Buchungen & Add-ons') }}</flux:heading>
<flux:subheading>
{{ __('Hier werden künftig gebuchte Leistungen, Add-ons und Erweiterungen für Ihre Firmen gebündelt.') }}
</flux:subheading>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('User Backend') }}</span>
<span class="eyebrow muted">{{ __('Mein Bereich · Finanzen') }}</span>
<span class="badge warn">{{ __('In Vorbereitung') }}</span>
</div>
<flux:badge color="zinc" icon="shopping-bag" size="lg">
{{ __('In Vorbereitung') }}
</flux:badge>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Buchungen & Add-ons') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Hier werden künftig gebuchte Leistungen, Add-ons und Erweiterungen für Ihre Firmen gebündelt.') }}
</p>
</div>
</flux:card>
</header>
<flux:callout color="blue" icon="information-circle">
{{ __('Der Bereich ist bereits in der Navigation vorbereitet. Buchbare Add-ons werden aktiviert, sobald das Preismodell und die Zahlungslogik final sind.') }}
</flux:callout>
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<flux:card>
<flux:heading size="sm">{{ __('Firmenbezogene Add-ons') }}</flux:heading>
<flux:text class="mt-2 text-sm text-zinc-500">
{{ __('Zum Beispiel zusätzliche Sichtbarkeit, Verifizierung oder besondere Platzierungen.') }}
</flux:text>
</flux:card>
<flux:card>
<flux:heading size="sm">{{ __('Credits & Tarif') }}</flux:heading>
<flux:text class="mt-2 text-sm text-zinc-500">
{{ __('Tarif- und Credit-Informationen folgen, sobald das neue Preismodell live ist.') }}
</flux:text>
</flux:card>
<flux:card>
<flux:heading size="sm">{{ __('Zahlungsarten') }}</flux:heading>
<flux:text class="mt-2 text-sm text-zinc-500">
{{ __('Zahlungsarten werden später unter Finanzen eingebunden.') }}
</flux:text>
</flux:card>
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-start gap-3
bg-[color:var(--color-hub-soft)] border-[color:var(--color-hub-soft-2)] text-[color:var(--color-ink-2)]">
<flux:icon.information-circle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-hub)]" />
<div class="flex-1">
{{ __('Der Bereich ist bereits in der Navigation vorbereitet. Buchbare Add-ons werden aktiviert, sobald das Preismodell und die Zahlungslogik final sind.') }}
</div>
</div>
<section class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Firmenbezogene Add-ons') }}</span>
</div>
<div class="p-5">
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">
{{ __('Zum Beispiel zusätzliche Sichtbarkeit, Verifizierung oder besondere Platzierungen.') }}
</p>
</div>
</article>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Credits & Tarif') }}</span>
</div>
<div class="p-5">
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">
{{ __('Tarif- und Credit-Informationen folgen, sobald das neue Preismodell live ist.') }}
</p>
</div>
</article>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Zahlungsarten') }}</span>
</div>
<div class="p-5">
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">
{{ __('Zahlungsarten werden später unter Finanzen eingebunden.') }}
</p>
</div>
</article>
</section>
</div>

View file

@ -44,15 +44,13 @@ new class extends Component
}; ?>
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-end">
@if($companies->isNotEmpty())
<div class="hidden text-xs font-medium uppercase tracking-wider text-zinc-500 dark:text-zinc-400 sm:block">
{{ __('Aktive Firma') }}
</div>
@if ($companies->isNotEmpty())
<span class="badge hub dot hidden sm:inline-flex">{{ __('Aktive Firma') }}</span>
<div class="min-w-0 sm:w-72">
<flux:select wire:model.live="activeCompany" size="sm">
<option value="all">{{ __('Alle Firmen') }}</option>
@foreach($companies as $company)
@foreach ($companies as $company)
<option value="{{ $company->id }}">
{{ $company->name }} · {{ $context->roleLabelFor($company, $user) }}
</option>
@ -60,15 +58,15 @@ new class extends Component
</flux:select>
</div>
<div class="hidden max-w-48 truncate text-xs text-zinc-500 dark:text-zinc-400 lg:block">
@if($selectedCompany)
<div class="hidden max-w-48 truncate text-[11px] uppercase tracking-[0.6px] font-semibold text-[color:var(--color-ink-3)] lg:block">
@if ($selectedCompany)
{{ $selectedCompany->portal?->label() ?? __('Portal unbekannt') }}
@else
{{ __('Aggregierte Sicht') }}
@endif
</div>
@if($selectedCompany)
@if ($selectedCompany)
<flux:button
size="sm"
variant="ghost"
@ -84,8 +82,6 @@ new class extends Component
</flux:button>
@endif
@else
<div class="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-200">
{{ __('Keine Firma zugeordnet') }}
</div>
<span class="badge warn dot">{{ __('Keine Firma zugeordnet') }}</span>
@endif
</div>

View file

@ -36,7 +36,7 @@ new #[Layout('components.layouts.app'), Title('Mein Dashboard')] class extends C
->with('company:id,name')
->latest()
->limit(5)
->get(['id', 'title', 'status', 'company_id', 'created_at']);
->get(['id', 'title', 'status', 'portal', 'company_id', 'created_at', 'published_at']);
$profile = $user->profile;
$billingAddress = $user->billingAddress;
@ -308,23 +308,42 @@ new #[Layout('components.layouts.app'), Title('Mein Dashboard')] class extends C
</div>
@forelse ($recent as $pr)
@php
$badgeClass = match ($pr->status->value) {
'published' => 'ok',
'review' => 'warn',
'rejected' => 'err',
'archived', 'draft' => 'muted',
default => 'hub',
};
$portal = $pr->portal?->value ?? 'both';
$showPe = in_array($portal, ['presseecho', 'both'], true);
$showBp = in_array($portal, ['businessportal24', 'both'], true);
$primaryDate = $pr->status === PressReleaseStatus::Published && $pr->published_at
? $pr->published_at
: $pr->created_at;
@endphp
<a href="{{ route('me.press-releases.show', $pr->id) }}" wire:navigate
class="flex items-center justify-between gap-3 px-5 py-3 border-b border-[color:var(--color-bg-rule)] last:border-b-0 hover:bg-[color:var(--color-bg)] transition-colors">
class="flex items-center justify-between gap-4 px-5 py-3 border-b border-[color:var(--color-bg-rule)] last:border-b-0 hover:bg-[color:var(--color-bg)] transition-colors">
<div class="min-w-0 flex-1">
<p class="truncate text-[13px] font-medium text-[color:var(--color-ink)] m-0">{{ $pr->title }}</p>
<p class="text-[11px] text-[color:var(--color-ink-3)] mt-0.5 m-0">
{{ $pr->company?->name ?? __('Ohne Firma') }} · {{ $pr->created_at->format('d.m.Y') }}
<p class="text-[11px] text-[color:var(--color-ink-3)] mt-0.5 m-0 truncate">
PM-{{ $pr->id }}
<span class="text-[color:var(--color-ink-4)] mx-1">·</span>
{{ $pr->company?->name ?? __('Ohne Firma') }}
<span class="text-[color:var(--color-ink-4)] mx-1">·</span>
{{ $primaryDate?->format('d.m.Y') }}
</p>
</div>
<span @class([
'badge',
'ok' => $pr->status === PressReleaseStatus::Published,
'warn' => $pr->status === PressReleaseStatus::Review,
'err' => $pr->status === PressReleaseStatus::Rejected,
'hub' => ! in_array($pr->status, [PressReleaseStatus::Published, PressReleaseStatus::Review, PressReleaseStatus::Rejected], true),
])>
{{ $pr->status->label() }}
</span>
<div class="flex items-center gap-1.5 shrink-0">
@if ($showPe)
<span class="portal-pill pe"><span class="pdot"></span>presseecho</span>
@endif
@if ($showBp)
<span class="portal-pill bp"><span class="pdot"></span>businessportal24</span>
@endif
<span class="badge {{ $badgeClass }} dot">{{ $pr->status->label() }}</span>
</div>
</a>
@empty
<div class="px-10 py-14 flex flex-col items-center text-center">

View file

@ -58,74 +58,95 @@ new #[Layout('components.layouts.app'), Title('Rechnungen')] class extends Compo
}
}; ?>
<div class="space-y-6">
<flux:card>
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<flux:heading size="lg">{{ __('Rechnungen') }}</flux:heading>
<flux:subheading>{{ __('Ihr Rechnungsarchiv im User Backend. PDFs werden bei Bedarf aus den Archivdaten erzeugt.') }}</flux:subheading>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('User Backend') }}</span>
<span class="eyebrow muted">{{ __('Mein Bereich · Finanzen') }}</span>
<span class="badge hub">{{ __('Archivdaten') }}</span>
</div>
<flux:badge color="zinc" icon="archive-box" size="lg">
{{ __('Archivdaten') }}
</flux:badge>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Rechnungen') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Ihr Rechnungsarchiv im User Backend. PDFs werden bei Bedarf aus den Archivdaten erzeugt.') }}
</p>
</div>
</flux:card>
<flux:card>
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<flux:heading size="sm">{{ __('Hinweis zu Rechnungen') }}</flux:heading>
<flux:text class="mt-1 text-sm text-zinc-500">
{{ __('Aktuell sehen Sie hier die aus dem Legacy-System übernommenen Rechnungen. Neue Abrechnungen werden später in dieselbe Finanznavigation integriert.') }}
</flux:text>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button size="sm" variant="ghost" icon="user" href="{{ route('me.profile') }}#rechnungsadresse" wire:navigate>
{{ __('Rechnungsadresse im Profil pflegen') }}
</flux:button>
</div>
</flux:card>
</header>
@if($notification)
<flux:callout color="yellow" icon="exclamation-triangle">
{{ $notification }}
</flux:callout>
@if ($notification)
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-start gap-3
bg-[color:var(--color-warn-soft)] border-[color:var(--color-warn)]/30 text-[color:var(--color-ink-2)]">
<flux:icon.exclamation-triangle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-accent-deep)]" />
<div class="flex-1">{{ $notification }}</div>
</div>
@endif
<div class="grid grid-cols-2 gap-4 lg:grid-cols-4">
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Rechnungen') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['count'] }}</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Archivsumme') }}</flux:text>
<flux:text size="xl" weight="bold">{{ number_format($stats['total_cents'] / 100, 2, ',', '.') }} </flux:text>
</flux:card>
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Bezahlt') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['paid_count'] }}</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('PDF-Download') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $stats['downloadable_count'] }}</flux:text>
</flux:card>
</div>
{{-- ============== HINWEIS-PANEL ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Hinweis zu Rechnungen') }}</span>
</div>
<div class="p-5">
<p class="text-[13px] text-[color:var(--color-ink-2)] m-0">
{{ __('Aktuell sehen Sie hier die aus dem Legacy-System übernommenen Rechnungen. Neue Abrechnungen werden später in dieselbe Finanznavigation integriert.') }}
</p>
</div>
</article>
<flux:card>
<div class="flex flex-col gap-3 sm:flex-row">
{{-- ============== KPI-Reihe ============== --}}
<section class="grid grid-cols-2 gap-4 lg:grid-cols-4">
<x-portal.stat-card variant="primary" :label="__('Rechnungen')" :value="number_format($stats['count'])">
<x-slot:meta>{{ __('Archivdatensätze') }}</x-slot:meta>
<x-slot:trend>{{ __('User-spezifisch') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Archivsumme')" :value="number_format($stats['total_cents'] / 100, 2, ',', '.').' €'">
<x-slot:meta>{{ __('historisch') }}</x-slot:meta>
<x-slot:trend>{{ __('alle Portale') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="ok" :label="__('Bezahlt')" :value="number_format($stats['paid_count'])">
<x-slot:meta>{{ __('vollständig') }}</x-slot:meta>
<x-slot:trend>{{ __('mit Zahlungsdatum') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="warn" :label="__('PDF-Download')" :value="number_format($stats['downloadable_count'])">
<x-slot:meta>{{ __('verfügbar') }}</x-slot:meta>
<x-slot:trend>{{ __('aus Archivdaten') }}</x-slot:trend>
</x-portal.stat-card>
</section>
{{-- ============== FILTER-PANEL ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Filter & Suche') }}</span>
</div>
<div class="p-5 flex flex-col gap-3 sm:flex-row">
<flux:input wire:model.live.debounce.300ms="search" placeholder="{{ __('Rechnungsnummer suchen…') }}" icon="magnifying-glass" class="flex-1" />
<flux:select wire:model.live="statusFilter" class="sm:w-48">
<option value="all">{{ __('Alle Status') }}</option>
@foreach($statusOptions as $status)
@foreach ($statusOptions as $status)
<option value="{{ $status }}">{{ $status }}</option>
@endforeach
</flux:select>
</div>
</flux:card>
</article>
<flux:card class="p-0">
<div class="p-4">
<flux:table>
{{-- ============== TABELLE ============== --}}
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Alle Rechnungen') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __(':count Einträge', ['count' => $invoices->count()]) }}
</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Rechnungsnr.') }}</flux:table.column>
<flux:table.column>{{ __('Portal') }}</flux:table.column>
@ -136,27 +157,37 @@ new #[Layout('components.layouts.app'), Title('Rechnungen')] class extends Compo
<flux:table.column>{{ __('PDF') }}</flux:table.column>
</flux:table.columns>
@forelse($invoices as $invoice)
@forelse ($invoices as $invoice)
<flux:table.row wire:key="legacy-invoice-{{ $invoice->id }}">
<flux:table.cell>
<flux:text weight="semibold">{{ $invoice->number ?? ('#'.$invoice->legacy_id) }}</flux:text>
<span class="text-[13px] font-semibold text-[color:var(--color-ink)]">
{{ $invoice->number ?? ('#'.$invoice->legacy_id) }}
</span>
</flux:table.cell>
<flux:table.cell>
<flux:badge size="sm" color="zinc">{{ $invoice->legacy_portal?->label() }}</flux:badge>
<span class="badge hub">{{ $invoice->legacy_portal?->label() }}</span>
</flux:table.cell>
<flux:table.cell>
<flux:text weight="semibold">{{ number_format($invoice->total_cents / 100, 2, ',', '.') }} </flux:text>
<span class="text-[13px] font-semibold text-[color:var(--color-ink)]">
{{ number_format($invoice->total_cents / 100, 2, ',', '.') }}
</span>
</flux:table.cell>
<flux:table.cell>
<flux:badge size="sm" color="{{ $invoice->paid_at ? 'green' : 'yellow' }}">
{{ $invoice->status ?? ($invoice->paid_at ? __('Bezahlt') : __('Offen')) }}
</flux:badge>
@if ($invoice->paid_at)
<span class="badge ok dot">{{ $invoice->status ?? __('Bezahlt') }}</span>
@else
<span class="badge warn dot">{{ $invoice->status ?? __('Offen') }}</span>
@endif
</flux:table.cell>
<flux:table.cell>
<flux:text class="text-sm text-zinc-500">{{ $invoice->invoice_date?->format('d.m.Y') ?? '' }}</flux:text>
<span class="text-[12px] text-[color:var(--color-ink-3)]">
{{ $invoice->invoice_date?->format('d.m.Y') ?? '' }}
</span>
</flux:table.cell>
<flux:table.cell>
<flux:text class="text-sm text-zinc-500">{{ $invoice->paid_at?->format('d.m.Y') ?? '' }}</flux:text>
<span class="text-[12px] text-[color:var(--color-ink-3)]">
{{ $invoice->paid_at?->format('d.m.Y') ?? '' }}
</span>
</flux:table.cell>
<flux:table.cell>
<flux:button
@ -174,11 +205,16 @@ new #[Layout('components.layouts.app'), Title('Rechnungen')] class extends Compo
<flux:table.row>
<flux:table.cell colspan="7">
<div class="flex flex-col items-center justify-center px-4 py-10 text-center">
<flux:icon.document-text class="size-10 text-zinc-300" />
<flux:text weight="semibold" class="mt-3">{{ __('Keine Rechnungen gefunden') }}</flux:text>
<flux:text class="mt-1 max-w-md text-sm text-zinc-500">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-3
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.document-text class="size-6" />
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Keine Rechnungen gefunden') }}
</div>
<p class="text-[12px] text-[color:var(--color-ink-3)] max-w-md m-0">
{{ __('Sobald Rechnungen aus dem Archiv oder aus neuen Buchungen vorhanden sind, erscheinen sie hier.') }}
</flux:text>
</p>
<flux:button class="mt-4" size="sm" variant="ghost" icon="user" href="{{ route('me.profile') }}#rechnungsadresse" wire:navigate>
{{ __('Rechnungsadresse prüfen') }}
</flux:button>
@ -186,9 +222,9 @@ new #[Layout('components.layouts.app'), Title('Rechnungen')] class extends Compo
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table>
</flux:table>
<div class="border-t border-[color:var(--color-bg-rule)] p-4">
{{ $invoices->links() }}
</div>
</flux:card>
{{ $invoices->links() }}
</article>
</div>

View file

@ -44,76 +44,113 @@ new #[Layout('components.layouts.app'), Title('Meine Firmen')] class extends Com
}
}; ?>
<div class="space-y-6">
<flux:card>
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<flux:heading size="lg">{{ __('Meine Firmen') }}</flux:heading>
<flux:subheading>{{ __('Verwalten Sie Firmen, Pressekontakte und zugeordnete Pressemitteilungen.') }}</flux:subheading>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('User Backend') }}</span>
<span class="eyebrow muted">{{ __('Mein Bereich · Firmen') }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Meine Firmen') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Verwalten Sie Firmen, Pressekontakte und zugeordnete Pressemitteilungen.') }}
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="primary" icon="plus" href="{{ route('me.profile') }}" wire:navigate>
{{ __('Firma anlegen anfragen') }}
{{ __('Firma anlegen anfragen') }}
</flux:button>
</div>
</flux:card>
</header>
<flux:card>
<flux:input wire:model.live.debounce.300ms="search" icon="magnifying-glass" placeholder="{{ __('Firma suchen...') }}" />
</flux:card>
{{-- ============== FILTER-PANEL ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Filter & Suche') }}</span>
</div>
<div class="p-5">
<flux:input wire:model.live.debounce.300ms="search" icon="magnifying-glass" placeholder="{{ __('Firma suchen...') }}" />
</div>
</article>
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
@forelse($pressKits as $company)
<flux:card class="space-y-4">
<div class="flex items-start justify-between gap-3">
{{-- ============== FIRMEN-CARDS ============== --}}
<section class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
@forelse ($pressKits as $company)
<article class="panel flex flex-col">
<div class="panel-head">
<div class="min-w-0">
<flux:heading size="sm" class="truncate">{{ $company->name }}</flux:heading>
<flux:text class="mt-1 text-xs text-zinc-500">{{ $company->slug }}</flux:text>
<span class="section-eyebrow truncate">{{ $company->name }}</span>
</div>
<flux:badge color="{{ $company->is_active ? 'green' : 'red' }}" size="sm">
{{ $company->is_active ? __('Aktiv') : __('Inaktiv') }}
</flux:badge>
</div>
<div class="flex flex-wrap gap-2">
<flux:badge color="zinc" size="sm">{{ $company->portal?->label() ?? __('Portal unbekannt') }}</flux:badge>
<flux:badge color="indigo" size="sm">{{ $context->roleLabelFor($company, $user) }}</flux:badge>
@if($company->disable_footer_code)
<flux:badge color="amber" size="sm">{{ __('Footer-Code aus') }}</flux:badge>
@if ($company->is_active)
<span class="badge ok dot">{{ __('Aktiv') }}</span>
@else
<span class="badge err dot">{{ __('Inaktiv') }}</span>
@endif
</div>
<div class="grid grid-cols-2 gap-3">
<div class="rounded-lg bg-zinc-50 p-3 dark:bg-zinc-900">
<flux:text class="text-xs text-zinc-500">{{ __('Pressemitteilungen') }}</flux:text>
<flux:text size="lg" weight="bold">{{ $company->press_releases_count }}</flux:text>
<div class="p-5 space-y-4 flex-1">
<div class="text-[11.5px] text-[color:var(--color-ink-3)] truncate">
{{ $company->slug }}
</div>
<div class="rounded-lg bg-zinc-50 p-3 dark:bg-zinc-900">
<flux:text class="text-xs text-zinc-500">{{ __('Pressekontakte') }}</flux:text>
<flux:text size="lg" weight="bold">{{ $company->contacts_count }}</flux:text>
<div class="flex flex-wrap gap-2">
<span class="badge hub">{{ $company->portal?->label() ?? __('Portal unbekannt') }}</span>
<span class="badge hub">{{ $context->roleLabelFor($company, $user) }}</span>
@if ($company->disable_footer_code)
<span class="badge warn">{{ __('Footer-Code aus') }}</span>
@endif
</div>
<div class="grid grid-cols-2 gap-3 pt-1">
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[10.5px] uppercase tracking-[0.6px] font-semibold text-[color:var(--color-ink-3)]">
{{ __('Pressemitteilungen') }}
</div>
<div class="text-[18px] font-bold text-[color:var(--color-ink)] mt-1">
{{ $company->press_releases_count }}
</div>
</div>
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[10.5px] uppercase tracking-[0.6px] font-semibold text-[color:var(--color-ink-3)]">
{{ __('Pressekontakte') }}
</div>
<div class="text-[18px] font-bold text-[color:var(--color-ink)] mt-1">
{{ $company->contacts_count }}
</div>
</div>
</div>
</div>
<div class="flex justify-end">
<div class="px-5 pb-4 pt-3 border-t border-[color:var(--color-bg-rule)] flex justify-end">
<flux:button size="sm" variant="ghost" icon="arrow-right" href="{{ route('me.press-kits.show', $company->id) }}" wire:navigate>
{{ __('Firma öffnen') }}
</flux:button>
</div>
</flux:card>
</article>
@empty
<flux:card class="md:col-span-2 xl:col-span-3">
<div class="flex flex-col items-center justify-center py-10 text-center">
<flux:icon.building-office class="size-10 text-zinc-300" />
<flux:text weight="semibold" class="mt-3">{{ __('Keine Firmen gefunden') }}</flux:text>
<flux:text class="mt-1 max-w-md text-sm text-zinc-500">
<article class="panel md:col-span-2 xl:col-span-3">
<div class="p-10 flex flex-col items-center justify-center text-center">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-3
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.building-office class="size-6" />
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Keine Firmen gefunden') }}
</div>
<p class="text-[12px] text-[color:var(--color-ink-3)] max-w-md m-0">
{{ __('Prüfen Sie die Suche oder wenden Sie sich an den Support, wenn eine Firma fehlen sollte.') }}
</flux:text>
</p>
<flux:button class="mt-4" variant="primary" href="{{ route('me.profile') }}" wire:navigate>
{{ __('Profil prüfen') }}
</flux:button>
</div>
</flux:card>
</article>
@endforelse
</div>
</section>
{{ $pressKits->links() }}
</div>

View file

@ -288,116 +288,112 @@ new #[Layout('components.layouts.app'), Title('Firma')] class extends Component
}
}; ?>
<div class="space-y-6">
<flux:card>
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div class="flex items-start gap-4">
<div
class="flex size-16 shrink-0 items-center justify-center rounded-xl border border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
@if ($company->logoUrl())
<img src="{{ $company->logoUrl() }}" alt="{{ $company->name }}" width="64" height="64"
class="h-20 max-h-20 w-20 max-w-20 rounded-xl object-contain p-2" />
@else
<flux:icon.building-office class="size-8 text-zinc-400" />
@endif
</div>
<div>
<div class="flex flex-wrap items-center gap-2">
<flux:heading size="xl">{{ $company->name }}</flux:heading>
<flux:badge color="{{ $company->is_active ? 'green' : 'red' }}" size="sm">
{{ $company->is_active ? __('Aktiv') : __('Inaktiv') }}
</flux:badge>
</div>
<flux:text class="mt-1 text-sm text-zinc-500">{{ $company->slug }}</flux:text>
<div class="mt-3 flex flex-wrap gap-2">
<flux:badge color="zinc" size="sm">
{{ $company->portal?->label() ?? __('Portal unbekannt') }}</flux:badge>
<flux:badge color="indigo" size="sm">{{ $roleLabel }}</flux:badge>
<flux:badge color="zinc" size="sm">{{ __('Pressemappe') }}</flux:badge>
@if ($company->disable_footer_code)
<flux:badge color="amber" size="sm">{{ __('Footer-Code deaktiviert') }}</flux:badge>
@endif
</div>
</div>
</div>
<div class="flex flex-wrap gap-2">
<flux:button icon="plus" variant="primary" href="{{ route('me.press-releases.create') }}"
wire:navigate>
{{ __('Neue Pressemitteilung') }}
</flux:button>
@if ($canManageCompany)
<flux:button icon="pencil" variant="ghost" wire:click="startEditCompany">
{{ __('Stammdaten bearbeiten') }}
</flux:button>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('User Backend') }}</span>
<span class="eyebrow muted">{{ __('Mein Bereich · Firma') }}</span>
@if ($company->is_active)
<span class="badge ok">{{ __('Aktiv') }}</span>
@else
<span class="badge err">{{ __('Inaktiv') }}</span>
@endif
<flux:button icon="arrow-left" variant="ghost" href="{{ route('me.press-kits.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
<span class="badge hub">{{ $company->portal?->label() ?? __('Portal unbekannt') }}</span>
<span class="badge hub">{{ $roleLabel }}</span>
@if ($company->disable_footer_code)
<span class="badge warn">{{ __('Footer-Code deaktiviert') }}</span>
@endif
</div>
<div class="flex items-start gap-4">
@if ($company->logoUrl())
<img src="{{ $company->logoUrl() }}" alt="{{ $company->name }}" width="64" height="64"
class="h-16 max-h-16 w-16 max-w-16 rounded-[6px] border border-[color:var(--color-bg-rule)] object-contain bg-[color:var(--color-bg-elev)] flex-shrink-0" />
@else
<div class="flex h-16 w-16 items-center justify-center rounded-[6px]
border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] flex-shrink-0">
<flux:icon.building-office class="size-7 text-[color:var(--color-ink-3)]" />
</div>
@endif
<div class="min-w-0">
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)] break-words">
{{ $company->name }}
</h1>
<div class="text-[12px] text-[color:var(--color-ink-3)] mt-1">{{ $company->slug }}</div>
</div>
</div>
</div>
</flux:card>
<flux:card>
<div class="flex flex-wrap gap-2">
<flux:button size="sm" variant="ghost" href="#stammdaten">{{ __('Stammdaten') }}</flux:button>
<flux:button size="sm" variant="ghost" href="#pressekontakte">{{ __('Pressekontakte') }}</flux:button>
<flux:button size="sm" variant="ghost" href="#pressemitteilungen">{{ __('Pressemitteilungen') }}</flux:button>
<flux:button size="sm" variant="ghost" href="#abrechnung">{{ __('Abrechnung') }}</flux:button>
<flux:button size="sm" variant="ghost" href="#statistik">{{ __('Statistik') }}</flux:button>
<div class="flex flex-wrap items-center gap-2 flex-shrink-0">
<flux:button icon="arrow-left" variant="ghost" href="{{ route('me.press-kits.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
@if ($canManageCompany)
<flux:button icon="pencil" variant="ghost" wire:click="startEditCompany">
{{ __('Stammdaten bearbeiten') }}
</flux:button>
@endif
<flux:button icon="plus" variant="primary" href="{{ route('me.press-releases.create') }}" wire:navigate>
{{ __('Neue Pressemitteilung') }}
</flux:button>
</div>
</flux:card>
</header>
<div class="grid grid-cols-2 gap-4 lg:grid-cols-4">
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Pressemitteilungen') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $company->press_releases_count }}</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Pressekontakte') }}</flux:text>
<flux:text size="xl" weight="bold">{{ $company->contacts_count }}</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Portal') }}</flux:text>
<flux:text weight="bold">{{ $company->portal?->label() ?? '' }}</flux:text>
</flux:card>
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Deine Rolle') }}</flux:text>
<flux:text weight="bold">{{ $roleLabel }}</flux:text>
</flux:card>
</div>
{{-- ============== QUICK-NAV ============== --}}
<nav class="flex flex-wrap items-center gap-2 border-b border-[color:var(--color-bg-rule)] pb-3">
<a href="#stammdaten" class="text-[12px] font-semibold text-[color:var(--color-ink-3)] hover:text-[color:var(--color-ink)] px-3 py-1.5 rounded-[4px] hover:bg-[color:var(--color-bg-elev)] transition-colors">{{ __('Stammdaten') }}</a>
<a href="#pressekontakte" class="text-[12px] font-semibold text-[color:var(--color-ink-3)] hover:text-[color:var(--color-ink)] px-3 py-1.5 rounded-[4px] hover:bg-[color:var(--color-bg-elev)] transition-colors">{{ __('Pressekontakte') }}</a>
<a href="#pressemitteilungen" class="text-[12px] font-semibold text-[color:var(--color-ink-3)] hover:text-[color:var(--color-ink)] px-3 py-1.5 rounded-[4px] hover:bg-[color:var(--color-bg-elev)] transition-colors">{{ __('Pressemitteilungen') }}</a>
<a href="#abrechnung" class="text-[12px] font-semibold text-[color:var(--color-ink-3)] hover:text-[color:var(--color-ink)] px-3 py-1.5 rounded-[4px] hover:bg-[color:var(--color-bg-elev)] transition-colors">{{ __('Abrechnung') }}</a>
<a href="#statistik" class="text-[12px] font-semibold text-[color:var(--color-ink-3)] hover:text-[color:var(--color-ink)] px-3 py-1.5 rounded-[4px] hover:bg-[color:var(--color-bg-elev)] transition-colors">{{ __('Statistik') }}</a>
</nav>
{{-- ============== KPI-Reihe ============== --}}
<section class="grid grid-cols-2 gap-4 lg:grid-cols-4">
<x-portal.stat-card variant="primary" :label="__('Pressemitteilungen')" :value="number_format($company->press_releases_count)">
<x-slot:meta>{{ __('zugeordnet') }}</x-slot:meta>
<x-slot:trend>{{ __('Content-Output') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="ok" :label="__('Pressekontakte')" :value="number_format($company->contacts_count)">
<x-slot:meta>{{ __('Ansprechpartner') }}</x-slot:meta>
<x-slot:trend>{{ __('für PMs verfügbar') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Portal')" :value="$company->portal?->label() ?? ''">
<x-slot:meta>{{ __('Sichtbarkeit') }}</x-slot:meta>
<x-slot:trend>{{ __('für diese Firma') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Deine Rolle')" :value="$roleLabel">
<x-slot:meta>{{ __('Berechtigungen') }}</x-slot:meta>
<x-slot:trend>{{ $canManageCompany ? __('Voller Schreibzugriff') : __('Lesezugriff') }}</x-slot:trend>
</x-portal.stat-card>
</section>
<div class="grid gap-6 xl:grid-cols-2">
<flux:card id="stammdaten">
<div class="mb-4 flex items-center justify-between gap-3">
<div>
<flux:heading size="lg">{{ __('Stammdaten') }}</flux:heading>
<flux:text class="text-sm text-zinc-500">{{ __('Firmendaten dieser Firma.') }}</flux:text>
</div>
<div class="flex items-center gap-2">
<flux:badge color="{{ $company->is_active ? 'green' : 'red' }}" size="sm">
{{ $company->is_active ? __('Aktiv') : __('Inaktiv') }}
</flux:badge>
@if ($canManageCompany)
<flux:button size="sm" variant="ghost" icon="pencil" wire:click="startEditCompany">
{{ __('Bearbeiten') }}
</flux:button>
@endif
</div>
{{-- ============== STAMMDATEN ============== --}}
<article class="panel" id="stammdaten">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Stammdaten') }}</span>
@if ($canManageCompany)
<flux:button size="sm" variant="ghost" icon="pencil" wire:click="startEditCompany">
{{ __('Bearbeiten') }}
</flux:button>
@endif
</div>
@if (session('company-status'))
<flux:callout color="green" icon="check-circle" class="mb-4">
<div class="mx-5 mt-4 px-4 py-3 rounded-[5px] border text-[12.5px] flex items-center gap-2
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
<flux:icon.check-circle class="size-[16px] flex-shrink-0" />
{{ session('company-status') }}
</flux:callout>
</div>
@endif
@if ($showCompanyForm)
<div class="mb-4 rounded-lg border border-zinc-200 p-4 dark:border-zinc-700">
<flux:heading size="sm" class="mb-4">
<div class="mx-5 my-4 rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-5">
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-4">
{{ __('Stammdaten bearbeiten') }}
</flux:heading>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<flux:field>
@ -429,23 +425,20 @@ new #[Layout('components.layouts.app'), Title('Firma')] class extends Component
<flux:error name="companyCountryCode" />
</flux:field>
<flux:field>
<flux:checkbox wire:model="companyDisableFooterCode"
:label="__('Footer-Code deaktivieren')" />
<flux:checkbox wire:model="companyDisableFooterCode" :label="__('Footer-Code deaktivieren')" />
</flux:field>
</div>
<flux:separator class="my-4" />
<div class="space-y-3">
<flux:heading size="xs">{{ __('Firmenlogo') }}</flux:heading>
<div class="mt-5 pt-4 border-t border-[color:var(--color-bg-rule)] space-y-3">
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">
{{ __('Firmenlogo') }}
</div>
@php($logoUrl = $company->logoUrl())
@if ($logoUrl && !$removeCompanyLogo)
@if ($logoUrl && ! $removeCompanyLogo)
<div class="flex items-center gap-3">
<img src="{{ $logoUrl }}" alt="{{ $company->name }}" width="64"
height="64"
class="h-16 max-h-16 w-16 max-w-16 rounded-md border border-zinc-200 object-contain dark:border-zinc-700" />
<flux:button type="button" size="sm" variant="ghost"
wire:click="$set('removeCompanyLogo', true)">
<img src="{{ $logoUrl }}" alt="{{ $company->name }}" width="64" height="64"
class="h-16 max-h-16 w-16 max-w-16 rounded-[6px] border border-[color:var(--color-bg-rule)] object-contain bg-white" />
<flux:button type="button" size="sm" variant="ghost" wire:click="$set('removeCompanyLogo', true)">
{{ __('Logo entfernen') }}
</flux:button>
</div>
@ -458,7 +451,7 @@ new #[Layout('components.layouts.app'), Title('Firma')] class extends Component
</flux:field>
</div>
<div class="mt-4 flex justify-end gap-2">
<div class="mt-4 pt-4 border-t border-[color:var(--color-bg-rule)] flex justify-end gap-2">
<flux:button type="button" variant="ghost" wire:click="cancelCompanyForm">
{{ __('Abbrechen') }}
</flux:button>
@ -469,62 +462,67 @@ new #[Layout('components.layouts.app'), Title('Firma')] class extends Component
</div>
@endif
<div class="grid gap-3 sm:grid-cols-2">
<dl class="p-5 grid gap-3 sm:grid-cols-2 text-[12.5px]">
<div>
<flux:text class="text-xs text-zinc-500">{{ __('E-Mail') }}</flux:text>
<flux:text>{{ $company->email ?: '' }}</flux:text>
<dt class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-1">{{ __('E-Mail') }}</dt>
<dd class="text-[color:var(--color-ink)] break-all">{{ $company->email ?: '' }}</dd>
</div>
<div>
<flux:text class="text-xs text-zinc-500">{{ __('Telefon') }}</flux:text>
<flux:text>{{ $company->phone ?: '' }}</flux:text>
<dt class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-1">{{ __('Telefon') }}</dt>
<dd class="text-[color:var(--color-ink)]">{{ $company->phone ?: '' }}</dd>
</div>
<div>
<flux:text class="text-xs text-zinc-500">{{ __('Website') }}</flux:text>
@if ($company->website)
<a href="{{ $company->website }}" target="_blank"
class="text-sm text-blue-600 hover:underline dark:text-blue-400">{{ $company->website }}</a>
@else
<flux:text></flux:text>
@endif
<dt class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-1">{{ __('Website') }}</dt>
<dd class="break-all">
@if ($company->website)
<a href="{{ $company->website }}" target="_blank" rel="noopener"
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">{{ $company->website }}</a>
@else
<span class="text-[color:var(--color-ink)]"></span>
@endif
</dd>
</div>
<div>
<flux:text class="text-xs text-zinc-500">{{ __('Land') }}</flux:text>
<flux:text>{{ $company->country_code ?: '' }}</flux:text>
<dt class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-1">{{ __('Land') }}</dt>
<dd class="text-[color:var(--color-ink)]">{{ $company->country_code ?: '' }}</dd>
</div>
<div class="sm:col-span-2">
<flux:text class="text-xs text-zinc-500">{{ __('Adresse') }}</flux:text>
<flux:text>{{ $company->address ?: '' }}</flux:text>
<dt class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-1">{{ __('Adresse') }}</dt>
<dd class="text-[color:var(--color-ink)] whitespace-pre-line">{{ $company->address ?: '' }}</dd>
</div>
</dl>
</article>
{{-- ============== PRESSEKONTAKTE ============== --}}
<article class="panel" id="pressekontakte">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Pressekontakte') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ trans_choice(':count Kontakt|:count Kontakte', $company->contacts_count, ['count' => $company->contacts_count]) }}
</span>
</div>
</flux:card>
<flux:card id="pressekontakte">
<div class="mb-4 flex items-center justify-between">
<div>
<flux:heading size="lg">{{ __('Pressekontakte') }}</flux:heading>
<flux:text class="text-sm text-zinc-500">
{{ trans_choice(':count Kontakt|:count Kontakte', $company->contacts_count, ['count' => $company->contacts_count]) }}
</flux:text>
</div>
@if ($canManageContacts)
@if ($canManageContacts)
<div class="px-5 pt-4 flex justify-end">
<flux:button size="sm" variant="primary" icon="plus" wire:click="startCreateContact">
{{ __('Kontakt hinzufügen') }}
</flux:button>
@endif
</div>
</div>
@endif
@if (session('contact-status'))
<flux:callout color="green" icon="check-circle" class="mb-4">
<div class="mx-5 mt-4 px-4 py-3 rounded-[5px] border text-[12.5px] flex items-center gap-2
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
<flux:icon.check-circle class="size-[16px] flex-shrink-0" />
{{ session('contact-status') }}
</flux:callout>
</div>
@endif
@if ($showContactForm)
<div class="mb-4 rounded-lg border border-zinc-200 p-4 dark:border-zinc-700">
<flux:heading size="sm" class="mb-4">
<div class="mx-5 my-4 rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-5">
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-4">
{{ $editingContactId ? __('Pressekontakt bearbeiten') : __('Neuen Pressekontakt anlegen') }}
</flux:heading>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<flux:field>
@ -549,7 +547,7 @@ new #[Layout('components.layouts.app'), Title('Firma')] class extends Component
</flux:field>
</div>
<div class="mt-4 flex justify-end gap-2">
<div class="mt-4 pt-4 border-t border-[color:var(--color-bg-rule)] flex justify-end gap-2">
<flux:button type="button" variant="ghost" wire:click="cancelContactForm">
{{ __('Abbrechen') }}
</flux:button>
@ -560,20 +558,21 @@ new #[Layout('components.layouts.app'), Title('Firma')] class extends Component
</div>
@endif
<div class="space-y-3">
@forelse($contacts as $contact)
<div class="rounded-lg bg-zinc-50 p-3 dark:bg-zinc-900">
<div class="p-5 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="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<flux:text weight="semibold">
{{ trim(($contact->first_name ?? '') . ' ' . ($contact->last_name ?? '')) ?: __('Kontakt ohne Name') }}
</flux:text>
<flux:text class="text-sm text-zinc-500">
{{ $contact->responsibility ?: __('Keine Rolle hinterlegt') }}</flux:text>
<div class="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs text-zinc-500">
<div class="min-w-0">
<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)] mt-0.5">
{{ $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-blue-600 hover:underline dark:text-blue-400">{{ $contact->email }}</a>
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>
@ -585,9 +584,8 @@ new #[Layout('components.layouts.app'), Title('Firma')] class extends Component
</div>
@if ($canManageContacts)
<div class="flex gap-1">
<flux:button size="sm" variant="ghost" icon="pencil"
wire:click="editContact({{ $contact->id }})">
<div class="flex gap-1 flex-shrink-0">
<flux:button size="sm" variant="ghost" icon="pencil" wire:click="editContact({{ $contact->id }})">
{{ __('Bearbeiten') }}
</flux:button>
<flux:button size="sm" variant="ghost" icon="trash"
@ -600,135 +598,145 @@ new #[Layout('components.layouts.app'), Title('Firma')] class extends Component
</div>
</div>
@empty
<div class="rounded-lg border border-dashed border-zinc-200 p-4 text-sm text-zinc-500 dark:border-zinc-700">
<flux:text weight="semibold">{{ __('Keine Pressekontakte hinterlegt') }}</flux:text>
<flux:text class="mt-1 text-sm text-zinc-500">
<div class="rounded-[5px] border border-dashed border-[color:var(--color-bg-rule)] p-4">
<div class="text-[13px] font-semibold text-[color:var(--color-ink)]">{{ __('Keine Pressekontakte hinterlegt') }}</div>
<p class="mt-1 text-[12px] text-[color:var(--color-ink-3)] m-0">
{{ __('Pressekontakte helfen, Pressemitteilungen eindeutig einer Ansprechperson zuzuordnen.') }}
</flux:text>
</p>
@if ($canManageContacts)
<flux:button class="mt-4" size="sm" variant="primary" icon="plus" wire:click="startCreateContact">
<flux:button class="mt-3" size="sm" variant="primary" icon="plus" wire:click="startCreateContact">
{{ __('Kontakt hinzufügen') }}
</flux:button>
@endif
</div>
@endforelse
</div>
</flux:card>
</article>
</div>
<flux:card id="pressemitteilungen" class="p-0">
<div class="flex items-center justify-between border-b border-zinc-200 px-4 py-3 dark:border-zinc-700">
<flux:heading size="lg">{{ __('Pressemitteilungen dieser Firma') }}</flux:heading>
{{-- ============== PRESSEMITTEILUNGEN ============== --}}
<article class="panel overflow-hidden" id="pressemitteilungen">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Pressemitteilungen dieser Firma') }}</span>
<flux:button size="sm" variant="ghost" href="{{ route('me.press-releases.index') }}" wire:navigate>
{{ __('Alle anzeigen') }}
</flux:button>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Titel') }}</flux:table.column>
<flux:table.column>{{ __('Status') }}</flux:table.column>
<flux:table.column>{{ __('Datum') }}</flux:table.column>
<flux:table.column>{{ __('Aktionen') }}</flux:table.column>
</flux:table.columns>
<div class="px-4 pb-4 pt-2">
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Titel') }}</flux:table.column>
<flux:table.column>{{ __('Status') }}</flux:table.column>
<flux:table.column>{{ __('Datum') }}</flux:table.column>
<flux:table.column>{{ __('Aktionen') }}</flux:table.column>
</flux:table.columns>
@forelse($pressReleases as $pressRelease)
<flux:table.row wire:key="company-pr-{{ $pressRelease->id }}">
<flux:table.cell>
<flux:text weight="semibold">{{ $pressRelease->title }}</flux:text>
</flux:table.cell>
<flux:table.cell>
<flux:badge
color="{{ match ($pressRelease->status->value) {
'published' => 'green',
'review' => 'yellow',
'rejected' => 'red',
'archived' => 'blue',
default => 'zinc',
} }}"
size="sm">
{{ $pressRelease->status->label() }}
</flux:badge>
</flux:table.cell>
<flux:table.cell>
<flux:text class="text-sm text-zinc-500">
{{ $pressRelease->published_at?->format('d.m.Y') ?? ($pressRelease->created_at?->format('d.m.Y') ?? '') }}
</flux:text>
</flux:table.cell>
<flux:table.cell>
<flux:button size="sm" variant="ghost" icon="eye"
href="{{ route('me.press-releases.show', $pressRelease->id) }}" wire:navigate>
{{ __('Öffnen') }}
</flux:button>
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="4">
<div class="flex flex-col items-center justify-center px-4 py-10 text-center">
<flux:icon.newspaper class="size-10 text-zinc-300" />
<flux:text weight="semibold" class="mt-3">
{{ __('Keine Pressemitteilungen für diese Firma') }}
</flux:text>
<flux:text class="mt-1 max-w-md text-sm text-zinc-500">
{{ __('Erstellen Sie die erste Pressemitteilung direkt mit dieser Firma als Kontext.') }}
</flux:text>
<flux:button class="mt-4" size="sm" variant="primary" icon="plus" href="{{ route('me.press-releases.create') }}" wire:navigate>
{{ __('Neue Pressemitteilung') }}
</flux:button>
@forelse ($pressReleases as $pressRelease)
<flux:table.row wire:key="company-pr-{{ $pressRelease->id }}">
<flux:table.cell>
<span class="text-[13px] font-semibold text-[color:var(--color-ink)]">{{ $pressRelease->title }}</span>
</flux:table.cell>
<flux:table.cell>
@php($statusValue = $pressRelease->status->value)
<span @class([
'badge dot',
'ok' => $statusValue === 'published',
'warn' => $statusValue === 'review',
'err' => $statusValue === 'rejected',
'hub' => ! in_array($statusValue, ['published', 'review', 'rejected'], true),
])>
{{ $pressRelease->status->label() }}
</span>
</flux:table.cell>
<flux:table.cell>
<span class="text-[12px] text-[color:var(--color-ink-3)]">
{{ $pressRelease->published_at?->format('d.m.Y') ?? ($pressRelease->created_at?->format('d.m.Y') ?? '') }}
</span>
</flux:table.cell>
<flux:table.cell>
<flux:button size="sm" variant="ghost" icon="eye"
href="{{ route('me.press-releases.show', $pressRelease->id) }}" wire:navigate>
{{ __('Öffnen') }}
</flux:button>
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="4">
<div class="flex flex-col items-center justify-center px-4 py-10 text-center">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-3
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.newspaper class="size-6" />
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table>
</div>
</flux:card>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Keine Pressemitteilungen für diese Firma') }}
</div>
<p class="text-[12px] text-[color:var(--color-ink-3)] max-w-md m-0">
{{ __('Erstellen Sie die erste Pressemitteilung direkt mit dieser Firma als Kontext.') }}
</p>
<flux:button class="mt-4" size="sm" variant="primary" icon="plus" href="{{ route('me.press-releases.create') }}" wire:navigate>
{{ __('Neue Pressemitteilung') }}
</flux:button>
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table>
</article>
<div class="grid gap-6 xl:grid-cols-2">
<flux:card id="abrechnung">
<div class="flex items-start justify-between gap-3">
<div>
<flux:heading size="lg">{{ __('Abrechnung') }}</flux:heading>
<flux:text class="mt-1 text-sm text-zinc-500">
{{ __('Firmenspezifische Zahlungsarten und Add-ons werden hier später zusammengeführt.') }}
</flux:text>
</div>
<flux:badge color="zinc" size="sm">{{ __('In Vorbereitung') }}</flux:badge>
{{-- ============== ABRECHNUNG ============== --}}
<article class="panel" id="abrechnung">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Abrechnung') }}</span>
<span class="badge warn">{{ __('In Vorbereitung') }}</span>
</div>
<div class="p-5 space-y-4">
<p class="text-[12.5px] text-[color:var(--color-ink-2)] m-0">
{{ __('Firmenspezifische Zahlungsarten und Add-ons werden hier später zusammengeführt.') }}
</p>
<div class="rounded-[5px] border border-dashed border-[color:var(--color-bg-rule)] p-4">
<div class="text-[13px] font-semibold text-[color:var(--color-ink)]">
{{ __('Noch keine firmenspezifische Abrechnung') }}
</div>
<p class="mt-1 text-[12px] text-[color:var(--color-ink-3)] m-0">
{{ __('Rechnungen finden Sie aktuell gesammelt im Finanzbereich. Firmenscharfe Zahlungsarten folgen mit dem Preismodell.') }}
</p>
<flux:button class="mt-3" size="sm" variant="ghost" href="{{ route('me.invoices.index') }}" wire:navigate>
{{ __('Rechnungen öffnen') }}
</flux:button>
</div>
</div>
</article>
<div class="mt-4 rounded-lg border border-dashed border-zinc-200 p-4 dark:border-zinc-700">
<flux:text weight="semibold">{{ __('Noch keine firmenspezifische Abrechnung') }}</flux:text>
<flux:text class="mt-1 text-sm text-zinc-500">
{{ __('Rechnungen finden Sie aktuell gesammelt im Finanzbereich. Firmenscharfe Zahlungsarten folgen mit dem Preismodell.') }}
</flux:text>
<flux:button class="mt-4" size="sm" variant="ghost" href="{{ route('me.invoices.index') }}" wire:navigate>
{{ __('Rechnungen öffnen') }}
</flux:button>
{{-- ============== STATISTIK ============== --}}
<article class="panel" id="statistik">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Statistik') }}</span>
<span class="badge warn">{{ __('Später') }}</span>
</div>
</flux:card>
<flux:card id="statistik">
<div class="flex items-start justify-between gap-3">
<div>
<flux:heading size="lg">{{ __('Statistik') }}</flux:heading>
<flux:text class="mt-1 text-sm text-zinc-500">
{{ __('Erste Kennzahlen zur Firma; detaillierte Auswertungen folgen später.') }}
</flux:text>
</div>
<flux:badge color="zinc" size="sm">{{ __('Später') }}</flux:badge>
</div>
<div class="mt-4 grid grid-cols-2 gap-3">
<div class="rounded-lg bg-zinc-50 p-3 dark:bg-zinc-900">
<flux:text class="text-xs text-zinc-500">{{ __('Pressemitteilungen') }}</flux:text>
<flux:text size="lg" weight="bold">{{ $company->press_releases_count }}</flux:text>
</div>
<div class="rounded-lg bg-zinc-50 p-3 dark:bg-zinc-900">
<flux:text class="text-xs text-zinc-500">{{ __('Pressekontakte') }}</flux:text>
<flux:text size="lg" weight="bold">{{ $company->contacts_count }}</flux:text>
<div class="p-5 space-y-4">
<p class="text-[12.5px] text-[color:var(--color-ink-2)] m-0">
{{ __('Erste Kennzahlen zur Firma; detaillierte Auswertungen folgen später.') }}
</p>
<div class="grid grid-cols-2 gap-3">
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[10.5px] uppercase tracking-[0.6px] font-semibold text-[color:var(--color-ink-3)]">
{{ __('Pressemitteilungen') }}
</div>
<div class="text-[18px] font-bold text-[color:var(--color-ink)] mt-1">
{{ $company->press_releases_count }}
</div>
</div>
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[10.5px] uppercase tracking-[0.6px] font-semibold text-[color:var(--color-ink-3)]">
{{ __('Pressekontakte') }}
</div>
<div class="text-[18px] font-bold text-[color:var(--color-ink)] mt-1">
{{ $company->contacts_count }}
</div>
</div>
</div>
</div>
</flux:card>
</article>
</div>
</div>

View file

@ -1,18 +1,19 @@
<?php
use App\Enums\Portal;
use App\Enums\PressReleaseStatus;
use App\Models\PressRelease;
use App\Services\Customer\CustomerCompanyContext;
use App\Services\PressRelease\BlacklistViolationException;
use App\Services\PressRelease\PressReleaseService;
use App\Services\Customer\CustomerCompanyContext;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Volt\Component;
use Livewire\WithPagination;
new #[Layout('components.layouts.app'), Title('Meine Pressemitteilungen')] class extends Component
{
new #[Layout('components.layouts.app'), Title('Meine Pressemitteilungen')] class extends Component {
use WithPagination;
public string $search = '';
@ -22,6 +23,8 @@ new #[Layout('components.layouts.app'), Title('Meine Pressemitteilungen')] class
#[Url(as: 'company', except: 'all')]
public string $companyFilter = 'all';
public string $portalFilter = 'all';
public string $sortBy = 'created_at';
public string $sortDir = 'desc';
@ -37,16 +40,49 @@ new #[Layout('components.layouts.app'), Title('Meine Pressemitteilungen')] class
$this->resetPage();
}
public function updatedSearch(): void { $this->resetPage(); }
public function setView(string $view): void
{
$this->statusFilter = $view;
$this->resetPage();
}
public function updatedStatusFilter(): void { $this->resetPage(); }
public function resetFilters(): void
{
$this->search = '';
$this->statusFilter = 'all';
$this->portalFilter = 'all';
if ($this->companyFilter !== 'all') {
$this->companyFilter = 'all';
}
$this->resetPage();
}
public function updatedCompanyFilter(): void { $this->resetPage(); }
public function updatedSearch(): void
{
$this->resetPage();
}
public function updatedStatusFilter(): void
{
$this->resetPage();
}
public function updatedCompanyFilter(): void
{
$this->resetPage();
}
public function updatedPortalFilter(): void
{
$this->resetPage();
}
public function submitForReview(int $id): void
{
$pr = $this->findMyPR($id);
if (! $pr) { return; }
if (!$pr) {
return;
}
try {
app(PressReleaseService::class)->submitForReview($pr);
@ -64,25 +100,41 @@ new #[Layout('components.layouts.app'), Title('Meine Pressemitteilungen')] class
$context = app(CustomerCompanyContext::class);
$selectedCompanyId = $context->selectedCompanyId(auth()->user());
$prs = PressRelease::withoutGlobalScopes()
$base = PressRelease::withoutGlobalScopes()
->where('user_id', $userId)
->when($selectedCompanyId !== null, fn($q) => $q->where('company_id', $selectedCompanyId))
->when($selectedCompanyId === null && $this->companyFilter === 'assigned', fn($q) => $q->whereNotNull('company_id'))
->when($selectedCompanyId === null && $this->companyFilter === 'unassigned', fn($q) => $q->whereNull('company_id'));
$statusCountsRaw = (clone $base)->toBase()->selectRaw('status, COUNT(*) as aggregate')->groupBy('status')->pluck('aggregate', 'status');
$statusCounts = [
'all' => (int) $statusCountsRaw->sum(),
PressReleaseStatus::Published->value => (int) ($statusCountsRaw[PressReleaseStatus::Published->value] ?? 0),
PressReleaseStatus::Draft->value => (int) ($statusCountsRaw[PressReleaseStatus::Draft->value] ?? 0),
PressReleaseStatus::Review->value => (int) ($statusCountsRaw[PressReleaseStatus::Review->value] ?? 0),
PressReleaseStatus::Rejected->value => (int) ($statusCountsRaw[PressReleaseStatus::Rejected->value] ?? 0),
PressReleaseStatus::Archived->value => (int) ($statusCountsRaw[PressReleaseStatus::Archived->value] ?? 0),
];
$prs = (clone $base)
->with('company:id,name')
->when($selectedCompanyId !== null, fn ($q) => $q->where('company_id', $selectedCompanyId))
->when($selectedCompanyId === null && $this->companyFilter === 'assigned', fn ($q) => $q->whereNotNull('company_id'))
->when($selectedCompanyId === null && $this->companyFilter === 'unassigned', fn ($q) => $q->whereNull('company_id'))
->when(filled($this->search), function ($q): void {
$term = $this->search;
$q->where('title', 'like', '%'.$term.'%');
$q->where('title', 'like', '%' . $term . '%');
})
->when($this->statusFilter !== 'all', fn ($q) => $q->where('status', $this->statusFilter))
->when($this->statusFilter !== 'all', fn($q) => $q->where('status', $this->statusFilter))
->when($this->portalFilter !== 'all', fn($q) => $q->where('portal', $this->portalFilter))
->orderBy(in_array($this->sortBy, ['title', 'status', 'created_at']) ? $this->sortBy : 'created_at', $this->sortDir)
->paginate(100);
->paginate(25);
return [
'pressReleases' => $prs,
'statusOptions' => PressReleaseStatus::cases(),
'portalOptions' => Portal::cases(),
'selectedCompany' => $context->selectedCompany(auth()->user()),
'hasGlobalCompanyContext' => $selectedCompanyId === null,
'statusCounts' => $statusCounts,
];
}
@ -95,16 +147,27 @@ new #[Layout('components.layouts.app'), Title('Meine Pressemitteilungen')] class
}
}; ?>
@php
$hasAnyFilter =
$search !== '' ||
$statusFilter !== 'all' ||
$portalFilter !== 'all' ||
($hasGlobalCompanyContext && $companyFilter !== 'all');
$hasAnyPR = $statusCounts['all'] > 0;
@endphp
<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)]">
{{-- ============== FLASH ============== --}}
@if (session('success'))
<div class="px-4 py-3 rounded-[5px] border-l-[3px] text-[12.5px]"
style="border-color: var(--color-ok); background: color-mix(in oklab, var(--color-ok) 10%, var(--color-bg)); color: var(--color-ink);">
{{ 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)]">
@if (session('error'))
<div class="px-4 py-3 rounded-[5px] border-l-[3px] text-[12.5px]"
style="border-color: var(--color-err); background: color-mix(in oklab, var(--color-err) 12%, var(--color-bg)); color: var(--color-ink);">
{{ session('error') }}
</div>
@endif
@ -119,118 +182,491 @@ new #[Layout('components.layouts.app'), Title('Meine Pressemitteilungen')] class
<h1 class="text-[34px] font-bold tracking-[-0.7px] leading-[1.1] m-0 text-[color:var(--color-ink)]">
{{ __('Meine Pressemitteilungen') }}
</h1>
<p class="text-[13.5px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
@if ($selectedCompany)
{{-- Counter-Strip + Kontext-Hinweis --}}
@if ($hasAnyPR)
<div class="counter-strip mt-3">
<span class="seg"><b>{{ $statusCounts['all'] }}</b> {{ __('Mitteilungen') }}</span>
@if ($statusCounts[\App\Enums\PressReleaseStatus::Published->value] > 0)
<span class="sep"></span>
<span
class="seg is-ok"><b>{{ $statusCounts[\App\Enums\PressReleaseStatus::Published->value] }}</b>
{{ __('veröffentlicht') }}</span>
@endif
@if ($statusCounts[\App\Enums\PressReleaseStatus::Review->value] > 0)
<span class="sep"></span>
<span
class="seg is-warn"><b>{{ $statusCounts[\App\Enums\PressReleaseStatus::Review->value] }}</b>
{{ __('in Prüfung') }}</span>
@endif
@if ($statusCounts[\App\Enums\PressReleaseStatus::Draft->value] > 0)
<span class="sep"></span>
<span
class="seg is-muted"><b>{{ $statusCounts[\App\Enums\PressReleaseStatus::Draft->value] }}</b>
{{ __('Entwürfe') }}</span>
@endif
@if ($statusCounts[\App\Enums\PressReleaseStatus::Rejected->value] > 0)
<span class="sep"></span>
<span
class="seg is-err"><b>{{ $statusCounts[\App\Enums\PressReleaseStatus::Rejected->value] }}</b>
{{ __('abgelehnt') }}</span>
@endif
</div>
@endif
@if ($selectedCompany)
<p class="text-[12.5px] leading-[1.55] mt-2 m-0 text-[color:var(--color-ink-3)]">
{{ __('Gefiltert auf :company', ['company' => $selectedCompany->name]) }}
@else
</p>
@elseif (!$hasAnyPR)
<p class="text-[13.5px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Übersicht aller PMs Ihres Kundenkontos, mit Filter und Schnellaktionen.') }}
@endif
</p>
</p>
@endif
</div>
<div class="flex items-center gap-3 flex-shrink-0">
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="primary" icon="plus" href="{{ route('me.press-releases.create') }}" wire:navigate>
{{ __('Neue Pressemitteilung') }}
</flux:button>
</div>
</header>
{{-- ============== FILTER-PANEL ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Filter & Suche') }}</span>
</div>
<div class="p-5 flex flex-col gap-3 sm:flex-row">
<flux:input wire:model.live.debounce.300ms="search" placeholder="{{ __('Titel suchen…') }}" icon="magnifying-glass" class="flex-1" />
<flux:select wire:model.live="statusFilter" class="sm:w-44">
<option value="all">{{ __('Alle Status') }}</option>
@foreach($statusOptions as $s)
<option value="{{ $s->value }}">{{ $s->label() }}</option>
@endforeach
</flux:select>
@if($hasGlobalCompanyContext)
<flux:select wire:model.live="companyFilter" class="sm:w-48">
<option value="all">{{ __('Alle Firmenzuordnungen') }}</option>
<option value="assigned">{{ __('Mit Firma') }}</option>
<option value="unassigned">{{ __('Ohne Firma') }}</option>
{{-- ============== SAVED-VIEWS-TABS ============== --}}
@if ($hasAnyPR)
<nav class="view-tabs" aria-label="{{ __('Gespeicherte Ansichten') }}">
<button type="button" wire:click="setView('all')" @class(['view-tab', 'is-active' => $statusFilter === 'all'])>
{{ __('Alle') }} <span class="cnt">{{ $statusCounts['all'] }}</span>
</button>
<button type="button" wire:click="setView('{{ \App\Enums\PressReleaseStatus::Published->value }}')"
@class([
'view-tab',
'is-active' =>
$statusFilter === \App\Enums\PressReleaseStatus::Published->value,
])>
{{ __('Veröffentlicht') }} <span
class="cnt">{{ $statusCounts[\App\Enums\PressReleaseStatus::Published->value] }}</span>
</button>
<button type="button" wire:click="setView('{{ \App\Enums\PressReleaseStatus::Draft->value }}')"
@class([
'view-tab',
'is-active' =>
$statusFilter === \App\Enums\PressReleaseStatus::Draft->value,
])>
{{ __('Entwürfe') }} <span
class="cnt">{{ $statusCounts[\App\Enums\PressReleaseStatus::Draft->value] }}</span>
</button>
<button type="button" wire:click="setView('{{ \App\Enums\PressReleaseStatus::Review->value }}')"
@class([
'view-tab',
'is-active' =>
$statusFilter === \App\Enums\PressReleaseStatus::Review->value,
])>
{{ __('In Prüfung') }} <span
class="cnt">{{ $statusCounts[\App\Enums\PressReleaseStatus::Review->value] }}</span>
</button>
<button type="button" wire:click="setView('{{ \App\Enums\PressReleaseStatus::Rejected->value }}')"
@class([
'view-tab',
'is-active' =>
$statusFilter === \App\Enums\PressReleaseStatus::Rejected->value,
])>
{{ __('Abgelehnt') }} <span
class="cnt">{{ $statusCounts[\App\Enums\PressReleaseStatus::Rejected->value] }}</span>
</button>
<button type="button" wire:click="setView('{{ \App\Enums\PressReleaseStatus::Archived->value }}')"
@class([
'view-tab',
'is-active' =>
$statusFilter === \App\Enums\PressReleaseStatus::Archived->value,
])>
{{ __('Archiv') }} <span
class="cnt">{{ $statusCounts[\App\Enums\PressReleaseStatus::Archived->value] }}</span>
</button>
</nav>
@endif
{{-- ============== FILTER + SUCHE ============== --}}
@if ($hasAnyPR)
<section class="space-y-3">
<div class="flex items-center gap-2 flex-wrap">
<flux:input wire:model.live.debounce.300ms="search" placeholder="{{ __('Titel suchen…') }}"
icon="magnifying-glass" class="max-w-[340px]" />
<span class="w-px h-6 bg-[color:var(--color-bg-rule)] mx-1"></span>
<flux:select wire:model.live="portalFilter" class="sm:w-44">
<option value="all">{{ __('Portal: Alle') }}</option>
@foreach ($portalOptions as $p)
<option value="{{ $p->value }}">{{ __('Portal:') }} {{ $p->label() }}</option>
@endforeach
</flux:select>
@if ($hasGlobalCompanyContext)
<flux:select wire:model.live="companyFilter" class="sm:w-56">
<option value="all">{{ __('Firma: Alle') }}</option>
<option value="assigned">{{ __('Firma: Mit Firma') }}</option>
<option value="unassigned">{{ __('Firma: Ohne Firma') }}</option>
</flux:select>
@endif
</div>
{{-- Active-Chips --}}
@if ($hasAnyFilter)
<div class="flex items-center gap-2 flex-wrap text-[11.5px]">
<span class="eyebrow muted" style="margin-right:2px;">{{ __('Aktiv') }}</span>
@if ($statusFilter !== 'all')
@php $statusEnum = \App\Enums\PressReleaseStatus::tryFrom($statusFilter); @endphp
<span class="active-chip">
<span>{{ __('Status') }}:
<strong>{{ $statusEnum?->label() ?? $statusFilter }}</strong></span>
<button type="button" class="x" wire:click="setView('all')"
aria-label="{{ __('Filter entfernen') }}">
<svg width="8" height="8" viewBox="0 0 12 12" fill="none">
<path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.6"
stroke-linecap="round" />
</svg>
</button>
</span>
@endif
@if ($portalFilter !== 'all')
@php $portalEnum = \App\Enums\Portal::tryFrom($portalFilter); @endphp
<span class="active-chip">
<span>{{ __('Portal') }}:
<strong>{{ $portalEnum?->label() ?? $portalFilter }}</strong></span>
<button type="button" class="x" wire:click="$set('portalFilter', 'all')"
aria-label="{{ __('Filter entfernen') }}">
<svg width="8" height="8" viewBox="0 0 12 12" fill="none">
<path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.6"
stroke-linecap="round" />
</svg>
</button>
</span>
@endif
@if ($hasGlobalCompanyContext && $companyFilter !== 'all')
<span class="active-chip">
<span>{{ __('Firma') }}:
<strong>{{ $companyFilter === 'assigned' ? __('Mit Firma') : __('Ohne Firma') }}</strong></span>
<button type="button" class="x" wire:click="$set('companyFilter', 'all')"
aria-label="{{ __('Filter entfernen') }}">
<svg width="8" height="8" viewBox="0 0 12 12" fill="none">
<path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.6"
stroke-linecap="round" />
</svg>
</button>
</span>
@endif
@if ($search !== '')
<span class="active-chip">
<span>{{ __('Suche') }}:
<strong>{{ \Illuminate\Support\Str::limit($search, 40) }}"</strong></span>
<button type="button" class="x" wire:click="$set('search', '')"
aria-label="{{ __('Filter entfernen') }}">
<svg width="8" height="8" viewBox="0 0 12 12" fill="none">
<path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.6"
stroke-linecap="round" />
</svg>
</button>
</span>
@endif
<button type="button" wire:click="resetFilters"
class="text-[11.5px] font-semibold text-[color:var(--color-ink-3)] hover:text-[color:var(--color-hub)] underline-offset-[3px] hover:underline">
{{ __('Alle zurücksetzen') }}
</button>
</div>
@endif
</div>
</article>
</section>
@endif
{{-- ============== TABELLE-PANEL ============== --}}
{{-- ============== TABELLE / EMPTY ============== --}}
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Alle Pressemitteilungen') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __(':count Einträge', ['count' => $pressReleases->count()]) }}
</span>
</div>
<div class="p-4">
<flux:table>
<flux:table.columns>
<flux:table.column sortable :sorted="$sortBy==='title'" :direction="$sortDir" wire:click="sort('title')">{{ __('Titel') }}</flux:table.column>
<flux:table.column>{{ __('Firma') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy==='status'" :direction="$sortDir" wire:click="sort('status')">{{ __('Status') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy==='created_at'" :direction="$sortDir" wire:click="sort('created_at')">{{ __('Erstellt') }}</flux:table.column>
<flux:table.column>{{ __('Aktionen') }}</flux:table.column>
</flux:table.columns>
@forelse($pressReleases as $pr)
<flux:table.row wire:key="{{ $pr->id }}">
<flux:table.cell>
<p class="max-w-xs truncate font-medium">{{ $pr->title }}</p>
</flux:table.cell>
<flux:table.cell>
<flux:text class="text-sm">{{ $pr->company?->name ?? '' }}</flux:text>
</flux:table.cell>
<flux:table.cell>
<span @class([
'badge',
'ok' => $pr->status->value === 'published',
'warn' => $pr->status->value === 'review',
'err' => $pr->status->value === 'rejected',
'hub' => in_array($pr->status->value, ['archived', 'draft'], true),
])>{{ $pr->status->label() }}</span>
</flux:table.cell>
<flux:table.cell>
<flux:text class="text-sm text-zinc-500">{{ $pr->created_at->format('d.m.Y') }}</flux:text>
</flux:table.cell>
<flux:table.cell>
<div class="flex items-center gap-1">
<flux:button size="sm" variant="ghost" icon="eye" href="{{ route('me.press-releases.show', $pr->id) }}" wire:navigate />
@if(in_array($pr->status->value, ['draft', 'rejected']))
<flux:button size="sm" variant="ghost" icon="pencil" href="{{ route('me.press-releases.edit', $pr->id) }}" wire:navigate />
<flux:button size="sm" variant="ghost" icon="paper-airplane" wire:click="submitForReview({{ $pr->id }})"
wire:confirm="{{ __('Pressemitteilung zur Prüfung einreichen?') }}" />
@endif
</div>
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="5">
<div class="flex flex-col items-center justify-center px-4 py-12 text-center">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-4
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.newspaper class="size-6" />
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Keine Pressemitteilungen gefunden') }}
</div>
<p class="text-[12px] text-[color:var(--color-ink-3)] max-w-md m-0 mb-4">
{{ __('Passen Sie die Filter an oder erstellen Sie eine neue Pressemitteilung.') }}
</p>
<flux:button size="sm" variant="primary" icon="plus" href="{{ route('me.press-releases.create') }}" wire:navigate>
{{ __('Neue Pressemitteilung') }}
</flux:button>
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table>
</div>
@if ($pressReleases->isNotEmpty())
<div class="panel-head">
<span class="section-eyebrow">
@if ($statusFilter !== 'all')
@php $sEnum = \App\Enums\PressReleaseStatus::tryFrom($statusFilter); @endphp
{{ $sEnum?->label() ?? __('Pressemitteilungen') }}
@else
{{ __('Alle Pressemitteilungen') }}
@endif
</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __(':count Einträge', ['count' => $pressReleases->total()]) }}
</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column class="w-[140px]">{{ __('Status') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'title'" :direction="$sortDir"
wire:click="sort('title')">
{{ __('Titel') }}
</flux:table.column>
<flux:table.column class="w-[180px]">{{ __('Portal') }}</flux:table.column>
<flux:table.column class="w-[180px]">{{ __('Firma') }}</flux:table.column>
<flux:table.column class="w-[140px]" sortable :sorted="$sortBy === 'created_at'"
:direction="$sortDir" wire:click="sort('created_at')">
{{ __('Datum') }}
</flux:table.column>
<flux:table.column class="w-[80px]">{{ __('Aktionen') }}</flux:table.column>
</flux:table.columns>
@foreach ($pressReleases as $pr)
@php
$status = $pr->status->value;
$rowClass = match ($status) {
'review' => 'is-row-warn',
'rejected' => 'is-row-err',
default => '',
};
$badgeClass = match ($status) {
'published' => 'ok',
'review' => 'warn',
'rejected' => 'err',
'archived', 'draft' => 'muted',
default => 'hub',
};
$portal = $pr->portal?->value ?? 'both';
$showPe = in_array($portal, ['presseecho', 'both'], true);
$showBp = in_array($portal, ['businessportal24', 'both'], true);
$dateSubLabel = match ($status) {
'published' => __('veröffentlicht'),
'review' => __('eingereicht'),
'rejected' => __('abgelehnt'),
'draft' => __('erstellt'),
'archived' => __('archiviert'),
default => __('erstellt'),
};
$primaryDate = match (true) {
$status === 'published' && $pr->published_at => $pr->published_at,
default => $pr->created_at,
};
@endphp
<flux:table.row wire:key="{{ $pr->id }}" class="{{ $rowClass }}">
<flux:table.cell>
<div class="flex items-center gap-1 flex-wrap">
<span class="badge {{ $badgeClass }} dot">{{ $pr->status->label() }}</span>
@if ($status === 'draft')
<button type="button" class="inline-action"
wire:click="submitForReview({{ $pr->id }})"
wire:confirm="{{ __('Pressemitteilung zur Prüfung einreichen?') }}"
title="{{ __('Zur redaktionellen Prüfung senden') }}">
{{ __('Zur Prüfung →') }}
</button>
@endif
</div>
</flux:table.cell>
<flux:table.cell>
<a href="{{ route('me.press-releases.show', $pr->id) }}" wire:navigate
class="block font-semibold text-[13.5px] leading-[1.35] text-[color:var(--color-ink)] hover:text-[color:var(--color-hub)] hover:underline underline-offset-[3px] decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
{{ $pr->title }}
</a>
<div class="text-[11.5px] text-[color:var(--color-ink-3)] mt-0.5 leading-[1.4]">
PM-{{ $pr->id }}
</div>
</flux:table.cell>
<flux:table.cell>
<div class="flex flex-wrap gap-1">
@if ($showPe)
<span class="portal-pill pe"><span class="pdot"></span>presseecho</span>
@endif
@if ($showBp)
<span class="portal-pill bp"><span
class="pdot"></span>businessportal24</span>
@endif
</div>
</flux:table.cell>
<flux:table.cell>
@if ($pr->company)
<span class="text-[12.5px] text-[color:var(--color-ink-2)] font-medium">
{{ $pr->company->name }}
</span>
@else
<span
class="text-[11.5px] text-[color:var(--color-ink-4)] italic">{{ __('— keine —') }}</span>
@endif
</flux:table.cell>
<flux:table.cell>
<div class="font-mono text-[12px] text-[color:var(--color-ink-2)] tabular-nums">
{{ $primaryDate?->format('d.m.Y') }}
</div>
<div class="text-[10.5px] text-[color:var(--color-ink-4)] mt-0.5">
{{ $dateSubLabel }} · {{ $primaryDate?->format('H:i') }}
</div>
</flux:table.cell>
<flux:table.cell>
<div class="flex items-center gap-1">
<flux:button size="sm" variant="ghost" icon="eye"
href="{{ route('me.press-releases.show', $pr->id) }}" wire:navigate />
@if (in_array($status, ['draft', 'rejected']))
<flux:button size="sm" variant="ghost" icon="pencil"
href="{{ route('me.press-releases.edit', $pr->id) }}" wire:navigate />
@endif
</div>
</flux:table.cell>
</flux:table.row>
@endforeach
</flux:table>
<div class="border-t border-[color:var(--color-bg-rule)] p-4">
{{ $pressReleases->links() }}
</div>
@elseif ($hasAnyPR && $search !== '')
{{-- Empty: Suche ohne Treffer --}}
<div class="empty-stage">
<div class="empty-ico">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
<circle cx="10.5" cy="10.5" r="6" stroke="currentColor" stroke-width="1.6" />
<path d="M15.5 15.5L20 20" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" />
</svg>
</div>
<h3 class="empty-title">{{ __('Keine Treffer für „:term"', ['term' => $search]) }}</h3>
<p class="empty-sub">
{{ __('Wir konnten zu Ihrer Suche nichts finden. Prüfen Sie die Schreibweise oder schränken Sie den Suchbegriff ein.') }}
</p>
<div class="flex items-center gap-2.5 mt-6">
<flux:button variant="primary" wire:click="$set('search', '')">
{{ __('Suche zurücksetzen') }}
</flux:button>
</div>
</div>
@elseif ($hasAnyPR && $hasAnyFilter)
{{-- Empty: Filter ohne Treffer --}}
<div class="empty-stage">
<div class="empty-ico warm">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
<path d="M4 6h16M7 12h10M10 18h4" stroke="currentColor" stroke-width="1.8"
stroke-linecap="round" />
</svg>
</div>
<h3 class="empty-title">{{ __('Keine Mitteilungen mit diesen Filtern') }}</h3>
<p class="empty-sub">
{{ __('Aktive Filter passen auf keine Einträge. Setzen Sie einen Filter zurück oder probieren Sie eine breitere Auswahl.') }}
</p>
<div class="flex items-center gap-2.5 mt-6">
<flux:button variant="primary" wire:click="resetFilters">{{ __('Alle Filter zurücksetzen') }}
</flux:button>
</div>
</div>
@else
{{-- Empty: gar keine PMs überhaupt --}}
<div class="empty-stage">
<div class="empty-ico">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
<rect x="3" y="4" width="14" height="16" stroke="currentColor" stroke-width="1.5" />
<path d="M17 7h4v13H6" stroke="currentColor" stroke-width="1.5" />
<path d="M6 8h8M6 11h8M6 14h5" stroke="currentColor" stroke-width="1.3"
stroke-linecap="round" />
</svg>
</div>
<h3 class="empty-title">{{ __('Noch keine Pressemitteilungen') }}</h3>
<p class="empty-sub">
{{ __('Starten Sie mit Ihrer ersten Mitteilung. Nach redaktioneller Prüfung erscheint sie i. d. R. binnen 4 Stunden werktags auf beiden Portalen.') }}
</p>
<div class="flex items-center gap-2.5 mt-6">
<flux:button variant="primary" icon="plus" href="{{ route('me.press-releases.create') }}"
wire:navigate>
{{ __('Erste Pressemitteilung erstellen') }}
</flux:button>
</div>
<div class="mt-9 grid gap-3 w-full max-w-[560px]" style="grid-template-columns:repeat(3,1fr);">
<div
class="text-left px-3 py-2.5 bg-[color:var(--color-bg-elev)] border border-[color:var(--color-bg-rule)] rounded-[3px]">
<div
class="font-mono text-[9.5px] tracking-[0.16em] text-[color:var(--color-accent-deep)] font-bold mb-1">
01</div>
<div class="text-[11.5px] font-semibold text-[color:var(--color-ink)] leading-tight">
{{ __('Firma zuordnen') }}</div>
</div>
<div
class="text-left px-3 py-2.5 bg-[color:var(--color-bg-elev)] border border-[color:var(--color-bg-rule)] rounded-[3px]">
<div
class="font-mono text-[9.5px] tracking-[0.16em] text-[color:var(--color-accent-deep)] font-bold mb-1">
02</div>
<div class="text-[11.5px] font-semibold text-[color:var(--color-ink)] leading-tight">
{{ __('Mitteilung verfassen') }}</div>
</div>
<div
class="text-left px-3 py-2.5 bg-[color:var(--color-bg-elev)] border border-[color:var(--color-bg-rule)] rounded-[3px]">
<div
class="font-mono text-[9.5px] tracking-[0.16em] text-[color:var(--color-accent-deep)] font-bold mb-1">
03</div>
<div class="text-[11.5px] font-semibold text-[color:var(--color-ink)] leading-tight">
{{ __('Zur Prüfung senden') }}</div>
</div>
</div>
</div>
@endif
</article>
{{ $pressReleases->links() }}
{{-- ============== STATUS-AKTIONEN-LEGENDE ============== --}}
@if ($hasAnyPR)
<article class="panel-warm p-5">
<div class="grid items-start gap-6" style="grid-template-columns:auto 1fr;">
<div class="min-w-[180px]">
<div class="section-eyebrow">{{ __('Aktionen je Status') }}</div>
<p class="text-[12px] text-[color:var(--color-ink-3)] leading-[1.55] mt-3 m-0 max-w-[200px]">
{{ __('Welche Aktionen pro Status verfügbar sind.') }}
</p>
</div>
<div class="grid gap-4" style="grid-template-columns:repeat(5,minmax(0,1fr));">
<div>
<span class="badge muted dot" style="margin-bottom:8px;">{{ __('Entwurf') }}</span>
<ul
class="text-[11.5px] text-[color:var(--color-ink-2)] leading-[1.7] mt-2 list-none p-0 m-0 space-y-0.5">
<li>{{ __('Bearbeiten') }}</li>
<li>{{ __('Vorschau') }}</li>
<li class="text-[color:var(--color-accent-deep)] font-semibold">
{{ __('→ Zur Prüfung senden') }}</li>
</ul>
</div>
<div>
<span class="badge warn dot" style="margin-bottom:8px;">{{ __('In Prüfung') }}</span>
<ul
class="text-[11.5px] text-[color:var(--color-ink-2)] leading-[1.7] mt-2 list-none p-0 m-0 space-y-0.5">
<li>{{ __('Vorschau') }}</li>
<li class="text-[color:var(--color-ink-3)] italic">{{ __('Warten auf Redaktion') }}</li>
</ul>
</div>
<div>
<span class="badge ok dot" style="margin-bottom:8px;">{{ __('Veröffentlicht') }}</span>
<ul
class="text-[11.5px] text-[color:var(--color-ink-2)] leading-[1.7] mt-2 list-none p-0 m-0 space-y-0.5">
<li>{{ __('Vorschau') }}</li>
<li class="text-[color:var(--color-ink-3)] italic">{{ __('Statistik (bald)') }}</li>
</ul>
</div>
<div>
<span class="badge err dot" style="margin-bottom:8px;">{{ __('Abgelehnt') }}</span>
<ul
class="text-[11.5px] text-[color:var(--color-ink-2)] leading-[1.7] mt-2 list-none p-0 m-0 space-y-0.5">
<li>{{ __('Bearbeiten') }}</li>
<li class="text-[color:var(--color-accent-deep)] font-semibold">
{{ __('→ Erneut einreichen') }}</li>
</ul>
</div>
<div>
<span class="badge muted dot" style="margin-bottom:8px;">{{ __('Archiviert') }}</span>
<ul
class="text-[11.5px] text-[color:var(--color-ink-2)] leading-[1.7] mt-2 list-none p-0 m-0 space-y-0.5">
<li>{{ __('Vorschau') }}</li>
</ul>
</div>
</div>
</div>
</article>
@endif
</div>

View file

@ -336,103 +336,135 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
}
}; ?>
<div class="space-y-6">
<flux:card>
<flux:heading size="lg">{{ __('Mein Profil') }}</flux:heading>
<flux:subheading>
{{ __('Hier pflegen Sie Ihre persönlichen Konto- und Profildaten. Firmendaten verwalten Sie direkt in der jeweiligen Firma.') }}
</flux:subheading>
</flux:card>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('User Backend') }}</span>
<span class="eyebrow muted">{{ __('Mein Bereich · Profil') }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Mein Profil') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Hier pflegen Sie Ihre persönlichen Konto- und Profildaten. Firmendaten verwalten Sie direkt in der jeweiligen Firma.') }}
</p>
</div>
</header>
@if(session('profile-status'))
<flux:callout color="green" icon="check-circle">{{ session('profile-status') }}</flux:callout>
@if (session('profile-status'))
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-center gap-2
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
<flux:icon.check-circle class="size-[16px] flex-shrink-0" />
{{ session('profile-status') }}
</div>
@endif
<form wire:submit="saveProfile" class="grid gap-6 lg:grid-cols-2">
<flux:card>
<flux:heading size="sm" class="mb-4">{{ __('Konto') }}</flux:heading>
<div class="space-y-4">
<flux:input wire:model="name" :label="__('Name')" required />
<flux:input value="{{ $user->email }}" :label="__('E-Mail')" disabled :description="__('Änderung über Konto-Sicherheit möglich.')" />
<flux:select wire:model="language" :label="__('Sprache')">
<option value="de">Deutsch</option>
<option value="en">English</option>
</flux:select>
</div>
</flux:card>
<form wire:submit="saveProfile" class="space-y-6">
<div class="grid gap-6 lg:grid-cols-2">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Konto') }}</span>
</div>
<div class="p-5 space-y-4">
<flux:input wire:model="name" :label="__('Name')" required />
<flux:input value="{{ $user->email }}" :label="__('E-Mail')" disabled :description="__('Änderung über Konto-Sicherheit möglich.')" />
<flux:select wire:model="language" :label="__('Sprache')">
<option value="de">Deutsch</option>
<option value="en">English</option>
</flux:select>
</div>
</article>
<flux:card id="profil">
<div class="mb-4 flex flex-wrap gap-2">
<flux:badge color="indigo" size="sm">{{ __('Profil') }}</flux:badge>
<flux:badge color="zinc" size="sm">{{ __('Rechnungsadresse') }}</flux:badge>
</div>
<flux:heading size="sm" class="mb-4">{{ __('Profil') }}</flux:heading>
<div class="grid gap-4 sm:grid-cols-2">
<flux:select wire:model="salutationKey" :label="__('Anrede')">
@foreach($salutations as $key => $label)
<option value="{{ $key }}">{{ $label }}</option>
@endforeach
</flux:select>
<flux:input wire:model="title" :label="__('Titel')" placeholder="Dr." />
<flux:input wire:model="firstName" :label="__('Vorname')" />
<flux:input wire:model="lastName" :label="__('Nachname')" />
<flux:input wire:model="phone" :label="__('Telefon')" />
<flux:input wire:model="backlinkUrl" :label="__('Backlink-URL')" placeholder="https://..." />
<flux:checkbox wire:model="showStats" :label="__('Statistiken in Pressemitteilungen anzeigen')" class="sm:col-span-2" />
<flux:checkbox wire:model="disableFooterCode" :label="__('Footer-Code in Pressemitteilungen deaktivieren')" class="sm:col-span-2" />
</div>
<flux:separator class="my-6" />
<flux:heading id="rechnungsadresse" size="sm" class="mb-2">{{ __('Rechnungsadresse') }}</flux:heading>
<flux:text class="mb-4 text-sm text-zinc-500">
{{ __('Diese Angaben werden für künftige Rechnungen verwendet. Eine vollständige Rechnungsadresse benötigt Name, Adresse, PLZ, Ort und Land.') }}
</flux:text>
@if(! $this->billingIsComplete())
<flux:callout color="amber" icon="exclamation-triangle" class="mb-4">
{{ __('Rechnungsadresse noch unvollständig. Bitte ergänzen Sie die Pflichtangaben, bevor neue Buchungen sauber abgerechnet werden können.') }}
</flux:callout>
@endif
<div class="grid gap-4 sm:grid-cols-2">
<flux:input wire:model="billingName" :label="__('Rechnungsname')" class="sm:col-span-2" />
<flux:input wire:model="billingAddress1" :label="__('Adresse Zeile 1')" class="sm:col-span-2" />
<flux:input wire:model="billingAddress2" :label="__('Adresse Zeile 2')" class="sm:col-span-2" />
<flux:input wire:model="billingPostalCode" :label="__('PLZ')" />
<flux:input wire:model="billingCity" :label="__('Ort')" />
<flux:select wire:model="billingCountryCode" :label="__('Land')">
@foreach($countries as $code => $name)
<option value="{{ $code }}">{{ $name }}</option>
@endforeach
</flux:select>
<flux:input wire:model="taxIdNumber" :label="__('USt-ID')" />
<flux:error name="billingName" class="sm:col-span-2" />
</div>
</flux:card>
<div class="lg:col-span-2 flex justify-end">
<flux:button type="submit" variant="primary">{{ __('Profil speichern') }}</flux:button>
<article class="panel" id="profil">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Profil') }}</span>
</div>
<div class="p-5 grid gap-4 sm:grid-cols-2">
<flux:select wire:model="salutationKey" :label="__('Anrede')">
@foreach ($salutations as $key => $label)
<option value="{{ $key }}">{{ $label }}</option>
@endforeach
</flux:select>
<flux:input wire:model="title" :label="__('Titel')" placeholder="Dr." />
<flux:input wire:model="firstName" :label="__('Vorname')" />
<flux:input wire:model="lastName" :label="__('Nachname')" />
<flux:input wire:model="phone" :label="__('Telefon')" />
<flux:input wire:model="backlinkUrl" :label="__('Backlink-URL')" placeholder="https://..." />
<flux:checkbox wire:model="showStats" :label="__('Statistiken in Pressemitteilungen anzeigen')" class="sm:col-span-2" />
<flux:checkbox wire:model="disableFooterCode" :label="__('Footer-Code in Pressemitteilungen deaktivieren')" class="sm:col-span-2" />
</div>
</article>
</div>
<article class="panel" id="rechnungsadresse">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Rechnungsadresse') }}</span>
</div>
<div class="p-5 space-y-4">
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">
{{ __('Diese Angaben werden für künftige Rechnungen verwendet. Eine vollständige Rechnungsadresse benötigt Name, Adresse, PLZ, Ort und Land.') }}
</p>
@if (! $this->billingIsComplete())
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-start gap-3
bg-[color:var(--color-warn-soft)] border-[color:var(--color-warn)]/30 text-[color:var(--color-ink-2)]">
<flux:icon.exclamation-triangle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-accent-deep)]" />
<div class="flex-1">
{{ __('Rechnungsadresse noch unvollständig. Bitte ergänzen Sie die Pflichtangaben, bevor neue Buchungen sauber abgerechnet werden können.') }}
</div>
</div>
@endif
<div class="grid gap-4 sm:grid-cols-2">
<flux:input wire:model="billingName" :label="__('Rechnungsname')" class="sm:col-span-2" />
<flux:input wire:model="billingAddress1" :label="__('Adresse Zeile 1')" class="sm:col-span-2" />
<flux:input wire:model="billingAddress2" :label="__('Adresse Zeile 2')" class="sm:col-span-2" />
<flux:input wire:model="billingPostalCode" :label="__('PLZ')" />
<flux:input wire:model="billingCity" :label="__('Ort')" />
<flux:select wire:model="billingCountryCode" :label="__('Land')">
@foreach ($countries as $code => $name)
<option value="{{ $code }}">{{ $name }}</option>
@endforeach
</flux:select>
<flux:input wire:model="taxIdNumber" :label="__('USt-ID')" />
<flux:error name="billingName" class="sm:col-span-2" />
</div>
</div>
</article>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Aktionen') }}</span>
</div>
<div class="p-5 flex justify-end">
<flux:button type="submit" variant="primary">{{ __('Profil speichern') }}</flux:button>
</div>
</article>
</form>
<flux:card>
<flux:heading size="sm" class="mb-4">{{ __('Zugeordnete Firmen') }}</flux:heading>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Zugeordnete Firmen') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ $companies->count() }} {{ __('Einträge') }}
</span>
</div>
@forelse($companies as $company)
<div class="flex flex-col gap-2 border-b border-zinc-100 py-3 last:border-0 dark:border-zinc-800 sm:flex-row sm:items-center sm:justify-between">
<div class="space-y-1">
<p class="font-medium text-sm">{{ $company->name }}</p>
@forelse ($companies as $company)
<div class="flex flex-col gap-2 px-5 py-3 border-b border-[color:var(--color-bg-rule)] last:border-0 sm:flex-row sm:items-center sm:justify-between">
<div class="space-y-1 min-w-0">
<p class="text-[13px] font-semibold text-[color:var(--color-ink)] m-0">{{ $company->name }}</p>
<div class="flex flex-wrap items-center gap-2">
<flux:badge color="zinc" size="sm">{{ $company->portal?->label() ?? '' }}</flux:badge>
<flux:badge color="indigo" size="sm">{{ $company->pivot->role ?? 'member' }}</flux:badge>
@if($company->owner_user_id === $user->id)
<flux:badge color="green" size="sm">{{ __('Eigentümer') }}</flux:badge>
<span class="badge hub">{{ $company->portal?->label() ?? '' }}</span>
<span class="badge hub">{{ $company->pivot->role ?? 'member' }}</span>
@if ($company->owner_user_id === $user->id)
<span class="badge ok">{{ __('Eigentümer') }}</span>
@endif
</div>
</div>
@if($company->owner_user_id === $user->id || in_array($company->pivot->role, ['owner', 'responsible'], true))
@if ($company->owner_user_id === $user->id || in_array($company->pivot->role, ['owner', 'responsible'], true))
<flux:button size="sm" variant="ghost" icon="arrow-right" href="{{ route('me.press-kits.show', $company->id) }}" wire:navigate>
{{ __('Firma verwalten') }}
</flux:button>
@ -443,9 +475,9 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
@endif
</div>
@empty
<flux:text class="text-sm text-zinc-500">
<div class="p-5 text-[12.5px] text-[color:var(--color-ink-3)]">
{{ __('Keine Firmen zugeordnet. Bitte wenden Sie sich an den Administrator.') }}
</flux:text>
</div>
@endforelse
</flux:card>
</article>
</div>

View file

@ -138,158 +138,210 @@ new #[Layout('components.layouts.app'), Title('Konto-Sicherheit')] class extends
}
}; ?>
<div class="space-y-6">
<flux:card>
<flux:heading size="lg">{{ __('Konto-Sicherheit') }}</flux:heading>
<flux:subheading>
{{ __('Passwort, E-Mail und Zwei-Faktor-Authentifizierung verwalten.') }}
</flux:subheading>
</flux:card>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('User Backend') }}</span>
<span class="eyebrow muted">{{ __('Mein Bereich · Sicherheit') }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Konto-Sicherheit') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Passwort, E-Mail und Zwei-Faktor-Authentifizierung verwalten.') }}
</p>
</div>
</header>
@if(session('security-status'))
<flux:callout color="green" icon="check-circle">{{ session('security-status') }}</flux:callout>
@if (session('security-status'))
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-center gap-2
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
<flux:icon.check-circle class="size-[16px] flex-shrink-0" />
{{ session('security-status') }}
</div>
@endif
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('E-Mail') }}</flux:text>
<flux:text weight="bold" class="mt-1 truncate">{{ $user->email }}</flux:text>
<flux:badge class="mt-3" color="{{ $user->email_verified_at ? 'green' : 'amber' }}" size="sm">
{{ $user->email_verified_at ? __('Bestätigt') : __('Nicht bestätigt') }}
</flux:badge>
</flux:card>
{{-- ============== KPI-Reihe ============== --}}
<section class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<article class="panel p-5 space-y-2">
<div class="text-[11px] uppercase tracking-[0.6px] font-semibold text-[color:var(--color-ink-3)]">
{{ __('E-Mail') }}
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] truncate">{{ $user->email }}</div>
<div>
@if ($user->email_verified_at)
<span class="badge ok">{{ __('Bestätigt') }}</span>
@else
<span class="badge warn">{{ __('Nicht bestätigt') }}</span>
@endif
</div>
</article>
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Zwei-Faktor') }}</flux:text>
<flux:text weight="bold" class="mt-1">
<article class="panel p-5 space-y-2">
<div class="text-[11px] uppercase tracking-[0.6px] font-semibold text-[color:var(--color-ink-3)]">
{{ __('Zwei-Faktor') }}
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)]">
{{ $twoFactorEnabled ? __('Aktiv') : __('Nicht aktiv') }}
</flux:text>
<flux:badge class="mt-3" color="{{ $twoFactorEnabled ? 'green' : 'zinc' }}" size="sm">
{{ $twoFactorEnabled ? __('Zusatzschutz aktiv') : __('Empfohlen') }}
</flux:badge>
</flux:card>
</div>
<div>
@if ($twoFactorEnabled)
<span class="badge ok">{{ __('Zusatzschutz aktiv') }}</span>
@else
<span class="badge warn">{{ __('Empfohlen') }}</span>
@endif
</div>
</article>
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Letzter Login') }}</flux:text>
<flux:text weight="bold" class="mt-1">
<article class="panel p-5 space-y-2">
<div class="text-[11px] uppercase tracking-[0.6px] font-semibold text-[color:var(--color-ink-3)]">
{{ __('Letzter Login') }}
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)]">
{{ $user->last_login_at?->format('d.m.Y H:i') ?? __('Unbekannt') }}
</flux:text>
<flux:text class="mt-3 text-xs text-zinc-500">
</div>
<div class="text-[11.5px] text-[color:var(--color-ink-3)] truncate">
{{ $user->last_login_ip ?: __('Keine IP gespeichert') }}
</flux:text>
</flux:card>
</div>
</article>
<flux:card>
<flux:text class="text-xs text-zinc-500">{{ __('Aktive Sessions') }}</flux:text>
<flux:text weight="bold" class="mt-1">{{ $sessions->count() }}</flux:text>
<flux:text class="mt-3 text-xs text-zinc-500">
<article class="panel p-5 space-y-2">
<div class="text-[11px] uppercase tracking-[0.6px] font-semibold text-[color:var(--color-ink-3)]">
{{ __('Aktive Sessions') }}
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)]">{{ $sessions->count() }}</div>
<div class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __('Aus den aktuellen Web-Sessions') }}
</flux:text>
</flux:card>
</div>
</div>
</article>
</section>
<div class="grid gap-6 lg:grid-cols-2">
<flux:card>
<flux:heading size="sm" class="mb-4">{{ __('Passwort ändern') }}</flux:heading>
<form wire:submit="updatePassword" class="space-y-4">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Passwort ändern') }}</span>
</div>
<form wire:submit="updatePassword" class="p-5 space-y-4">
<flux:input wire:model="current_password" type="password" :label="__('Aktuelles Passwort')" autocomplete="current-password" required />
<flux:input wire:model="password" type="password" :label="__('Neues Passwort')" autocomplete="new-password" required />
<flux:input wire:model="password_confirmation" type="password" :label="__('Neues Passwort bestätigen')" autocomplete="new-password" required />
<div class="flex justify-end">
<div class="flex justify-end pt-2 border-t border-[color:var(--color-bg-rule)]">
<flux:button type="submit" variant="primary">{{ __('Passwort speichern') }}</flux:button>
</div>
</form>
</flux:card>
</article>
<flux:card>
<flux:heading size="sm" class="mb-4">{{ __('E-Mail-Adresse ändern') }}</flux:heading>
<form wire:submit="updateEmail" class="space-y-4">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('E-Mail-Adresse ändern') }}</span>
</div>
<form wire:submit="updateEmail" class="p-5 space-y-4">
<flux:input wire:model="email" type="email" :label="__('Neue E-Mail-Adresse')" autocomplete="email" required />
<flux:text class="text-xs text-zinc-500">
<p class="text-[12px] text-[color:var(--color-ink-3)] m-0">
{{ __('Nach der Änderung kann eine erneute Bestätigung der E-Mail-Adresse erforderlich sein.') }}
</flux:text>
<div class="flex justify-end">
</p>
<div class="flex justify-end pt-2 border-t border-[color:var(--color-bg-rule)]">
<flux:button type="submit" variant="primary">{{ __('E-Mail speichern') }}</flux:button>
</div>
</form>
</flux:card>
</article>
</div>
<flux:card>
<flux:heading size="sm" class="mb-4">{{ __('Zwei-Faktor-Authentifizierung') }}</flux:heading>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Zwei-Faktor-Authentifizierung') }}</span>
@if ($twoFactorEnabled)
<span class="badge ok dot">{{ __('Aktiv') }}</span>
@endif
</div>
<div class="p-5">
@if (! $twoFactorEnabled)
<p class="text-[13px] text-[color:var(--color-ink-2)] m-0">
{{ __('Schützen Sie Ihren Account zusätzlich mit einer Authenticator-App (TOTP).') }}
</p>
<flux:button class="mt-4" wire:click="enableTwoFactorAuthentication" variant="primary">
{{ __('Zwei-Faktor-Authentifizierung aktivieren') }}
</flux:button>
@else
@if ($twoFactorQrSvg)
<div class="flex flex-col gap-5 lg:flex-row lg:items-start">
{{-- QR-Code: bg-white ist BEWUSST konstant in beiden Modi
QR-Codes brauchen schwarz-auf-weiß für zuverlässiges Scannen. --}}
<div class="rounded-[6px] border border-[color:var(--color-bg-rule)] bg-white p-4 flex-shrink-0">
{!! $twoFactorQrSvg !!}
</div>
<div class="space-y-3 min-w-0">
<p class="text-[13px] text-[color:var(--color-ink-2)] m-0">
{{ __('Scannen Sie den QR-Code mit Ihrer Authenticator-App (z. B. 1Password, Google Authenticator).') }}
</p>
@if (! empty($recoveryCodes))
<div class="text-[11px] uppercase tracking-[0.6px] font-semibold text-[color:var(--color-ink-3)]">
{{ __('Wiederherstellungs-Codes') }}
</div>
<ul class="grid grid-cols-2 gap-2 text-[11.5px] font-mono m-0 p-0 list-none">
@foreach ($recoveryCodes as $code)
<li class="rounded-[4px] bg-[color:var(--color-bg-elev)] border border-[color:var(--color-bg-rule)] px-2 py-1 text-[color:var(--color-ink)]">{{ $code }}</li>
@endforeach
</ul>
@endif
</div>
</div>
@endif
@if(! $twoFactorEnabled)
<flux:text class="text-sm text-zinc-500">
{{ __('Schützen Sie Ihren Account zusätzlich mit einer Authenticator-App (TOTP).') }}
</flux:text>
<flux:button class="mt-4" wire:click="enableTwoFactorAuthentication" variant="primary">
{{ __('Zwei-Faktor-Authentifizierung aktivieren') }}
</flux:button>
@else
@if($twoFactorQrSvg)
<div class="flex flex-col gap-4 lg:flex-row lg:items-start">
<div class="rounded-md border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-800">
{!! $twoFactorQrSvg !!}
</div>
<div class="space-y-3">
<flux:text class="text-sm">
{{ __('Scannen Sie den QR-Code mit Ihrer Authenticator-App (z. B. 1Password, Google Authenticator).') }}
</flux:text>
@if(! empty($recoveryCodes))
<flux:heading size="xs">{{ __('Wiederherstellungs-Codes') }}</flux:heading>
<ul class="grid grid-cols-2 gap-2 text-xs font-mono">
@foreach($recoveryCodes as $code)
<li class="rounded bg-zinc-100 px-2 py-1 dark:bg-zinc-800">{{ $code }}</li>
@endforeach
</ul>
@endif
</div>
<div class="mt-5 pt-4 border-t border-[color:var(--color-bg-rule)] flex flex-wrap gap-2">
<flux:button wire:click="regenerateRecoveryCodes" variant="ghost">
{{ __('Neue Wiederherstellungs-Codes erzeugen') }}
</flux:button>
<flux:button wire:click="disableTwoFactorAuthentication" variant="danger">
{{ __('Zwei-Faktor deaktivieren') }}
</flux:button>
</div>
@endif
</div>
</article>
<div class="mt-4 flex flex-wrap gap-2">
<flux:button wire:click="regenerateRecoveryCodes" variant="ghost">
{{ __('Neue Wiederherstellungs-Codes erzeugen') }}
</flux:button>
<flux:button wire:click="disableTwoFactorAuthentication" variant="danger">
{{ __('Zwei-Faktor deaktivieren') }}
</flux:button>
</div>
@endif
</flux:card>
<flux:card class="p-0">
<div class="border-b border-zinc-200 px-4 py-3 dark:border-zinc-700">
<flux:heading size="sm">{{ __('Aktive Sessions') }}</flux:heading>
<flux:text class="mt-1 text-sm text-zinc-500">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Aktive Sessions') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ $sessions->count() }} {{ __('Einträge') }}
</span>
</div>
<div class="px-5 pb-2 pt-3">
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">
{{ __('Hier sehen Sie die letzten bekannten Web-Sessions Ihres Kontos. Abmelden erfolgt aktuell über das Nutzer-Menü.') }}
</flux:text>
</p>
</div>
<div class="divide-y divide-zinc-100 dark:divide-zinc-800">
@forelse($sessions as $session)
<div class="flex flex-col gap-2 px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
<div class="divide-y divide-[color:var(--color-bg-rule)]">
@forelse ($sessions as $session)
<div class="flex flex-col gap-2 px-5 py-3 sm:flex-row sm:items-center sm:justify-between">
<div class="min-w-0">
<flux:text weight="semibold">
<div class="text-[13px] font-semibold text-[color:var(--color-ink)]">
{{ $session->ip_address ?: __('IP unbekannt') }}
</flux:text>
<flux:text class="mt-1 truncate text-xs text-zinc-500">
</div>
<div class="mt-0.5 text-[11.5px] text-[color:var(--color-ink-3)] truncate">
{{ Str::limit($session->user_agent ?: __('User-Agent unbekannt'), 120) }}
</flux:text>
</div>
</div>
<flux:badge color="zinc" size="sm">
<span class="badge hub">
{{ \Carbon\Carbon::createFromTimestamp($session->last_activity)->diffForHumans() }}
</flux:badge>
</span>
</div>
@empty
<div class="flex flex-col items-center justify-center px-4 py-10 text-center">
<flux:icon.shield-check class="size-10 text-zinc-300" />
<flux:text weight="semibold" class="mt-3">{{ __('Keine Sessions gefunden') }}</flux:text>
<flux:text class="mt-1 max-w-md text-sm text-zinc-500">
<div class="flex flex-col items-center justify-center px-5 py-10 text-center">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-3
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.shield-check class="size-6" />
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)]">{{ __('Keine Sessions gefunden') }}</div>
<p class="mt-1 max-w-md text-[12px] text-[color:var(--color-ink-3)] m-0">
{{ __('Sobald Sessions protokolliert werden, erscheinen sie hier.') }}
</flux:text>
</p>
</div>
@endforelse
</div>
</flux:card>
</article>
</div>

View file

@ -85,76 +85,107 @@ new #[Layout('components.layouts.app'), Title('API-Tokens')] class extends Compo
}
}; ?>
<div class="space-y-6">
<flux:card>
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<flux:heading size="lg">{{ __('API-Tokens') }}</flux:heading>
<flux:subheading>{{ __('Erstellen und widerrufen Sie persönliche Tokens für die neue API v1.') }}</flux:subheading>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('User Backend') }}</span>
<span class="eyebrow muted">{{ __('Mein Bereich · API') }}</span>
</div>
<flux:button href="{{ route('docs.api.v1') }}" variant="subtle">
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('API-Tokens') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Erstellen und widerrufen Sie persönliche Tokens für die neue API v1.') }}
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button href="{{ route('docs.api.v1') }}" variant="ghost" icon="book-open">
{{ __('API-Dokumentation') }}
</flux:button>
</div>
</flux:card>
</header>
@if($notification)
<flux:callout color="green" icon="check-circle">
@if ($notification)
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-center gap-2
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
<flux:icon.check-circle class="size-[16px] flex-shrink-0" />
{{ $notification }}
</flux:callout>
</div>
@endif
@if($eligibilityMessage || $apiTokenDenialReason)
<flux:callout color="yellow" icon="lock-closed">
{{ $eligibilityMessage ?? $apiTokenDenialReason }}
</flux:callout>
@if ($eligibilityMessage || $apiTokenDenialReason)
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-start gap-3
bg-[color:var(--color-warn-soft)] border-[color:var(--color-warn)]/30 text-[color:var(--color-ink-2)]">
<flux:icon.lock-closed class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-accent-deep)]" />
<div class="flex-1">{{ $eligibilityMessage ?? $apiTokenDenialReason }}</div>
</div>
@endif
@if($plainTextToken)
<flux:callout color="yellow" icon="key">
<div class="space-y-3">
<flux:text weight="semibold">{{ __('Neuer Token') }}</flux:text>
<code class="block overflow-x-auto rounded-md bg-zinc-950 px-3 py-2 text-sm text-white">{{ $plainTextToken }}</code>
@if ($plainTextToken)
<article class="panel" style="border-left:3px solid var(--color-warn);">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Neuer Token') }}</span>
<span class="badge warn">{{ __('Nur jetzt sichtbar') }}</span>
</div>
</flux:callout>
<div class="p-5 space-y-3">
<p class="text-[12.5px] text-[color:var(--color-ink-2)] m-0">
{{ __('Bitte kopieren Sie ihn jetzt, er wird später nicht erneut angezeigt.') }}
</p>
{{-- Token-Anzeige: dunkler Hintergrund konstant in Light + Dark
(deshalb panel-dark-2 statt --color-ink, das im Dark Mode hell wird). --}}
<code class="block overflow-x-auto rounded-[5px] bg-[color:var(--color-panel-dark-2)] px-3 py-2 text-[12px] text-white font-mono">{{ $plainTextToken }}</code>
</div>
</article>
@endif
{{-- ============== FORM-PANEL ============== --}}
<form wire:submit="createToken">
<flux:card class="space-y-5">
<div>
<flux:heading size="sm">{{ __('Neuen Token erstellen') }}</flux:heading>
<flux:text class="mt-1 text-sm text-zinc-500">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Neuen Token erstellen') }}</span>
</div>
<div class="p-5 space-y-5">
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">
{{ __('Wählen Sie nur die Berechtigungen aus, die der jeweilige API-Client wirklich benötigt.') }}
</flux:text>
</div>
</p>
<flux:field>
<flux:label>{{ __('Name') }}</flux:label>
<flux:input wire:model="tokenName" placeholder="{{ __('z.B. Website-Integration') }}" />
<flux:error name="tokenName" />
</flux:field>
<flux:field>
<flux:label>{{ __('Name') }}</flux:label>
<flux:input wire:model="tokenName" placeholder="{{ __('z.B. Website-Integration') }}" />
<flux:error name="tokenName" />
</flux:field>
<div>
<flux:label>{{ __('Berechtigungen') }}</flux:label>
<div class="mt-3 grid gap-3 md:grid-cols-2">
@foreach($abilityOptions as $ability => $label)
<flux:checkbox wire:model="selectedAbilities" value="{{ $ability }}" label="{{ $label }}" />
@endforeach
<div>
<flux:label>{{ __('Berechtigungen') }}</flux:label>
<div class="mt-3 grid gap-3 md:grid-cols-2">
@foreach ($abilityOptions as $ability => $label)
<flux:checkbox wire:model="selectedAbilities" value="{{ $ability }}" label="{{ $label }}" />
@endforeach
</div>
<flux:error name="selectedAbilities" class="mt-3" />
</div>
<flux:error name="selectedAbilities" class="mt-3" />
</div>
<div class="flex justify-end">
<flux:button type="submit" variant="primary" icon="key" :disabled="! $canCreateApiToken">
{{ __('Token erstellen') }}
</flux:button>
<div class="flex justify-end pt-3 border-t border-[color:var(--color-bg-rule)]">
<flux:button type="submit" variant="primary" icon="key" :disabled="! $canCreateApiToken">
{{ __('Token erstellen') }}
</flux:button>
</div>
</div>
</flux:card>
</article>
</form>
<flux:card class="p-0">
<div class="p-4">
<flux:table>
{{-- ============== TABELLE ============== --}}
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Bestehende Tokens') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __(':count Einträge', ['count' => $tokens->count()]) }}
</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Name') }}</flux:table.column>
<flux:table.column>{{ __('Berechtigungen') }}</flux:table.column>
@ -163,23 +194,27 @@ new #[Layout('components.layouts.app'), Title('API-Tokens')] class extends Compo
<flux:table.column>{{ __('Aktionen') }}</flux:table.column>
</flux:table.columns>
@forelse($tokens as $token)
@forelse ($tokens as $token)
<flux:table.row wire:key="token-{{ $token->id }}">
<flux:table.cell>
<flux:text weight="semibold">{{ $token->name }}</flux:text>
<span class="text-[13px] font-semibold text-[color:var(--color-ink)]">{{ $token->name }}</span>
</flux:table.cell>
<flux:table.cell>
<div class="flex flex-wrap gap-1">
@foreach($token->abilities ?? [] as $ability)
<flux:badge size="sm" color="zinc">{{ $ability }}</flux:badge>
@foreach ($token->abilities ?? [] as $ability)
<span class="badge hub">{{ $ability }}</span>
@endforeach
</div>
</flux:table.cell>
<flux:table.cell>
<flux:text class="text-sm text-zinc-500">{{ $token->created_at?->format('d.m.Y H:i') }}</flux:text>
<span class="text-[12px] text-[color:var(--color-ink-3)]">
{{ $token->created_at?->format('d.m.Y H:i') }}
</span>
</flux:table.cell>
<flux:table.cell>
<flux:text class="text-sm text-zinc-500">{{ $token->last_used_at?->format('d.m.Y H:i') ?? __('Nie') }}</flux:text>
<span class="text-[12px] text-[color:var(--color-ink-3)]">
{{ $token->last_used_at?->format('d.m.Y H:i') ?? __('Nie') }}
</span>
</flux:table.cell>
<flux:table.cell>
<flux:button
@ -197,16 +232,20 @@ new #[Layout('components.layouts.app'), Title('API-Tokens')] class extends Compo
<flux:table.row>
<flux:table.cell colspan="5">
<div class="flex flex-col items-center justify-center px-4 py-10 text-center">
<flux:icon.key class="size-10 text-zinc-300" />
<flux:text weight="semibold" class="mt-3">{{ __('Keine API-Tokens vorhanden') }}</flux:text>
<flux:text class="mt-1 max-w-md text-sm text-zinc-500">
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-3
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.key class="size-6" />
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Keine API-Tokens vorhanden') }}
</div>
<p class="text-[12px] text-[color:var(--color-ink-3)] max-w-md m-0">
{{ __('Erstellen Sie erst dann einen Token, wenn eine konkrete API-Integration ihn benötigt.') }}
</flux:text>
</p>
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table>
</div>
</flux:card>
</flux:table>
</article>
</div>

View file

@ -23,17 +23,28 @@ new class extends Component
}
}; ?>
<section class="mt-10 space-y-6">
<div class="relative mb-5">
<flux:heading>{{ __('Delete account') }}</flux:heading>
<flux:subheading>{{ __('Delete your account and all of its resources') }}</flux:subheading>
</div>
<section>
<div class="rounded-[6px] border-l-[3px] border-l-[color:var(--color-err)] border-[color:var(--color-bg-rule)] border bg-[color:var(--color-err-soft)]/30 p-5">
<div class="text-[11px] uppercase tracking-[0.6px] font-semibold text-[color:var(--color-err)] mb-2">
{{ __('Danger Zone') }}
</div>
<div class="flex items-start justify-between gap-4 flex-wrap">
<div class="min-w-0 flex-1">
<h3 class="text-[14px] font-semibold text-[color:var(--color-ink)] m-0">
{{ __('Delete account') }}
</h3>
<p class="text-[12.5px] text-[color:var(--color-ink-2)] mt-1 m-0">
{{ __('Delete your account and all of its resources') }}
</p>
</div>
<flux:modal.trigger name="confirm-user-deletion">
<flux:button variant="danger" x-data="" x-on:click.prevent="$dispatch('open-modal', 'confirm-user-deletion')">
{{ __('Delete account') }}
</flux:button>
</flux:modal.trigger>
<flux:modal.trigger name="confirm-user-deletion">
<flux:button variant="danger" icon="trash" x-data="" x-on:click.prevent="$dispatch('open-modal', 'confirm-user-deletion')">
{{ __('Delete account') }}
</flux:button>
</flux:modal.trigger>
</div>
</div>
<flux:modal name="confirm-user-deletion" :show="$errors->isNotEmpty()" focusable class="max-w-lg">
<form wire:submit="deleteUser" class="space-y-6">

View file

@ -45,7 +45,7 @@ new #[Layout('components.layouts.app')] class extends Component
@include('partials.settings-heading')
<x-settings.layout :heading="__('Update password')" :subheading="__('Ensure your account is using a long, random password to stay secure')">
<form wire:submit="updatePassword" class="mt-6 space-y-6">
<form wire:submit="updatePassword" class="space-y-5">
<flux:input
wire:model="current_password"
:label="__('Current password')"
@ -68,13 +68,11 @@ new #[Layout('components.layouts.app')] class extends Component
autocomplete="new-password"
/>
<div class="flex items-center gap-4">
<div class="flex items-center justify-end">
<flux:button variant="primary" type="submit" class="w-full">{{ __('Save') }}</flux:button>
</div>
<div class="flex items-center gap-4 pt-2 border-t border-[color:var(--color-bg-rule)]">
<flux:button variant="primary" type="submit">{{ __('Save') }}</flux:button>
<x-action-message class="me-3" on="password-updated">
{{ __('Saved.') }}
<x-action-message on="password-updated">
<span class="text-[12px] text-[color:var(--color-gain-deep)]">{{ __('Saved.') }}</span>
</x-action-message>
</div>
</form>

View file

@ -76,42 +76,45 @@ new #[Layout('components.layouts.app')] class extends Component
@include('partials.settings-heading')
<x-settings.layout :heading="__('Profile')" :subheading="__('Update your name and email address')">
<form wire:submit="updateProfileInformation" class="my-6 w-full space-y-6">
<form wire:submit="updateProfileInformation" class="w-full space-y-5">
<flux:input wire:model="name" :label="__('Name')" type="text" required autofocus autocomplete="name" />
<div>
<flux:input wire:model="email" :label="__('Email')" type="email" required autocomplete="email" />
@if (auth()->user() instanceof \Illuminate\Contracts\Auth\MustVerifyEmail &&! auth()->user()->hasVerifiedEmail())
<div>
<flux:text class="mt-4">
@if (auth()->user() instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! auth()->user()->hasVerifiedEmail())
<div class="mt-3 px-4 py-3 rounded-[5px] border text-[12.5px] flex items-start gap-3
bg-[color:var(--color-warn-soft)] border-[color:var(--color-warn)]/30 text-[color:var(--color-ink-2)]">
<flux:icon.exclamation-triangle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-accent-deep)]" />
<div class="flex-1">
{{ __('Your email address is unverified.') }}
<flux:link class="text-sm cursor-pointer" wire:click.prevent="resendVerificationNotification">
<button type="button"
wire:click.prevent="resendVerificationNotification"
class="underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)] text-[color:var(--color-hub)] cursor-pointer">
{{ __('Click here to re-send the verification email.') }}
</flux:link>
</flux:text>
</button>
@if (session('status') === 'verification-link-sent')
<flux:text class="mt-2 font-medium !dark:text-green-400 !text-green-600">
{{ __('A new verification link has been sent to your email address.') }}
</flux:text>
@endif
@if (session('status') === 'verification-link-sent')
<div class="mt-2 text-[color:var(--color-gain-deep)] font-medium">
{{ __('A new verification link has been sent to your email address.') }}
</div>
@endif
</div>
</div>
@endif
</div>
<div class="flex items-center gap-4">
<div class="flex items-center justify-end">
<flux:button variant="primary" type="submit" class="w-full">{{ __('Save') }}</flux:button>
</div>
<div class="flex items-center gap-4 pt-2 border-t border-[color:var(--color-bg-rule)]">
<flux:button variant="primary" type="submit">{{ __('Save') }}</flux:button>
<x-action-message class="me-3" on="profile-updated">
{{ __('Saved.') }}
<x-action-message on="profile-updated">
<span class="text-[12px] text-[color:var(--color-gain-deep)]">{{ __('Saved.') }}</span>
</x-action-message>
</div>
</form>
<livewire:settings.delete-user-form />
<div class="mt-8 pt-6 border-t border-[color:var(--color-bg-rule)]">
<livewire:settings.delete-user-form />
</div>
</x-settings.layout>
</section>

View file

@ -15,6 +15,25 @@
<link rel="stylesheet" href="{{ asset('vendor/livewire/livewire.css') }}">
@fluxAppearance
{{-- Phase 5 Anti-Flash-Bridge: siehe partials/head.blade.php. Identische
Cookie-Logik, damit Controller+Blade Admin-Pages denselben Mechanismus
bekommen wie Volt-Pages. --}}
<script>
(function () {
if (typeof window.Flux === 'undefined') return;
const writeCookie = () => {
const applied = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
document.cookie = 'flux_appearance=' + applied + '; path=/; max-age=31536000; SameSite=Lax';
};
const original = window.Flux.applyAppearance;
window.Flux.applyAppearance = function (appearance) {
original.call(this, appearance);
writeCookie();
};
writeCookie();
})();
</script>
<!-- Debug: Asset-Status -->
<script>
console.log('Admin Head geladen');

View file

@ -20,3 +20,25 @@
Für x-data im Portal greift Alpine aus @fluxScripts. --}}
@vite(['resources/css/portal.css'], 'build/portal')
@fluxAppearance
{{-- Phase 5 Anti-Flash-Bridge: FluxUI speichert die Erscheinung nur in
LocalStorage. Bei wire:navigate morpht Livewire das DOM und das neue
HTML kommt vom Server OHNE class="dark" kurzer weißer Flash, bis das
JS die Klasse wieder anhängt. Wir spiegeln den effektiv applizierten
Modus in ein Cookie, das der Server beim nächsten Render liest und
class="dark" direkt im <html>-Tag setzt (siehe Layout-Files). --}}
<script>
(function () {
if (typeof window.Flux === 'undefined') return;
const writeCookie = () => {
const applied = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
document.cookie = 'flux_appearance=' + applied + '; path=/; max-age=31536000; SameSite=Lax';
};
const original = window.Flux.applyAppearance;
window.Flux.applyAppearance = function (appearance) {
original.call(this, appearance);
writeCookie();
};
writeCookie();
})();
</script>

View file

@ -1,5 +1,15 @@
<div class="relative mb-6 w-full">
<flux:heading size="xl" level="1">{{ __('Settings') }}</flux:heading>
<flux:subheading size="lg" class="mb-6">{{ __('Manage your profile and account settings') }}</flux:subheading>
<flux:separator variant="subtle" />
</div>
{{-- Hub-style settings page header (used by all /settings/* pages) --}}
<header class="mb-8 grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Mein Konto · Einstellungen') }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Settings') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Manage your profile and account settings') }}
</p>
</div>
</header>

View file

@ -1,21 +0,0 @@
<x-layouts.auth>
<div class="flex flex-col gap-6 p-8 bg-white rounded-lg shadow-lg max-w-md mx-auto">
<h1 class="text-2xl font-bold text-gray-900 text-center">Einfacher Test</h1>
<p class="text-gray-600 text-center">Diese Seite funktioniert ohne Volt/Livewire</p>
<div class="mt-4 p-4 bg-green-100 rounded text-sm">
<h2 class="font-semibold mb-2">Erfolg!</h2>
<p>Das Layout funktioniert korrekt.</p>
</div>
<div class="mt-4 p-4 bg-blue-100 rounded text-sm">
<h2 class="font-semibold mb-2">Informationen:</h2>
<ul class="space-y-1">
<li><strong>Route:</strong> {{ request()->url() }}</li>
<li><strong>Domain:</strong> {{ request()->getHost() }}</li>
<li><strong>Method:</strong> {{ request()->method() }}</li>
</ul>
</div>
</div>
</x-layouts.auth>