12-05-2026 Frontend dev
This commit is contained in:
parent
405df0a122
commit
5b8bdf4182
779 changed files with 480564 additions and 6241 deletions
323
resources/views/livewire/customer/press-releases/show.blade.php
Normal file
323
resources/views/livewire/customer/press-releases/show.blade.php
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\PressReleaseStatus;
|
||||
use App\Models\PressRelease;
|
||||
use App\Services\Auth\MagicLinkGenerator;
|
||||
use App\Services\PressRelease\BlacklistViolationException;
|
||||
use App\Services\PressRelease\PressReleaseService;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends Component
|
||||
{
|
||||
#[Locked]
|
||||
public int $id;
|
||||
|
||||
public ?string $shareUrl = null;
|
||||
|
||||
public ?string $shareExpiresAt = null;
|
||||
|
||||
public function mount(int $id): void
|
||||
{
|
||||
$this->id = $id;
|
||||
$pr = $this->getMyPR();
|
||||
$this->authorize('view', $pr);
|
||||
}
|
||||
|
||||
public function submitForReview(): void
|
||||
{
|
||||
$pr = $this->getMyPR();
|
||||
$this->authorize('submitForReview', $pr);
|
||||
|
||||
try {
|
||||
app(PressReleaseService::class)->submitForReview($pr);
|
||||
} catch (BlacklistViolationException $e) {
|
||||
session()->flash('error', __('Pressemitteilung wurde automatisch abgelehnt: unzulässiges Wort ":word".', ['word' => $e->word]));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
session()->flash('success', __('Pressemitteilung zur Prüfung eingereicht.'));
|
||||
}
|
||||
|
||||
public function generateShareLink(MagicLinkGenerator $generator): void
|
||||
{
|
||||
$pr = $this->getMyPR();
|
||||
$this->authorize('view', $pr);
|
||||
|
||||
$share = $generator->createPressReleaseShareLink($pr, auth()->user());
|
||||
|
||||
$this->shareUrl = $share['url'];
|
||||
$this->shareExpiresAt = $share['expires_at']->format('d.m.Y H:i');
|
||||
|
||||
session()->flash('success', __('Vorschau-Link wurde erzeugt.'));
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
$pr = $this->getMyPR();
|
||||
$this->authorize('view', $pr);
|
||||
|
||||
$categoryName = $pr->category?->translations->firstWhere('locale', 'de')?->name ?? '–';
|
||||
|
||||
$latestRejection = null;
|
||||
if ($pr->status->value === 'rejected') {
|
||||
$latestRejection = $pr->statusLogs
|
||||
->firstWhere(fn ($log) => $log->to_status?->value === 'rejected');
|
||||
}
|
||||
|
||||
return [
|
||||
'pr' => $pr,
|
||||
'categoryName' => $categoryName,
|
||||
'canEdit' => auth()->user()->can('update', $pr)
|
||||
&& in_array($pr->status->value, ['draft', 'rejected']),
|
||||
'latestRejection' => $latestRejection,
|
||||
'contacts' => $pr->contacts,
|
||||
'statusLogs' => $pr->statusLogs,
|
||||
'statusColor' => match($pr->status->value) {
|
||||
'published' => 'green',
|
||||
'review' => 'yellow',
|
||||
'rejected' => 'red',
|
||||
'archived' => 'blue',
|
||||
default => 'zinc',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private function getMyPR(): PressRelease
|
||||
{
|
||||
return PressRelease::withoutGlobalScopes()
|
||||
->where('user_id', auth()->id())
|
||||
->with([
|
||||
'company:id,name,email,phone',
|
||||
'category.translations',
|
||||
'contacts' => fn ($query) => $query
|
||||
->withoutGlobalScopes()
|
||||
->orderBy('last_name')
|
||||
->orderBy('first_name')
|
||||
->select(['contacts.id', 'contacts.company_id', 'contacts.first_name', 'contacts.last_name', 'contacts.responsibility', 'contacts.email', 'contacts.phone']),
|
||||
'statusLogs.changedBy:id,name,email',
|
||||
])
|
||||
->findOrFail($this->id);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="space-y-6">
|
||||
@if(session('success'))
|
||||
<div class="rounded-md border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800 dark:border-green-800 dark:bg-green-900/20 dark:text-green-300">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<flux:card>
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:badge :color="$statusColor">{{ $pr->status->label() }}</flux:badge>
|
||||
<flux:badge color="zinc" size="sm">{{ strtoupper($pr->language) }}</flux:badge>
|
||||
</div>
|
||||
<flux:heading size="xl" class="mt-2">{{ $pr->title }}</flux:heading>
|
||||
<flux:text class="mt-1 text-sm text-zinc-500">
|
||||
{{ $pr->company?->name ?? '–' }} · {{ $categoryName }} · {{ $pr->created_at->format('d.m.Y') }}
|
||||
</flux:text>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@if($canEdit)
|
||||
<flux:button variant="ghost" icon="pencil" href="{{ route('me.press-releases.edit', $pr->id) }}" wire:navigate>
|
||||
{{ __('Bearbeiten') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
<flux:button variant="ghost" icon="link" wire:click="generateShareLink">
|
||||
{{ __('Vorschau-Link') }}
|
||||
</flux:button>
|
||||
<flux:button variant="ghost" icon="arrow-left" href="{{ route('me.press-releases.index') }}" wire:navigate>
|
||||
{{ __('Zurück') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($shareUrl)
|
||||
<div class="mt-4 rounded-md border border-emerald-300 bg-emerald-50 p-4 dark:border-emerald-700 dark:bg-emerald-900/20">
|
||||
<flux:heading size="sm" class="mb-2">{{ __('Öffentlicher Vorschau-Link erstellt') }}</flux:heading>
|
||||
<flux:text class="mb-2 text-xs text-zinc-500">{{ __('Gültig bis :date.', ['date' => $shareExpiresAt]) }}</flux:text>
|
||||
<flux:input readonly :value="$shareUrl" />
|
||||
</div>
|
||||
@endif
|
||||
</flux:card>
|
||||
|
||||
@if($pr->status === PressReleaseStatus::Rejected && $latestRejection)
|
||||
<flux:callout color="red" icon="exclamation-triangle">
|
||||
<flux:callout.heading>{{ __('Diese Pressemitteilung wurde abgelehnt') }}</flux:callout.heading>
|
||||
<flux:callout.text>
|
||||
@if($latestRejection->reason)
|
||||
<strong>{{ __('Begründung') }}:</strong>
|
||||
<span class="block mt-1 whitespace-pre-line">{{ $latestRejection->reason }}</span>
|
||||
@else
|
||||
{{ __('Bitte überarbeiten Sie den Inhalt und reichen Sie die Pressemitteilung erneut ein.') }}
|
||||
@endif
|
||||
<span class="mt-2 block text-xs text-red-700/70 dark:text-red-300/70">
|
||||
{{ __('Abgelehnt am') }} {{ $latestRejection->created_at->format('d.m.Y H:i') }}
|
||||
</span>
|
||||
</flux:callout.text>
|
||||
</flux:callout>
|
||||
@endif
|
||||
|
||||
@if($pr->status === PressReleaseStatus::Draft || $pr->status === PressReleaseStatus::Rejected)
|
||||
<flux:card>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<flux:text class="text-sm text-zinc-500">
|
||||
{{ $pr->status === PressReleaseStatus::Rejected
|
||||
? __('Sie können den Text bearbeiten und erneut zur Prüfung einreichen.')
|
||||
: __('Reichen Sie den Entwurf ein, sobald er vollständig ist.') }}
|
||||
</flux:text>
|
||||
<div class="flex items-center gap-2">
|
||||
@if($canEdit)
|
||||
<flux:button variant="ghost" icon="pencil" href="{{ route('me.press-releases.edit', $pr->id) }}" wire:navigate>
|
||||
{{ __('Bearbeiten') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
<flux:button type="button" variant="primary" wire:click="submitForReview"
|
||||
wire:confirm="{{ __('Pressemitteilung zur Prüfung einreichen?') }}">
|
||||
{{ $pr->status === PressReleaseStatus::Rejected ? __('Erneut einreichen') : __('Zur Prüfung einreichen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
@endif
|
||||
|
||||
@if($pr->status === PressReleaseStatus::Review)
|
||||
<flux:callout color="yellow" icon="clock">
|
||||
{{ __('Ihre Pressemitteilung wird gerade geprüft. Sie werden benachrichtigt, sobald eine Entscheidung vorliegt.') }}
|
||||
</flux:callout>
|
||||
@endif
|
||||
|
||||
<div class="grid gap-6 xl:grid-cols-2">
|
||||
<flux:card>
|
||||
<div class="mb-4 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Zugeordnete Pressekontakte') }}</flux:heading>
|
||||
<flux:text class="text-sm text-zinc-500">
|
||||
{{ __('Kontakte, die dieser Pressemitteilung zugeordnet sind.') }}
|
||||
</flux:text>
|
||||
</div>
|
||||
|
||||
@if($pr->company)
|
||||
<flux:button size="sm" variant="ghost" icon="building-office" href="{{ route('me.press-kits.show', $pr->company->id) }}" wire:navigate>
|
||||
{{ __('Firma') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
@forelse($contacts as $contact)
|
||||
<div class="rounded-lg bg-zinc-50 p-3 dark:bg-zinc-900">
|
||||
<flux:text weight="semibold">
|
||||
{{ trim(($contact->first_name ?? '').' '.($contact->last_name ?? '')) ?: __('Kontakt ohne Name') }}
|
||||
</flux:text>
|
||||
<flux:text class="text-sm text-zinc-500">{{ $contact->responsibility ?: __('Keine Rolle hinterlegt') }}</flux:text>
|
||||
<div class="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs text-zinc-500">
|
||||
@if($contact->email)
|
||||
<a href="mailto:{{ $contact->email }}" class="text-blue-600 hover:underline dark:text-blue-400">{{ $contact->email }}</a>
|
||||
@endif
|
||||
@if($contact->phone)
|
||||
<span>{{ $contact->phone }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="rounded-lg border border-dashed border-zinc-200 p-4 text-sm text-zinc-500 dark:border-zinc-700">
|
||||
{{ __('Dieser Pressemitteilung ist noch kein Pressekontakt zugeordnet.') }}
|
||||
@if($pr->company)
|
||||
<a href="{{ route('me.press-kits.show', $pr->company->id) }}" wire:navigate class="font-medium text-blue-600 hover:underline dark:text-blue-400">
|
||||
{{ __('Kontakte in der Firma prüfen.') }}
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
<flux:card>
|
||||
<flux:heading size="lg" class="mb-4">{{ __('Status & Verlauf') }}</flux:heading>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<div class="rounded-lg bg-zinc-50 p-3 dark:bg-zinc-900">
|
||||
<flux:text class="text-xs text-zinc-500">{{ __('Aktueller Status') }}</flux:text>
|
||||
<flux:badge class="mt-1" :color="$statusColor">{{ $pr->status->label() }}</flux:badge>
|
||||
</div>
|
||||
<div class="rounded-lg bg-zinc-50 p-3 dark:bg-zinc-900">
|
||||
<flux:text class="text-xs text-zinc-500">{{ __('Erstellt') }}</flux:text>
|
||||
<flux:text weight="semibold">{{ $pr->created_at?->format('d.m.Y H:i') ?? '–' }}</flux:text>
|
||||
</div>
|
||||
<div class="rounded-lg bg-zinc-50 p-3 dark:bg-zinc-900">
|
||||
<flux:text class="text-xs text-zinc-500">{{ __('Veröffentlicht') }}</flux:text>
|
||||
<flux:text weight="semibold">{{ $pr->published_at?->format('d.m.Y H:i') ?? '–' }}</flux:text>
|
||||
</div>
|
||||
<div class="rounded-lg bg-zinc-50 p-3 dark:bg-zinc-900">
|
||||
<flux:text class="text-xs text-zinc-500">{{ __('Aufrufe') }}</flux:text>
|
||||
<flux:text weight="semibold">{{ number_format($pr->hits, 0, ',', '.') }}</flux:text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<flux:separator class="my-4" />
|
||||
|
||||
@if($statusLogs->isNotEmpty())
|
||||
<ol class="space-y-3 border-s border-zinc-200 ps-4 dark:border-zinc-700">
|
||||
@foreach($statusLogs as $log)
|
||||
<li class="text-sm">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@php
|
||||
$color = match($log->to_status?->value) {
|
||||
'published' => 'green',
|
||||
'review' => 'yellow',
|
||||
'rejected' => 'red',
|
||||
'archived' => 'blue',
|
||||
default => 'zinc',
|
||||
};
|
||||
@endphp
|
||||
<flux:badge size="sm" :color="$color">{{ $log->to_status?->label() }}</flux:badge>
|
||||
<span class="text-xs text-zinc-500">
|
||||
{{ $log->created_at->format('d.m.Y H:i') }}
|
||||
</span>
|
||||
@if($log->changedBy)
|
||||
<span class="text-xs text-zinc-500">
|
||||
{{ __('durch :name', ['name' => $log->changedBy->name]) }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
@if($log->reason)
|
||||
<p class="mt-1 text-zinc-600 dark:text-zinc-400">{{ $log->reason }}</p>
|
||||
@endif
|
||||
</li>
|
||||
@endforeach
|
||||
</ol>
|
||||
@else
|
||||
<flux:text class="text-sm text-zinc-500">
|
||||
{{ __('Noch keine Statusänderungen protokolliert.') }}
|
||||
</flux:text>
|
||||
@endif
|
||||
</flux:card>
|
||||
</div>
|
||||
|
||||
<flux:card>
|
||||
<div class="prose prose-zinc dark:prose-invert max-w-none">
|
||||
{!! nl2br(e($pr->text)) !!}
|
||||
</div>
|
||||
|
||||
@if($pr->keywords || $pr->backlink_url)
|
||||
<div class="mt-6 space-y-2 border-t border-zinc-200 pt-4 text-sm text-zinc-500 dark:border-zinc-700">
|
||||
@if($pr->keywords)
|
||||
<p><strong>{{ __('Stichwörter') }}:</strong> {{ $pr->keywords }}</p>
|
||||
@endif
|
||||
@if($pr->backlink_url)
|
||||
<p><strong>{{ __('Backlink') }}:</strong>
|
||||
<a href="{{ $pr->backlink_url }}" target="_blank" class="text-blue-600 underline">{{ $pr->backlink_url }}</a>
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</flux:card>
|
||||
|
||||
</div>
|
||||
Loading…
Add table
Add a link
Reference in a new issue