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([ // Öffentliche Kurzfelder sind keine Werbefläche: keine Links, // keine Adressen, kein Blacklist-Treffer (NoPromotionalText). 'companyName' => ['required', 'string', 'max:255', app(NoPromotionalText::class)], 'companyAddress' => ['nullable', 'string', 'max:1000', app(NoPromotionalText::class)], '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'])); $address = trim((string) $validated['companyAddress']); // Rot-Ebene der bestehenden Prüfung — dieselbe wie für eine Meldung. // Gilt für beide Fließtextfelder, jeweils nur bei Änderung. 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.'), ]); } if ($address !== '' && $address !== trim((string) $company->address) && $profileCheck->isRed($company, $address)) { throw ValidationException::withMessages([ 'companyAddress' => __('Die Adresse 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, ]); $previousLogoPath = $company->logo_path; $previousLogoVariants = $company->logo_variants; $discardPreviousLogo = false; $stored = null; if ($this->removeCompanyLogo) { $company->logo_path = null; $company->logo_variants = null; $discardPreviousLogo = true; } if ($this->companyLogo) { // Erst die neue Datei speichern, dann die DB-Referenz wechseln, das // alte Logo erst danach löschen: Scheitert der Storage, bleiben // bisheriges Logo und Datensatz unangetastet (F23). try { $stored = $imageService->storeCompanyLogo($this->companyLogo, $company->portal?->value ?? 'presseecho', $company->id); } catch (\RuntimeException $exception) { report($exception); throw ValidationException::withMessages([ 'companyLogo' => __('Das Logo konnte nicht gespeichert werden. Bitte versuchen Sie es erneut.'), ]); } $company->logo_path = $stored['path']; $company->logo_variants = $stored['variants']; $discardPreviousLogo = true; } try { $company->save(); } catch (\Throwable $exception) { // Ohne DB-Referenz wäre die neue Datei verwaist. if ($stored !== null) { $imageService->deleteCompanyLogo($stored['path'], $stored['variants']); } throw $exception; } if ($discardPreviousLogo && $previousLogoPath !== null && $previousLogoPath !== $company->logo_path) { $imageService->deleteCompanyLogo($previousLogoPath, $previousLogoVariants); } $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', app(NoPromotionalText::class)], 'contactLastName' => ['nullable', 'string', 'max:80', app(NoPromotionalText::class)], 'contactResponsibility' => ['nullable', 'string', 'max:255', app(NoPromotionalText::class)], '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.')); } // Die Kontakte liegen mit im gecachten Newsroom-Seitenpaket — die // Änderung soll nicht erst nach Ablauf des Seiten-Caches greifen. WebCaches::forgetPublicPages(); $this->resetContactForm(); } public function deleteContact(int $contactId): void { $this->authorize('update', $this->company()); $contact = $this->contact($contactId); $contact->delete(); WebCaches::forgetPublicPages(); 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::withoutGlobalScope(PortalScope::class) ->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::withoutGlobalScope(PortalScope::class) ->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::withoutGlobalScope(PortalScope::class)->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(); } }; ?>
{{-- ============== PAGE HEADER ============== --}} {{-- ============== QUICK-NAV ============== --}} {{-- ============== KPI-Reihe ============== --}}
{{ __('zugeordnet') }} {{ __('Content-Output') }} {{ __('Ansprechpartner') }} {{ __('für PMs verfügbar') }} {{ __('Sichtbarkeit') }} {{ __('für diese Firma') }} {{ __('Berechtigungen') }} {{ $canManageCompany ? __('Voller Schreibzugriff') : __('Lesezugriff') }}
{{-- ============== STAMMDATEN ============== --}}
{{ __('Stammdaten') }} @if ($canManageCompany) {{ __('Bearbeiten') }} @endif
@if (session('company-status'))
{{ session('company-status') }}
@endif @if ($showCompanyForm)
{{ __('Stammdaten bearbeiten') }}
@foreach ($countries as $code => $name) @endforeach
{{ __('Firmenlogo') }}
@php($logoUrl = $company->logoUrl()) @if ($logoUrl && ! $removeCompanyLogo)
{{ $company->name }} {{ __('Logo entfernen') }}
@endif @if ($companyLogo) @endif
{{-- Bilder im Newsroom: zurückgestellt, bis die Bild-Kennzeichnung angepasst ist (Entscheidung 15.09.2026). Sichtbar als BALD, nicht weggelassen — Leitplanke der Roadmap. --}}
{{ __('Bilder im Newsroom') }} {{ __('BALD') }}

{{ __('Funktion folgt. Bilder im Newsroom unterliegen derselben KI-Kennzeichnung wie Bilder in Meldungen; die wird gerade überarbeitet.') }}

{{ __('Abbrechen') }} {{ __('Speichern') }}
@endif
{{ __('E-Mail') }}
{{ $company->email ?: '–' }}
{{ __('Telefon') }}
{{ $company->phone ?: '–' }}
{{ __('Website') }}
@if ($company->website) {{ $company->website }} @else – @endif
{{ __('Land') }}
{{ $company->country_code ?: '–' }}
{{ __('Adresse') }}
{{ $company->address ?: '–' }}
{{ __('Beschreibung') }}
{{ $company->boilerplate ?: '–' }}
{{-- ============== PRESSEKONTAKTE ============== --}}
{{ __('Pressekontakte') }} {{ trans_choice(':count Kontakt|:count Kontakte', $company->contacts_count, ['count' => $company->contacts_count]) }}
@if ($canManageContacts)
{{ __('Kontakt hinzufügen') }}
@endif @if (session('contact-status'))
{{ session('contact-status') }}
@endif @if ($showContactForm)
{{ $editingContactId ? __('Pressekontakt bearbeiten') : __('Neuen Pressekontakt anlegen') }}
{{ __('Abbrechen') }} {{ __('Speichern') }}
@endif
@forelse ($contacts as $contact)
{{ $contact->displayName() }}
{{ $contact->responsibility ?: __('Keine Rolle hinterlegt') }}
@if ($contact->email) {{ $contact->email }} @endif @if ($contact->phone) {{ $contact->phone }} @endif @if ($contact->press_releases_count > 0) {{ trans_choice('in :count Pressemitteilung hinterlegt|in :count Pressemitteilungen hinterlegt', $contact->press_releases_count, ['count' => $contact->press_releases_count]) }} @endif
@if ($canManageContacts)
{{ __('Bearbeiten') }} {{ __('Löschen') }}
@endif
@empty
{{ __('Keine Pressekontakte hinterlegt') }}

{{ __('Pressekontakte helfen, Pressemitteilungen eindeutig einer Ansprechperson zuzuordnen.') }}

@if ($canManageContacts) {{ __('Kontakt hinzufügen') }} @endif
@endforelse
{{-- ============== PRESSEMITTEILUNGEN ============== --}}
{{ __('Pressemitteilungen dieser Firma') }} {{ __('Alle anzeigen') }}
{{ __('Titel') }} {{ __('Status') }} {{ __('Datum') }} {{ __('Aufrufe') }} {{ __('30 Tage') }} {{ __('Aktionen') }} @forelse ($pressReleases as $pressRelease) {{ $pressRelease->title }} @if ($pressRelease->user_id !== auth()->id()) {{ __('eingereicht über Partner') }} @endif @php($statusValue = $pressRelease->status->value) $statusValue === 'published', 'warn' => $statusValue === 'review', 'err' => $statusValue === 'rejected', 'hub' => ! in_array($statusValue, ['published', 'review', 'rejected'], true), ])> {{ $pressRelease->status->label() }} {{ $pressRelease->published_at?->format('d.m.Y') ?? ($pressRelease->created_at?->format('d.m.Y') ?? '–') }} {{ number_format((int) $pressRelease->hits, 0, ',', '.') }} {{ number_format($recentViews[$pressRelease->id] ?? 0, 0, ',', '.') }} {{ __('Öffnen') }} @empty
{{ __('Keine Pressemitteilungen für diese Firma') }}

{{ __('Erstellen Sie die erste Pressemitteilung direkt mit dieser Firma als Kontext.') }}

{{ __('Neue Pressemitteilung') }}
@endforelse
{{-- ============== ABRECHNUNG ============== --}}
{{ __('Abrechnung') }} {{ __('In Vorbereitung') }}

{{ __('Firmenspezifische Zahlungsarten und Add-ons werden hier später zusammengeführt.') }}

{{ __('Noch keine firmenspezifische Abrechnung') }}

{{ __('Rechnungen finden Sie aktuell gesammelt im Finanzbereich. Firmenscharfe Zahlungsarten folgen mit dem Preismodell.') }}

{{ __('Rechnungen öffnen') }}
{{-- ============== STATISTIK (Roadmap 1c.3) ============== --}}
{{ __('Sichtbarkeit') }} {{ __('alle Meldungen dieser Firma') }}
{{ __('Aufrufe gesamt') }}
{{ number_format($summary['views_total'], 0, ',', '.') }}
{{ __('Aufrufe 30 Tage') }}
{{ number_format($summary['views_recent'], 0, ',', '.') }}
{{ __('Veröffentlicht') }}
{{ number_format($summary['published'], 0, ',', '.') }} / {{ number_format($summary['releases'], 0, ',', '.') }}
{{ __('Zuletzt veröffentlicht') }}
{{ $summary['last_published_at']?->format('d.m.Y') ?? '–' }}

{{ __('Aufrufe sind Seitenaufrufe der Meldungen auf dem Portal. Der Verlauf je Meldung steht auf ihrer Detailseite.') }}