presseportale/resources/views/livewire/customer/press-kits/show.blade.php
Kevin Adametz 3ef1b84600 customer: Sichtbarkeit belegen - Aufrufe im Verlauf, Newsroom-Uebersicht (Roadmap 1c)
- VisibilityStats: Firmen-Kennzahlen ueber alle Meldungen, Aufrufe je
  Meldung (30 Tage), lueckenlose Tagesreihe (Tage vor der Zaehlung sind
  null, nicht 0), Sitemap-seit und Google-Suchlink (Indexierung Variante B).
- Firmenseite: alle Meldungen der Firma statt nur eigene (Beansprucher
  sahen eine leere Liste), Aufrufe-Spalten, echtes Statistik-Panel.
- Meldungsseite: Panel Sichtbarkeit mit SVG-Tagesbalken 30/90 Tage.
- Claim aus der Veroeffentlichungsmail landet auf der Meldung
  (payload press_release_id), Mail-Hinweis erweitert, ein Element (E4).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 11:53:09 +00:00

848 lines
44 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

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

<?php
use App\Models\Company;
use App\Models\Contact;
use App\Models\PressRelease;
use App\Services\Company\CompanyProfileCheck;
use App\Services\Conversion\ConversionRecorder;
use App\Services\Customer\CustomerCompanyContext;
use App\Services\Image\ImageService;
use App\Services\PressRelease\VisibilityStats;
use App\Support\WebCaches;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Title;
use Livewire\Volt\Component;
use Livewire\WithFileUploads;
new #[Layout('components.layouts.app'), Title('Firma')] class extends Component {
use WithFileUploads;
#[Locked]
public int $id;
public bool $showCompanyForm = false;
public string $companyName = '';
public string $companyAddress = '';
public string $companyEmail = '';
public string $companyPhone = '';
public string $companyWebsite = '';
public string $companyCountryCode = 'DE';
public string $companyBoilerplate = '';
public bool $companyDisableFooterCode = false;
public $companyLogo = null;
public bool $removeCompanyLogo = false;
public bool $showContactForm = false;
public ?int $editingContactId = null;
public string $contactFirstName = '';
public string $contactLastName = '';
public string $contactResponsibility = '';
public string $contactEmail = '';
public string $contactPhone = '';
public function mount(int $id): void
{
$this->id = $id;
$context = app(CustomerCompanyContext::class);
$company = $context->findFor(auth()->user(), $id);
abort_unless($company !== null, 404);
$context->select(auth()->user(), $id);
}
public function startEditCompany(): void
{
$company = $this->company();
$this->authorize('update', $company);
$this->companyName = (string) $company->name;
$this->companyAddress = (string) ($company->address ?? '');
$this->companyEmail = (string) ($company->email ?? '');
$this->companyPhone = (string) ($company->phone ?? '');
$this->companyWebsite = (string) ($company->website ?? '');
$this->companyCountryCode = (string) ($company->country_code ?? 'DE');
$this->companyBoilerplate = (string) ($company->boilerplate ?? '');
$this->companyDisableFooterCode = (bool) $company->disable_footer_code;
$this->companyLogo = null;
$this->removeCompanyLogo = false;
$this->showCompanyForm = true;
}
public function cancelCompanyForm(): void
{
$this->resetCompanyForm();
}
public function saveCompany(ImageService $imageService, CompanyProfileCheck $profileCheck, ConversionRecorder $conversions): void
{
$company = $this->company();
$this->authorize('update', $company);
$validated = $this->validate([
'companyName' => ['required', 'string', 'max:255'],
'companyAddress' => ['nullable', 'string', 'max:1000'],
'companyEmail' => ['nullable', 'email', 'max:190'],
'companyPhone' => ['nullable', 'string', 'max:40'],
'companyWebsite' => ['nullable', 'url', 'max:190'],
'companyCountryCode' => ['nullable', 'string', 'size:2', Rule::in(array_keys((array) config('countries.items', [])))],
// Reiner Text (Roadmap 1b.3): Links im Profiltext sind nicht
// vorgesehen — sonst wird das Profil ein Backlink-Automat. Die
// Website ist das eine, systemseitig ausgezeichnete Link-Feld.
'companyBoilerplate' => ['nullable', 'string', 'max:2000'],
'companyLogo' => ['nullable', 'image', 'max:' . (int) (ImageService::MAX_LOGO_BYTES / 1024)],
]);
$boilerplate = trim(strip_tags((string) $validated['companyBoilerplate']));
// Rot-Ebene der bestehenden Prüfung — dieselbe wie für eine Meldung.
if ($boilerplate !== '' && $boilerplate !== trim((string) $company->boilerplate) && $profileCheck->isRed($company, $boilerplate)) {
throw ValidationException::withMessages([
'companyBoilerplate' => __('Die Beschreibung enthält unzulässige Inhalte und kann so nicht gespeichert werden.'),
]);
}
$company->fill([
'name' => $validated['companyName'],
'address' => $validated['companyAddress'] ?: null,
'email' => $validated['companyEmail'] ?: null,
'phone' => $validated['companyPhone'] ?: null,
'website' => $validated['companyWebsite'] ?: null,
'country_code' => $validated['companyCountryCode'] ?: null,
'boilerplate' => $boilerplate !== '' ? $boilerplate : null,
'disable_footer_code' => $this->companyDisableFooterCode,
]);
if ($this->removeCompanyLogo) {
$imageService->deleteCompanyLogo($company->logo_path, $company->logo_variants);
$company->logo_path = null;
$company->logo_variants = null;
}
if ($this->companyLogo) {
$imageService->deleteCompanyLogo($company->logo_path, $company->logo_variants);
$stored = $imageService->storeCompanyLogo($this->companyLogo, $company->portal?->value ?? 'presseecho', $company->id);
$company->logo_path = $stored['path'];
$company->logo_variants = $stored['variants'];
}
$company->save();
$this->resetCompanyForm();
// Stufe 3 der Konversionsleiter: Newsroom gepflegt (einmal je Firma).
$conversions->recordNewsroomEdited($company, auth()->user());
// Der öffentliche Newsroom zeigt Name, Logo und Beschreibung — die
// Änderung soll nicht erst nach Ablauf des Seiten-Caches sichtbar sein.
WebCaches::forgetPublicPages();
session()->flash('company-status', __('Stammdaten wurden gespeichert.'));
}
public function startCreateContact(): void
{
$this->authorize('update', $this->company());
$this->resetContactForm();
$this->showContactForm = true;
}
public function editContact(int $contactId): void
{
$this->authorize('update', $this->company());
$contact = $this->contact($contactId);
$this->editingContactId = $contact->id;
$this->contactFirstName = (string) ($contact->first_name ?? '');
$this->contactLastName = (string) ($contact->last_name ?? '');
$this->contactResponsibility = (string) ($contact->responsibility ?? '');
$this->contactEmail = (string) ($contact->email ?? '');
$this->contactPhone = (string) ($contact->phone ?? '');
$this->showContactForm = true;
}
public function cancelContactForm(): void
{
$this->resetContactForm();
}
public function saveContact(): void
{
$company = $this->company();
$this->authorize('update', $company);
$validated = $this->validate([
'contactFirstName' => ['nullable', 'string', 'max:80'],
'contactLastName' => ['nullable', 'string', 'max:80'],
'contactResponsibility' => ['nullable', 'string', 'max:255'],
'contactEmail' => ['required', 'email', 'max:255'],
'contactPhone' => ['nullable', 'string', 'max:40'],
]);
if (blank($validated['contactFirstName']) && blank($validated['contactLastName'])) {
throw ValidationException::withMessages([
'contactLastName' => __('Bitte geben Sie mindestens einen Namen an.'),
]);
}
$payload = [
'company_id' => $company->id,
'portal' => $company->portal?->value,
'first_name' => $validated['contactFirstName'] ?: null,
'last_name' => $validated['contactLastName'] ?: null,
'responsibility' => $validated['contactResponsibility'] ?: null,
'email' => $validated['contactEmail'],
'phone' => $validated['contactPhone'] ?: null,
];
if ($this->editingContactId) {
$this->contact($this->editingContactId)->update($payload);
session()->flash('contact-status', __('Pressekontakt wurde aktualisiert.'));
} else {
Contact::query()->create($payload);
session()->flash('contact-status', __('Pressekontakt wurde angelegt.'));
}
$this->resetContactForm();
}
public function deleteContact(int $contactId): void
{
$this->authorize('update', $this->company());
$contact = $this->contact($contactId);
$contact->delete();
if ($this->editingContactId === $contactId) {
$this->resetContactForm();
}
session()->flash('contact-status', __('Pressekontakt wurde gelöscht.'));
}
public function with(): array
{
$user = auth()->user();
$context = app(CustomerCompanyContext::class);
$stats = app(VisibilityStats::class);
$company = $context
->accessibleCompanyQuery($user)
->withCount(['contacts', 'pressReleases'])
->findOrFail($this->id);
return [
'company' => $company,
'roleLabel' => $context->roleLabelFor($company, $user),
'canManageCompany' => $user->can('update', $company),
'canManageContacts' => $user->can('update', $company),
'countries' => (array) config('countries.items', []),
'contacts' => Contact::withoutGlobalScopes()
->where('company_id', $company->id)
->withCount('pressReleases')
->orderBy('last_name')
->orderBy('first_name')
->limit(10)
->get(['id', 'company_id', 'first_name', 'last_name', 'responsibility', 'email', 'phone']),
// Alle Meldungen der Firma, nicht nur die eigenen (Roadmap 1c.3):
// Wer den Newsroom beansprucht hat, soll sehen, was connektar
// für ihn eingeliefert hat — die Policy erlaubt das Firmen-
// mitgliedern ohnehin. Sortiert nach Veröffentlichung, Entwürfe
// nach Anlage.
'pressReleases' => $pressReleases = PressRelease::withoutGlobalScopes()
->where('company_id', $company->id)
->orderByRaw('coalesce(published_at, created_at) desc')
->limit(15)
->get(['id', 'title', 'status', 'created_at', 'published_at', 'hits', 'user_id']),
'recentViews' => $stats->recentViewsFor($pressReleases->modelKeys(), 30),
'summary' => $stats->companySummary($company, 30),
];
}
private function company(): Company
{
$company = app(CustomerCompanyContext::class)->findFor(auth()->user(), $this->id);
abort_unless($company !== null, 404);
return $company;
}
private function contact(int $contactId): Contact
{
return Contact::withoutGlobalScopes()->where('company_id', $this->id)->findOrFail($contactId);
}
private function resetCompanyForm(): void
{
$this->showCompanyForm = false;
$this->companyName = '';
$this->companyAddress = '';
$this->companyEmail = '';
$this->companyPhone = '';
$this->companyWebsite = '';
$this->companyCountryCode = 'DE';
$this->companyBoilerplate = '';
$this->companyDisableFooterCode = false;
$this->companyLogo = null;
$this->removeCompanyLogo = false;
$this->resetValidation();
}
private function resetContactForm(): void
{
$this->showContactForm = false;
$this->editingContactId = null;
$this->contactFirstName = '';
$this->contactLastName = '';
$this->contactResponsibility = '';
$this->contactEmail = '';
$this->contactPhone = '';
$this->resetValidation();
}
}; ?>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<header class="page-header">
<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
<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>
<div class="flex flex-wrap items-center gap-2 flex-shrink-0">
<flux:button icon="arrow-left" variant="filled" href="{{ route('me.press-kits.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
@if ($canManageCompany)
<flux:button icon="pencil" variant="filled" 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>
</header>
{{-- ============== 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">
{{-- ============== STAMMDATEN ============== --}}
<article class="panel" id="stammdaten">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Stammdaten') }}</span>
@if ($canManageCompany)
<flux:button size="sm" variant="filled" icon="pencil" wire:click="startEditCompany">
{{ __('Bearbeiten') }}
</flux:button>
@endif
</div>
@if (session('company-status'))
<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') }}
</div>
@endif
@if ($showCompanyForm)
<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') }}
</div>
<div class="grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:input wire:model="companyName" :label="__('Firmenname')" required />
<flux:error name="companyName" />
</flux:field>
<flux:field>
<flux:input wire:model="companyEmail" :label="__('E-Mail')" type="email" />
<flux:error name="companyEmail" />
</flux:field>
<flux:field>
<flux:input wire:model="companyPhone" :label="__('Telefon')" />
<flux:error name="companyPhone" />
</flux:field>
<flux:field>
<flux:input wire:model="companyWebsite" :label="__('Website')" placeholder="https://..." />
<flux:error name="companyWebsite" />
</flux:field>
<flux:field class="sm:col-span-2">
<flux:textarea wire:model="companyAddress" :label="__('Adresse')" rows="3" />
<flux:error name="companyAddress" />
</flux:field>
<flux:field class="sm:col-span-2">
<flux:textarea wire:model="companyBoilerplate" :label="__('Beschreibung')" rows="5" maxlength="2000"
:description="__('Kurzporträt des Unternehmens — erscheint im Newsroom und unter jeder Meldung als „Über das Unternehmen“. Reiner Text, keine Links; die Website steht im eigenen Feld.')" />
<flux:error name="companyBoilerplate" />
</flux:field>
<flux:field>
<flux:select wire:model="companyCountryCode" :label="__('Land')">
@foreach ($countries as $code => $name)
<option value="{{ $code }}">{{ $name }}</option>
@endforeach
</flux:select>
<flux:error name="companyCountryCode" />
</flux:field>
<flux:field>
<flux:switch wire:model="companyDisableFooterCode" :label="__('Footer-Code deaktivieren')" />
</flux:field>
</div>
<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)
<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-[6px] border border-[color:var(--color-bg-rule)] object-contain bg-white" />
<flux:button type="button" size="sm" variant="filled" wire:click="$set('removeCompanyLogo', true)">
{{ __('Logo entfernen') }}
</flux:button>
</div>
@endif
<flux:field>
<flux:file-upload wire:model="companyLogo"
:label="__('Neues Logo hochladen')"
accept="image/jpeg,image/png,image/webp,image/gif"
:description="__('JPG/PNG/WebP/GIF, max. 4 MB. Varianten werden automatisch generiert.')">
<flux:file-upload.dropzone
:heading="__('Logo hierher ziehen oder klicken')"
:text="__('JPG, PNG, WebP oder GIF · max. 4 MB')"
with-progress
inline
/>
</flux:file-upload>
<flux:error name="companyLogo" />
@if ($companyLogo)
<flux:file-item class="mt-2"
:heading="$companyLogo->getClientOriginalName()"
:image="$companyLogo->temporaryUrl()"
:size="$companyLogo->getSize()"
>
<x-slot name="actions">
<flux:file-item.remove wire:click="$set('companyLogo', null)" :aria-label="__('Logo entfernen')" />
</x-slot>
</flux:file-item>
@endif
</flux:field>
</div>
{{-- Bilder im Newsroom: zurückgestellt, bis die Bild-Kennzeichnung
angepasst ist (Entscheidung 15.09.2026). Sichtbar als BALD,
nicht weggelassen — Leitplanke der Roadmap. --}}
<div class="mt-5 pt-4 border-t border-[color:var(--color-bg-rule)] space-y-2">
<div class="flex items-center gap-2">
<span class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Bilder im Newsroom') }}</span>
<flux:badge size="sm" color="zinc">{{ __('BALD') }}</flux:badge>
</div>
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">
{{ __('Funktion folgt. Bilder im Newsroom unterliegen derselben KI-Kennzeichnung wie Bilder in Meldungen; die wird gerade überarbeitet.') }}
</p>
</div>
<div class="mt-4 pt-4 border-t border-[color:var(--color-bg-rule)] flex justify-end gap-2">
<flux:button type="button" variant="filled" wire:click="cancelCompanyForm">
{{ __('Abbrechen') }}
</flux:button>
<flux:button type="button" variant="primary" wire:click="saveCompany">
{{ __('Speichern') }}
</flux:button>
</div>
</div>
@endif
<dl class="p-5 grid gap-3 sm:grid-cols-2 text-[12.5px]">
<div>
<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>
<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>
<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>
<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">
<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>
<div class="sm:col-span-2">
<dt class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-1">{{ __('Beschreibung') }}</dt>
<dd class="text-[color:var(--color-ink)] whitespace-pre-line">{{ $company->boilerplate ?: '–' }}</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>
@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>
</div>
@endif
@if (session('contact-status'))
<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') }}
</div>
@endif
@if ($showContactForm)
<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') }}
</div>
<div class="grid gap-4 sm:grid-cols-2">
<flux:field>
<flux:input wire:model="contactFirstName" :label="__('Vorname')" />
<flux:error name="contactFirstName" />
</flux:field>
<flux:field>
<flux:input wire:model="contactLastName" :label="__('Nachname')" />
<flux:error name="contactLastName" />
</flux:field>
<flux:field class="sm:col-span-2">
<flux:input wire:model="contactResponsibility" :label="__('Position / Rolle')" />
<flux:error name="contactResponsibility" />
</flux:field>
<flux:field>
<flux:input wire:model="contactEmail" :label="__('E-Mail')" type="email" required />
<flux:error name="contactEmail" />
</flux:field>
<flux:field>
<flux:input wire:model="contactPhone" :label="__('Telefon')" />
<flux:error name="contactPhone" />
</flux:field>
</div>
<div class="mt-4 pt-4 border-t border-[color:var(--color-bg-rule)] flex justify-end gap-2">
<flux:button type="button" variant="filled" wire:click="cancelContactForm">
{{ __('Abbrechen') }}
</flux:button>
<flux:button type="button" variant="primary" wire:click="saveContact">
{{ __('Speichern') }}
</flux:button>
</div>
</div>
@endif
<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 class="min-w-0">
<div class="text-[13px] font-semibold text-[color:var(--color-ink)]">
{{ $contact->displayName() }}
</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-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">{{ $contact->email }}</a>
@endif
@if ($contact->phone)
<span>{{ $contact->phone }}</span>
@endif
@if ($contact->press_releases_count > 0)
<span>{{ trans_choice('in :count Pressemitteilung hinterlegt|in :count Pressemitteilungen hinterlegt', $contact->press_releases_count, ['count' => $contact->press_releases_count]) }}</span>
@endif
</div>
</div>
@if ($canManageContacts)
<div class="flex gap-1 flex-shrink-0">
<flux:button size="sm" variant="filled" icon="pencil" wire:click="editContact({{ $contact->id }})">
{{ __('Bearbeiten') }}
</flux:button>
<flux:button size="sm" variant="filled" icon="trash"
wire:click="deleteContact({{ $contact->id }})"
wire:confirm="{{ __('Diesen Pressekontakt löschen?') }}">
{{ __('Löschen') }}
</flux:button>
</div>
@endif
</div>
</div>
@empty
<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.') }}
</p>
@if ($canManageContacts)
<flux:button class="mt-3" size="sm" variant="primary" icon="plus" wire:click="startCreateContact">
{{ __('Kontakt hinzufügen') }}
</flux:button>
@endif
</div>
@endforelse
</div>
</article>
</div>
{{-- ============== 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="filled" 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 align="end">{{ __('Aufrufe') }}</flux:table.column>
<flux:table.column align="end">{{ __('30 Tage') }}</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>
<a href="{{ route('me.press-releases.show', $pressRelease->id) }}" wire:navigate class="text-[13px] font-semibold text-[color:var(--color-ink)] hover:underline">{{ $pressRelease->title }}</a>
@if ($pressRelease->user_id !== auth()->id())
<span class="block text-[11px] text-[color:var(--color-ink-4)]">{{ __('eingereicht über Partner') }}</span>
@endif
</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 align="end">
<span class="text-[12.5px] tabular-nums font-semibold text-[color:var(--color-ink)]">{{ number_format((int) $pressRelease->hits, 0, ',', '.') }}</span>
</flux:table.cell>
<flux:table.cell align="end">
<span class="text-[12.5px] tabular-nums text-[color:var(--color-ink-2)]">{{ number_format($recentViews[$pressRelease->id] ?? 0, 0, ',', '.') }}</span>
</flux:table.cell>
<flux:table.cell>
<flux:button size="sm" variant="filled" 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="6">
<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>
<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">
{{-- ============== 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="filled" href="{{ route('me.invoices.index') }}" wire:navigate>
{{ __('Rechnungen öffnen') }}
</flux:button>
</div>
</div>
</article>
{{-- ============== STATISTIK (Roadmap 1c.3) ============== --}}
<article class="panel" id="statistik">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Sichtbarkeit') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">{{ __('alle Meldungen dieser Firma') }}</span>
</div>
<div class="p-5 space-y-4">
<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)]">
{{ __('Aufrufe gesamt') }}
</div>
<div class="text-[18px] font-bold tabular-nums text-[color:var(--color-ink)] mt-1">
{{ number_format($summary['views_total'], 0, ',', '.') }}
</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)]">
{{ __('Aufrufe 30 Tage') }}
</div>
<div class="text-[18px] font-bold tabular-nums text-[color:var(--color-ink)] mt-1">
{{ number_format($summary['views_recent'], 0, ',', '.') }}
</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)]">
{{ __('Veröffentlicht') }}
</div>
<div class="text-[18px] font-bold tabular-nums text-[color:var(--color-ink)] mt-1">
{{ number_format($summary['published'], 0, ',', '.') }}
<span class="text-[11.5px] font-medium text-[color:var(--color-ink-3)]">/ {{ number_format($summary['releases'], 0, ',', '.') }}</span>
</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)]">
{{ __('Zuletzt veröffentlicht') }}
</div>
<div class="text-[14px] font-bold text-[color:var(--color-ink)] mt-1.5">
{{ $summary['last_published_at']?->format('d.m.Y') ?? '–' }}
</div>
</div>
</div>
<p class="text-[11.5px] text-[color:var(--color-ink-3)] m-0 leading-relaxed">
{{ __('Aufrufe sind Seitenaufrufe der Meldungen auf dem Portal. Der Verlauf je Meldung steht auf ihrer Detailseite.') }}
</p>
</div>
</article>
</div>
</div>