newsroom: Beschreibung pflegen, durch die Rot-Ebene geprüft (Roadmap 1b.3)

Die Firmenseite im Kundenbereich bekommt den Beschreibungstext (max.
2 000 Zeichen, reiner Text). Er läuft durch dieselbe Klassifikation wie
eine Meldung (CompanyProfileCheck, KI mit deterministischem Fallback);
Rot wird abgewiesen. Links im Profiltext gibt es nicht — die Website ist
das eine Link-Feld. Bilder zurückgestellt (BALD-Platzhalter), bis die
Bild-Kennzeichnung angepasst ist.

Stufe 3 der Konversionsleiter (newsroom_edited) wird hier geschrieben,
einmal je Firma, mit der Herkunft des zuletzt eingelösten Magic Links.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-15 13:04:12 +00:00
parent 079477a423
commit 87f95fb7a7
6 changed files with 253 additions and 2 deletions

View file

@ -3,8 +3,11 @@
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\Support\WebCaches;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Layout;
@ -33,6 +36,8 @@ new #[Layout('components.layouts.app'), Title('Firma')] class extends Component
public string $companyCountryCode = 'DE';
public string $companyBoilerplate = '';
public bool $companyDisableFooterCode = false;
public $companyLogo = null;
@ -76,6 +81,7 @@ new #[Layout('components.layouts.app'), Title('Firma')] class extends Component
$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;
@ -87,7 +93,7 @@ new #[Layout('components.layouts.app'), Title('Firma')] class extends Component
$this->resetCompanyForm();
}
public function saveCompany(ImageService $imageService): void
public function saveCompany(ImageService $imageService, CompanyProfileCheck $profileCheck, ConversionRecorder $conversions): void
{
$company = $this->company();
$this->authorize('update', $company);
@ -99,9 +105,22 @@ new #[Layout('components.layouts.app'), Title('Firma')] class extends Component
'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,
@ -109,6 +128,7 @@ new #[Layout('components.layouts.app'), Title('Firma')] class extends Component
'phone' => $validated['companyPhone'] ?: null,
'website' => $validated['companyWebsite'] ?: null,
'country_code' => $validated['companyCountryCode'] ?: null,
'boilerplate' => $boilerplate !== '' ? $boilerplate : null,
'disable_footer_code' => $this->companyDisableFooterCode,
]);
@ -130,6 +150,13 @@ new #[Layout('components.layouts.app'), Title('Firma')] class extends Component
$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.'));
}
@ -269,6 +296,7 @@ new #[Layout('components.layouts.app'), Title('Firma')] class extends Component
$this->companyPhone = '';
$this->companyWebsite = '';
$this->companyCountryCode = 'DE';
$this->companyBoilerplate = '';
$this->companyDisableFooterCode = false;
$this->companyLogo = null;
$this->removeCompanyLogo = false;
@ -416,6 +444,11 @@ new #[Layout('components.layouts.app'), Title('Firma')] class extends Component
<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)
@ -471,6 +504,19 @@ new #[Layout('components.layouts.app'), Title('Firma')] class extends Component
</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') }}
@ -510,6 +556,10 @@ new #[Layout('components.layouts.app'), Title('Firma')] class extends Component
<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>