Rebrand Hub+Flux
This commit is contained in:
parent
0a3e52d603
commit
9b47296cea
130 changed files with 9357 additions and 3345 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue