admin: PM-Detailseite verlinkt Firma, Autor und Firmen-Benutzer

Firma und Autor sind im Header und Status-Grid klickbar
(admin/companies/{id} bzw. admin/users/{id}). Das Kontakte-Panel heißt
jetzt "Firma & Pressekontakte" und zeigt die Firmendaten (Adresse,
E-Mail, Telefon, Website) über den Pressekontakten. Neu: "Benutzer mit
Zugriff (über Firma)" listet alle Firmen-Mitglieder mit Rolle und
Autor-Markierung — mehrere User hängen nur über die Firma an einer PM,
Magic-Links sind reine Login-Tokens.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-21 08:48:22 +00:00
parent fa303360ee
commit e3ce626b73
2 changed files with 127 additions and 10 deletions

View file

@ -73,7 +73,8 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
{
$pr = PressRelease::withoutGlobalScopes()
->with([
'company:id,name,email,phone,slug',
'company:id,name,email,phone,website,address,country_code,slug',
'company.users:users.id,users.name,users.email',
'category.translations',
'user:id,name,email',
'images',
@ -161,13 +162,27 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
@endif
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[720px] text-[color:var(--color-ink-2)]">
<strong class="font-semibold text-[color:var(--color-ink)]">{{ __('Firma') }}:</strong>
{{ $pr->company?->name ?? '–' }}
@if ($pr->company)
<a href="{{ route('admin.companies.show', $pr->company->id) }}" wire:navigate
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
{{ $pr->company->name }}
</a>
@else
–
@endif
<span class="text-[color:var(--color-bg-rule)] mx-1">·</span>
<strong class="font-semibold text-[color:var(--color-ink)]">{{ __('Kategorie') }}:</strong>
{{ $categoryName }}
<span class="text-[color:var(--color-bg-rule)] mx-1">·</span>
<strong class="font-semibold text-[color:var(--color-ink)]">{{ __('Autor') }}:</strong>
{{ $pr->user?->name ?? '–' }}
@if ($pr->user)
<a href="{{ route('admin.users.show', $pr->user->id) }}" wire:navigate
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
{{ $pr->user->name }}
</a>
@else
–
@endif
</p>
</div>
@ -321,17 +336,52 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
<div class="grid gap-6 xl:grid-cols-2">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Zugeordnete Pressekontakte') }}</span>
<span class="section-eyebrow">{{ __('Firma & Pressekontakte') }}</span>
@if ($pr->company)
<flux:button size="sm" variant="filled" icon="building-office" href="{{ route('admin.companies.show', $pr->company->id) }}" wire:navigate>
{{ __('Firma') }}
{{ __('Firma öffnen') }}
</flux:button>
@endif
</div>
<div class="p-5">
<p class="text-[12px] text-[color:var(--color-ink-3)] mt-0 mb-4">
{{ __('Kontakte, die dieser Pressemitteilung zugeordnet sind.') }}
</p>
@if ($pr->company)
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[13px] font-semibold">
<a href="{{ route('admin.companies.show', $pr->company->id) }}" wire:navigate
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
{{ $pr->company->name }}
</a>
</div>
@if ($pr->company->address || $pr->company->country_code)
<div class="text-[12px] text-[color:var(--color-ink-3)] whitespace-pre-line">{{ trim($pr->company->address.' '.$pr->company->country_code) }}</div>
@endif
<div class="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-[11.5px] text-[color:var(--color-ink-3)]">
@if ($pr->company->email)
<a href="mailto:{{ $pr->company->email }}"
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
{{ $pr->company->email }}
</a>
@endif
@if ($pr->company->phone)
<span>{{ $pr->company->phone }}</span>
@endif
@if ($pr->company->website)
<a href="{{ $pr->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)]">
{{ $pr->company->website }}
</a>
@endif
</div>
</div>
@else
<div class="rounded-[5px] border border-dashed border-[color:var(--color-bg-rule)] p-4 text-[12.5px] text-[color:var(--color-ink-3)]">
{{ __('Dieser Pressemitteilung ist keine Firma zugeordnet.') }}
</div>
@endif
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mt-4 mb-2">
{{ __('Zugeordnete Pressekontakte') }}
</div>
<div class="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">
@ -372,7 +422,14 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Autor') }}</div>
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1 truncate">
{{ $pr->user?->name ?? '–' }}
@if ($pr->user)
<a href="{{ route('admin.users.show', $pr->user->id) }}" wire:navigate
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
{{ $pr->user->name }}
</a>
@else
–
@endif
</div>
</div>
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
@ -411,6 +468,32 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
@endif
</div>
@php
// Neben dem Autor können weitere User (Firmen-Mitglieder, z. B.
// per Magic-Link eingeladen) auf die PM zugreifen.
$companyUsers = $pr->company?->users ?? collect();
@endphp
@if ($companyUsers->isNotEmpty())
<div class="mt-3">
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">
{{ __('Benutzer mit Zugriff (über Firma)') }}
</div>
<div class="mt-1.5 flex flex-wrap gap-x-3 gap-y-1 text-[12.5px]">
@foreach ($companyUsers as $companyUser)
<span class="inline-flex items-center gap-1.5">
<a href="{{ route('admin.users.show', $companyUser->id) }}" wire:navigate
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
{{ $companyUser->name }}
</a>
<span class="text-[11px] text-[color:var(--color-ink-3)]">
{{ $companyUser->pivot->role === 'owner' ? __('Owner') : __('Member') }}@if ($companyUser->id === $pr->user_id) · {{ __('Autor') }}@endif
</span>
</span>
@endforeach
</div>
</div>
@endif
@if ($pr->no_export)
<div class="mt-3 flex items-center gap-2 text-[12px] text-[color:var(--color-ink-3)]">
<flux:icon.no-symbol variant="micro" class="size-3.5" />

View file

@ -51,7 +51,12 @@ test('admin show zeigt zugeordnete Pressekontakte', function () {
$admin = makeAdminForShow();
$this->actingAs($admin);
$company = Company::factory()->presseecho()->create();
$company = Company::factory()->presseecho()->create([
'name' => 'Muster GmbH',
'email' => 'firma@example.test',
'phone' => '+49 30 1234567',
'website' => 'https://muster.example.test',
]);
$contact = Contact::factory()->for($company)->create([
'first_name' => 'Max',
'last_name' => 'Mustermann',
@ -64,6 +69,11 @@ test('admin show zeigt zugeordnete Pressekontakte', function () {
$pr->contacts()->sync([$contact->id]);
LivewireVolt::test('admin.press-releases.show', ['id' => $pr->id])
->assertSee('Firma & Pressekontakte')
->assertSee('Muster GmbH')
->assertSee('firma@example.test')
->assertSee('+49 30 1234567')
->assertSee('https://muster.example.test')
->assertSee('Zugeordnete Pressekontakte')
->assertSee('Max Mustermann')
->assertSee('Pressesprecher')
@ -112,6 +122,30 @@ test('admin show zeigt Embargo-Info im Published-Workflow', function () {
->assertSee('Sperrfrist bis');
});
test('admin show verlinkt Firma, Autor und Firmen-Benutzer', function () {
/** @var TestCase $this */
$admin = makeAdminForShow();
$this->actingAs($admin);
$company = Company::factory()->presseecho()->create(['name' => 'Muster GmbH']);
$author = User::factory()->create(['name' => 'Anna Autorin']);
$coEditor = User::factory()->create(['name' => 'Carl Co-Editor']);
$company->users()->attach($author->id, ['role' => 'owner']);
$company->users()->attach($coEditor->id, ['role' => 'member']);
$pr = PressRelease::factory()->create([
'company_id' => $company->id,
'user_id' => $author->id,
]);
LivewireVolt::test('admin.press-releases.show', ['id' => $pr->id])
->assertSee('Benutzer mit Zugriff')
->assertSee('Carl Co-Editor')
->assertSeeHtml(route('admin.companies.show', $company->id))
->assertSeeHtml(route('admin.users.show', $author->id))
->assertSeeHtml(route('admin.users.show', $coEditor->id));
});
test('admin show zeigt Autor im Status-Verlauf-Grid', function () {
/** @var TestCase $this */
$admin = makeAdminForShow();