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 ============== --}}
{{ __('Stammdaten') }}
{{ __('Pressekontakte') }}
{{ __('Pressemitteilungen') }}
{{ __('Abrechnung') }}
{{ __('Statistik') }}
{{-- ============== 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)
{{ $name }}
@endforeach
{{ __('Firmenlogo') }}
@php($logoUrl = $company->logoUrl())
@if ($logoUrl && ! $removeCompanyLogo)
{{ __('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 ?: '–' }}
{{ __('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.') }}