893 lines
40 KiB
PHP
893 lines
40 KiB
PHP
<?php
|
||
|
||
use App\Models\Company;
|
||
use App\Models\User;
|
||
use App\Services\Customer\CustomerCompanyContext;
|
||
use Illuminate\Database\Eloquent\Builder;
|
||
use Illuminate\Support\Carbon;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Livewire\Attributes\Layout;
|
||
use Livewire\Attributes\Title;
|
||
use Livewire\Attributes\Url;
|
||
use Livewire\Volt\Component;
|
||
use Livewire\WithPagination;
|
||
|
||
new #[Layout('components.layouts.app'), Title('Meine Firmen')] class extends Component
|
||
{
|
||
use WithPagination;
|
||
|
||
public string $search = '';
|
||
|
||
#[Url(as: 'view', except: 'all')]
|
||
public string $savedView = 'all';
|
||
|
||
#[Url(as: 'portal', except: '')]
|
||
public string $portalFilter = '';
|
||
|
||
#[Url(as: 'role', except: 'all')]
|
||
public string $roleFilter = 'all';
|
||
|
||
#[Url(as: 'mode', except: 'cards')]
|
||
public string $viewMode = 'cards';
|
||
|
||
public function updatedSearch(): void
|
||
{
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function setSavedView(string $view): void
|
||
{
|
||
$allowed = ['all', 'active', 'drafts', 'inactive', 'shared'];
|
||
$this->savedView = in_array($view, $allowed, true) ? $view : 'all';
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function setPortalFilter(string $portal): void
|
||
{
|
||
$allowed = ['', 'presseecho', 'businessportal24'];
|
||
$this->portalFilter = in_array($portal, $allowed, true) ? $portal : '';
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function setRoleFilter(string $role): void
|
||
{
|
||
$allowed = ['all', 'owner', 'responsible', 'member'];
|
||
$this->roleFilter = in_array($role, $allowed, true) ? $role : 'all';
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function setViewMode(string $mode): void
|
||
{
|
||
$this->viewMode = $mode === 'list' ? 'list' : 'cards';
|
||
}
|
||
|
||
public function resetFilters(): void
|
||
{
|
||
$this->search = '';
|
||
$this->savedView = 'all';
|
||
$this->portalFilter = '';
|
||
$this->roleFilter = 'all';
|
||
$this->resetPage();
|
||
}
|
||
|
||
/**
|
||
* @return Builder<Company>
|
||
*/
|
||
private function baseQuery(User $user): Builder
|
||
{
|
||
return app(CustomerCompanyContext::class)
|
||
->accessibleCompanyQuery($user);
|
||
}
|
||
|
||
/**
|
||
* Wendet die "Saved View"-Logik auf eine Query an.
|
||
*
|
||
* @param Builder<Company> $query
|
||
*/
|
||
private function applySavedView(Builder $query, User $user, string $view): void
|
||
{
|
||
match ($view) {
|
||
'active' => $query->where('is_active', true),
|
||
'inactive' => $query->where('is_active', false),
|
||
'drafts' => $query->whereRaw('1 = 0'),
|
||
'shared' => $query->where('owner_user_id', '!=', $user->id),
|
||
default => null,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* @param Builder<Company> $query
|
||
*/
|
||
private function applySharedFilters(Builder $query): void
|
||
{
|
||
if (filled($this->portalFilter)) {
|
||
$query->where(function ($query) {
|
||
$query->where('portal', $this->portalFilter)
|
||
->orWhere('portal', 'both');
|
||
});
|
||
}
|
||
|
||
if (filled($this->search)) {
|
||
$search = trim($this->search);
|
||
$query->where(function ($query) use ($search): void {
|
||
$query->where('name', 'like', '%'.$search.'%')
|
||
->orWhere('email', 'like', '%'.$search.'%')
|
||
->orWhere('address', 'like', '%'.$search.'%')
|
||
->orWhere('slug', 'like', '%'.$search.'%');
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param Builder<Company> $query
|
||
*/
|
||
private function applyRoleFilter(Builder $query, User $user, string $role): void
|
||
{
|
||
if ($role === 'all') {
|
||
return;
|
||
}
|
||
|
||
if ($role === 'owner') {
|
||
$query->where('owner_user_id', $user->id);
|
||
|
||
return;
|
||
}
|
||
|
||
$query->where('owner_user_id', '!=', $user->id)
|
||
->whereHas('users', function ($query) use ($user, $role): void {
|
||
$query->where('users.id', $user->id)
|
||
->where('company_user.role', $role);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Sammelt alle Counter-Werte in genau drei Queries:
|
||
* 1) aggregiertes COUNT/SUM CASE auf companies
|
||
* 2) COUNT auf press_releases
|
||
* 3) COUNT auf contacts
|
||
*
|
||
* @return array{
|
||
* counters: array{companies: int, active: int, press_releases: int, contacts: int},
|
||
* saved_views: array{all: int, active: int, drafts: int, inactive: int, shared: int},
|
||
* }
|
||
*/
|
||
private function buildAggregateCounts(User $user): array
|
||
{
|
||
$row = $this->baseQuery($user)
|
||
->selectRaw(
|
||
'COUNT(*) as total_companies,
|
||
SUM(CASE WHEN is_active = 1 THEN 1 ELSE 0 END) as active_companies,
|
||
SUM(CASE WHEN is_active = 0 THEN 1 ELSE 0 END) as inactive_companies,
|
||
SUM(CASE WHEN owner_user_id <> ? THEN 1 ELSE 0 END) as shared_companies',
|
||
[$user->id]
|
||
)
|
||
->first();
|
||
|
||
$totalCompanies = (int) ($row->total_companies ?? 0);
|
||
$activeCompanies = (int) ($row->active_companies ?? 0);
|
||
$inactiveCompanies = (int) ($row->inactive_companies ?? 0);
|
||
$sharedCompanies = (int) ($row->shared_companies ?? 0);
|
||
|
||
if ($totalCompanies === 0) {
|
||
return [
|
||
'counters' => [
|
||
'companies' => 0,
|
||
'active' => 0,
|
||
'press_releases' => 0,
|
||
'contacts' => 0,
|
||
],
|
||
'saved_views' => [
|
||
'all' => 0,
|
||
'active' => 0,
|
||
'drafts' => 0,
|
||
'inactive' => 0,
|
||
'shared' => 0,
|
||
],
|
||
];
|
||
}
|
||
|
||
$companyIdsQuery = $this->baseQuery($user)->select('companies.id');
|
||
|
||
$pressReleaseCount = (int) \App\Models\PressRelease::query()
|
||
->withoutGlobalScopes()
|
||
->whereIn('company_id', $companyIdsQuery)
|
||
->count();
|
||
|
||
$contactsCount = (int) \App\Models\Contact::query()
|
||
->withoutGlobalScopes()
|
||
->whereIn('company_id', $companyIdsQuery)
|
||
->count();
|
||
|
||
return [
|
||
'counters' => [
|
||
'companies' => $totalCompanies,
|
||
'active' => $activeCompanies,
|
||
'press_releases' => $pressReleaseCount,
|
||
'contacts' => $contactsCount,
|
||
],
|
||
'saved_views' => [
|
||
'all' => $totalCompanies,
|
||
'active' => $activeCompanies,
|
||
'drafts' => 0,
|
||
'inactive' => $inactiveCompanies,
|
||
'shared' => $sharedCompanies,
|
||
],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Bestimmt deterministisch einen Logo-Token (lg-*) anhand der Company-Id.
|
||
*/
|
||
public function logoVariant(Company $company): string
|
||
{
|
||
$variants = ['lg-brew', 'lg-mv', 'lg-soft', 'lg-warm'];
|
||
|
||
if (blank($company->name)) {
|
||
return 'lg-blank';
|
||
}
|
||
|
||
return $variants[$company->id % count($variants)];
|
||
}
|
||
|
||
/**
|
||
* Initialen aus dem Firmennamen (max. 2 Zeichen, Großbuchstaben).
|
||
*/
|
||
public function logoInitials(Company $company): string
|
||
{
|
||
$name = trim((string) $company->name);
|
||
if (blank($name)) {
|
||
return '–';
|
||
}
|
||
|
||
$words = preg_split('/\s+/u', $name) ?: [];
|
||
$letters = '';
|
||
foreach ($words as $word) {
|
||
$first = mb_substr($word, 0, 1);
|
||
if ($first !== '') {
|
||
$letters .= $first;
|
||
}
|
||
if (mb_strlen($letters) >= 2) {
|
||
break;
|
||
}
|
||
}
|
||
|
||
if ($letters === '') {
|
||
$letters = mb_substr($name, 0, 2);
|
||
}
|
||
|
||
return mb_strtoupper($letters);
|
||
}
|
||
|
||
/**
|
||
* Liefert eine kompakte Meta-Line: Stadt · Typ.
|
||
*/
|
||
public function metaLine(Company $company): string
|
||
{
|
||
$parts = [];
|
||
|
||
$address = trim((string) ($company->address ?? ''));
|
||
if (filled($address)) {
|
||
$lastLine = collect(preg_split('/\r?\n/', $address))
|
||
->map(fn ($line) => trim((string) $line))
|
||
->filter()
|
||
->last();
|
||
if (is_string($lastLine) && filled($lastLine)) {
|
||
$parts[] = $lastLine;
|
||
}
|
||
}
|
||
|
||
$type = $company->type?->label();
|
||
if (is_string($type) && filled($type)) {
|
||
$parts[] = $type;
|
||
}
|
||
|
||
return implode(' · ', $parts);
|
||
}
|
||
|
||
/**
|
||
* Rolle des aktuellen Users für die Karte (admin|member).
|
||
*/
|
||
public function userRoleKey(Company $company, User $user): string
|
||
{
|
||
if ($company->owner_user_id === $user->id) {
|
||
return 'owner';
|
||
}
|
||
|
||
return (string) ($company->getAttribute('current_user_role') ?? $company->pivot?->role ?? 'member');
|
||
}
|
||
|
||
public function isAdminRole(string $roleKey): bool
|
||
{
|
||
return in_array($roleKey, ['owner', 'responsible'], true);
|
||
}
|
||
|
||
public function roleLabel(string $roleKey): string
|
||
{
|
||
return match ($roleKey) {
|
||
'owner' => __('Owner'),
|
||
'responsible' => __('Verantwortlich'),
|
||
default => __('Mitglied'),
|
||
};
|
||
}
|
||
|
||
public function fastLogoUrl(Company $company): ?string
|
||
{
|
||
// Delegiert an die zentrale Auflösung inkl. der migrierten
|
||
// Legacy-Pfade (company-logos/{portal}/{id}/…) — die frühere
|
||
// „schnelle" Variante übersprang Legacy-Firmen komplett, wodurch
|
||
// in der Übersicht trotz vorhandenem Logo nur die Initialen
|
||
// erschienen. Die Existenz-Checks laufen auf dem lokalen Disk
|
||
// und sind für 50 Karten pro Seite unkritisch.
|
||
return $company->logoUrl();
|
||
}
|
||
|
||
public function with(): array
|
||
{
|
||
$user = auth()->user();
|
||
|
||
$query = $this->baseQuery($user)
|
||
->select([
|
||
'companies.id',
|
||
'companies.owner_user_id',
|
||
'companies.portal',
|
||
'companies.type',
|
||
'companies.name',
|
||
'companies.address',
|
||
'companies.logo_path',
|
||
'companies.legacy_portal',
|
||
'companies.is_active',
|
||
])
|
||
->addSelect([
|
||
'current_user_role' => DB::table('company_user')
|
||
->select('role')
|
||
->whereColumn('company_user.company_id', 'companies.id')
|
||
->where('company_user.user_id', $user->id)
|
||
->limit(1),
|
||
])
|
||
->withCount([
|
||
'contacts' => fn ($q) => $q->withoutGlobalScopes(),
|
||
'pressReleases' => fn ($q) => $q->withoutGlobalScopes(),
|
||
])
|
||
->withMax(['pressReleases' => fn ($q) => $q->withoutGlobalScopes()], 'published_at');
|
||
|
||
$this->applySavedView($query, $user, $this->savedView);
|
||
$this->applySharedFilters($query);
|
||
$this->applyRoleFilter($query, $user, $this->roleFilter);
|
||
|
||
$pressKits = $query
|
||
->orderBy('name')
|
||
->paginate(50)
|
||
->through(function (Company $company) use ($user): Company {
|
||
$roleKey = $this->userRoleKey($company, $user);
|
||
$lastPublishedAt = $company->press_releases_max_published_at
|
||
? Carbon::parse($company->press_releases_max_published_at)
|
||
: null;
|
||
|
||
$company->setAttribute('panel_role_key', $roleKey);
|
||
$company->setAttribute('panel_is_admin', $this->isAdminRole($roleKey));
|
||
$company->setAttribute('panel_role_label', $this->roleLabel($roleKey));
|
||
$company->setAttribute('panel_logo_url', $this->fastLogoUrl($company));
|
||
$company->setAttribute('panel_logo_variant', $this->logoVariant($company));
|
||
$company->setAttribute('panel_logo_initials', $this->logoInitials($company));
|
||
$company->setAttribute('panel_meta_line', $this->metaLine($company));
|
||
$company->setAttribute(
|
||
'panel_last_press_release_short',
|
||
$lastPublishedAt?->format('d.m.Y') ?? '—'
|
||
);
|
||
$company->setAttribute(
|
||
'panel_last_press_release_date',
|
||
$lastPublishedAt?->format('d.m.Y') ?? '—'
|
||
);
|
||
|
||
return $company;
|
||
});
|
||
|
||
$aggregates = $this->buildAggregateCounts($user);
|
||
|
||
return [
|
||
'pressKits' => $pressKits,
|
||
'user' => $user,
|
||
'hasActiveFilters' => filled($this->search)
|
||
|| $this->savedView !== 'all'
|
||
|| filled($this->portalFilter)
|
||
|| $this->roleFilter !== 'all',
|
||
'counters' => $aggregates['counters'],
|
||
'savedViewCounts' => $aggregates['saved_views'],
|
||
];
|
||
}
|
||
}; ?>
|
||
|
||
<div class="space-y-6">
|
||
{{-- ============== PAGE HEADER ============== --}}
|
||
<header class="page-header">
|
||
<div class="min-w-0">
|
||
<div class="flex items-center gap-3 mb-3 flex-nowrap whitespace-nowrap">
|
||
<span class="badge hub dot">{{ __('User Backend') }}</span>
|
||
<span class="eyebrow muted">{{ __('Mein Bereich · Firmen') }}</span>
|
||
</div>
|
||
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
|
||
{{ __('Meine Firmen') }}
|
||
</h1>
|
||
|
||
<div class="counter-strip mt-3">
|
||
<span class="seg">
|
||
<b>{{ $counters['companies'] }}</b> {{ __('Firmen') }}
|
||
</span>
|
||
<span class="sep"></span>
|
||
<span class="seg is-ok">
|
||
<b>{{ $counters['active'] }}</b> {{ __('aktiv') }}
|
||
</span>
|
||
<span class="sep"></span>
|
||
<span class="seg">
|
||
<b>{{ $counters['press_releases'] }}</b>
|
||
{{ __('Pressemitteilungen gesamt') }}
|
||
</span>
|
||
<span class="sep"></span>
|
||
<span class="seg">
|
||
<b>{{ $counters['contacts'] }}</b>
|
||
{{ __('Pressekontakte hinterlegt') }}
|
||
</span>
|
||
</div>
|
||
|
||
<p class="mt-3 text-[12.5px] leading-[1.55] max-w-[640px] m-0 text-[color:var(--color-ink-3)]">
|
||
{{ __('Eine Firma ist der Container für Pressemitteilungen: Stammdaten, Boilerplate, Pressekontakte. Anlage ohne separate Freigabe — die redaktionelle Prüfung erfolgt erst bei der Pressemitteilung.') }}
|
||
</p>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-2 flex-shrink-0">
|
||
<flux:button variant="filled" icon="document-arrow-down" disabled>
|
||
{{ __('Export') }}
|
||
<span class="badge muted ml-2" style="font-size:9px;padding:0 5px;letter-spacing:0.06em;">
|
||
{{ __('bald') }}
|
||
</span>
|
||
</flux:button>
|
||
<flux:button variant="primary" icon="plus" href="{{ route('me.press-kits.create') }}" wire:navigate>
|
||
{{ __('Firma anlegen') }}
|
||
</flux:button>
|
||
</div>
|
||
</header>
|
||
|
||
{{-- ============== SAVED VIEW TABS ============== --}}
|
||
<nav class="view-tabs" aria-label="{{ __('Gespeicherte Ansichten') }}">
|
||
@php
|
||
$savedViewMeta = [
|
||
'all' => __('Alle'),
|
||
'active' => __('Aktiv'),
|
||
'drafts' => __('In Anlage'),
|
||
'inactive' => __('Inaktiv'),
|
||
'shared' => __('Mit mir geteilt'),
|
||
];
|
||
@endphp
|
||
|
||
@foreach ($savedViewMeta as $key => $label)
|
||
<button
|
||
type="button"
|
||
wire:click="setSavedView('{{ $key }}')"
|
||
class="view-tab {{ $savedView === $key ? 'is-active' : '' }}"
|
||
data-view="{{ $key }}"
|
||
@if ($key === 'drafts') disabled aria-disabled="true" @endif
|
||
>
|
||
{{ $label }}
|
||
<span class="cnt">{{ $savedViewCounts[$key] }}</span>
|
||
</button>
|
||
@endforeach
|
||
</nav>
|
||
|
||
{{-- ============== FILTER + SUCHE ============== --}}
|
||
<section class="space-y-3">
|
||
<div class="flex items-center gap-2 flex-wrap">
|
||
<flux:dropdown align="start">
|
||
<button type="button" class="filter-chip {{ filled($portalFilter) ? 'is-active' : '' }}">
|
||
@if ($portalFilter === 'presseecho')
|
||
<span class="dot-pe inline-block"></span>
|
||
@elseif ($portalFilter === 'businessportal24')
|
||
<span class="dot-bp inline-block"></span>
|
||
@else
|
||
<span class="dot-pe inline-block" style="margin-right:1px;"></span>
|
||
<span class="dot-bp inline-block" style="margin-left:-2px;"></span>
|
||
@endif
|
||
{{ __('Portal') }}:
|
||
<strong class="font-semibold">
|
||
@switch($portalFilter)
|
||
@case('presseecho') presseecho @break
|
||
@case('businessportal24') businessportal24 @break
|
||
@default {{ __('Alle') }}
|
||
@endswitch
|
||
</strong>
|
||
<flux:icon.chevron-down class="size-3 caret" />
|
||
</button>
|
||
<flux:menu>
|
||
<flux:menu.item wire:click="setPortalFilter('')">{{ __('Alle Portale') }}</flux:menu.item>
|
||
<flux:menu.item wire:click="setPortalFilter('presseecho')">presseecho</flux:menu.item>
|
||
<flux:menu.item wire:click="setPortalFilter('businessportal24')">businessportal24</flux:menu.item>
|
||
</flux:menu>
|
||
</flux:dropdown>
|
||
|
||
<flux:dropdown align="start">
|
||
<button type="button" class="filter-chip {{ $roleFilter !== 'all' ? 'is-active' : '' }}">
|
||
<flux:icon.user class="size-3 opacity-70" />
|
||
{{ __('Rolle') }}:
|
||
<strong class="font-semibold">
|
||
@switch($roleFilter)
|
||
@case('owner') {{ __('Owner') }} @break
|
||
@case('responsible') {{ __('Verantwortlich') }} @break
|
||
@case('member') {{ __('Mitglied') }} @break
|
||
@default {{ __('Alle') }}
|
||
@endswitch
|
||
</strong>
|
||
<flux:icon.chevron-down class="size-3 caret" />
|
||
</button>
|
||
<flux:menu>
|
||
<flux:menu.item wire:click="setRoleFilter('all')">{{ __('Alle Rollen') }}</flux:menu.item>
|
||
<flux:menu.item wire:click="setRoleFilter('owner')">{{ __('Owner') }}</flux:menu.item>
|
||
<flux:menu.item wire:click="setRoleFilter('responsible')">{{ __('Verantwortlich') }}</flux:menu.item>
|
||
<flux:menu.item wire:click="setRoleFilter('member')">{{ __('Mitglied') }}</flux:menu.item>
|
||
</flux:menu>
|
||
</flux:dropdown>
|
||
|
||
<button type="button" class="filter-chip" disabled aria-disabled="true" title="{{ __('Branche-Filter folgt') }}">
|
||
<flux:icon.tag class="size-3 opacity-70" />
|
||
{{ __('Branche') }}: <strong class="font-semibold">{{ __('bald') }}</strong>
|
||
</button>
|
||
|
||
<span class="w-px h-6 bg-[color:var(--color-bg-rule)] mx-1"></span>
|
||
|
||
<div class="search-wrap" style="max-width:340px;">
|
||
<flux:icon.magnifying-glass class="ico size-3" />
|
||
<input
|
||
type="search"
|
||
wire:model.live.debounce.300ms="search"
|
||
placeholder="{{ __('Firmenname, Stadt oder E-Mail…') }}"
|
||
/>
|
||
</div>
|
||
|
||
<span class="flex-1"></span>
|
||
|
||
{{-- View-Toggle Karten/Liste --}}
|
||
<div class="seg-toggle" role="tablist" aria-label="{{ __('Ansicht umschalten') }}">
|
||
<button
|
||
type="button"
|
||
wire:click="setViewMode('cards')"
|
||
class="cursor-pointer {{ $viewMode === 'cards' ? 'is-active' : '' }}"
|
||
aria-label="{{ __('Kartenansicht') }}"
|
||
data-viewmode="cards"
|
||
>
|
||
<flux:icon.squares-2x2 class="size-3" />
|
||
{{ __('Karten') }}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
wire:click="setViewMode('list')"
|
||
class="cursor-pointer {{ $viewMode === 'list' ? 'is-active' : '' }}"
|
||
aria-label="{{ __('Listenansicht') }}"
|
||
data-viewmode="list"
|
||
>
|
||
<flux:icon.list-bullet class="size-3" />
|
||
{{ __('Liste') }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{{-- ============== CONTENT-HOST ============== --}}
|
||
<article data-state-host>
|
||
|
||
@if ($pressKits->isEmpty())
|
||
{{-- Empty States --}}
|
||
@if ($hasActiveFilters)
|
||
{{-- Empty: Filter ohne Treffer --}}
|
||
<div class="panel" data-state="empty-filter">
|
||
<div class="empty-stage">
|
||
<div class="empty-ico warm">
|
||
<flux:icon.funnel class="size-6" />
|
||
</div>
|
||
<h3 class="empty-title">{{ __('Keine Firmen mit diesen Filtern') }}</h3>
|
||
<p class="empty-sub">
|
||
{{ __('Aktive Filter passen auf keine Einträge. Filter zurücksetzen oder weiter fassen.') }}
|
||
</p>
|
||
<div class="flex items-center gap-2.5 mt-6">
|
||
<flux:button variant="primary" wire:click="resetFilters">
|
||
{{ __('Alle Filter zurücksetzen') }}
|
||
</flux:button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
@else
|
||
{{-- Empty: noch keine Firma --}}
|
||
<div class="panel" data-state="empty-none">
|
||
<div class="empty-stage">
|
||
<div class="empty-ico">
|
||
<flux:icon.building-office class="size-6" />
|
||
</div>
|
||
<h3 class="empty-title">{{ __('Noch keine Firma angelegt') }}</h3>
|
||
<p class="empty-sub">
|
||
{{ __('Lege deine erste Firma an. Du kannst direkt im Anschluss eine Pressemitteilung darauf veröffentlichen — eine separate Freigabe der Firma ist nicht erforderlich.') }}
|
||
</p>
|
||
<div class="flex items-center gap-2.5 mt-6">
|
||
<flux:button variant="primary" icon="plus" href="{{ route('me.press-kits.create') }}" wire:navigate>
|
||
{{ __('Erste Firma anlegen') }}
|
||
</flux:button>
|
||
</div>
|
||
<div class="mt-9 grid gap-3 w-full max-w-[560px]" style="grid-template-columns:repeat(3,1fr);">
|
||
<div class="text-left px-3 py-2.5 rounded-[3px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)]">
|
||
<div class="font-mono text-[9.5px] tracking-[0.16em] font-bold mb-1 text-[color:var(--color-accent-deep)]">01</div>
|
||
<div class="text-[11.5px] font-semibold leading-tight text-[color:var(--color-ink)]">
|
||
{{ __('Stammdaten erfassen') }}
|
||
</div>
|
||
</div>
|
||
<div class="text-left px-3 py-2.5 rounded-[3px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)]">
|
||
<div class="font-mono text-[9.5px] tracking-[0.16em] font-bold mb-1 text-[color:var(--color-accent-deep)]">02</div>
|
||
<div class="text-[11.5px] font-semibold leading-tight text-[color:var(--color-ink)]">
|
||
{{ __('Boilerplate schreiben') }}
|
||
</div>
|
||
</div>
|
||
<div class="text-left px-3 py-2.5 rounded-[3px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)]">
|
||
<div class="font-mono text-[9.5px] tracking-[0.16em] font-bold mb-1 text-[color:var(--color-accent-deep)]">03</div>
|
||
<div class="text-[11.5px] font-semibold leading-tight text-[color:var(--color-ink)]">
|
||
{{ __('Pressekontakte zuordnen') }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
@endif
|
||
|
||
@elseif ($viewMode === 'cards')
|
||
{{-- Karten-Ansicht --}}
|
||
<div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3" data-state="cards">
|
||
@foreach ($pressKits as $company)
|
||
<div
|
||
class="firm-card {{ $company->panel_is_admin ? 'is-self' : '' }}"
|
||
wire:key="firm-card-{{ $company->id }}"
|
||
data-testid="firm-card-{{ $company->id }}"
|
||
>
|
||
<div class="flex items-start justify-between gap-3">
|
||
<div class="logo {{ $company->panel_logo_url ? '' : $company->panel_logo_variant }}">
|
||
@if ($company->panel_logo_url)
|
||
<img src="{{ $company->panel_logo_url }}" alt="{{ $company->name }}" loading="lazy" />
|
||
@else
|
||
{{ $company->panel_logo_initials }}
|
||
@endif
|
||
</div>
|
||
<div class="flex items-center gap-1">
|
||
@if ($company->is_active)
|
||
<span class="badge ok dot">{{ __('Aktiv') }}</span>
|
||
@else
|
||
<span class="badge err dot">{{ __('Inaktiv') }}</span>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
|
||
<div class="min-w-0">
|
||
<h3 class="name">{{ $company->name }}</h3>
|
||
@if (filled($company->panel_meta_line))
|
||
<div class="meta-line">{{ $company->panel_meta_line }}</div>
|
||
@endif
|
||
</div>
|
||
|
||
<div class="flex items-center gap-2 flex-wrap">
|
||
@if ($company->portal === \App\Enums\Portal::Both)
|
||
<span class="portal-pill pe"><span class="pdot"></span>presseecho</span>
|
||
<span class="portal-pill bp"><span class="pdot"></span>businessportal24</span>
|
||
@elseif ($company->portal === \App\Enums\Portal::Presseecho)
|
||
<span class="portal-pill pe"><span class="pdot"></span>presseecho</span>
|
||
@elseif ($company->portal === \App\Enums\Portal::Businessportal24)
|
||
<span class="portal-pill bp"><span class="pdot"></span>businessportal24</span>
|
||
@endif
|
||
|
||
<span class="role-pill {{ $company->panel_is_admin ? 'admin' : '' }}">
|
||
{{ $company->panel_role_label }}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="kpis">
|
||
<div class="kpi">
|
||
<span class="k">{{ $company->press_releases_count }}</span>
|
||
<span class="l">{{ __('PMs') }}</span>
|
||
</div>
|
||
<div class="kpi">
|
||
<span class="k">{{ $company->contacts_count }}</span>
|
||
<span class="l">{{ __('Kontakte') }}</span>
|
||
</div>
|
||
<div class="kpi">
|
||
<span class="k">
|
||
{{ $company->panel_last_press_release_short }}
|
||
</span>
|
||
<span class="l">{{ __('letzte PM') }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-2 pt-1">
|
||
<a href="{{ route('me.press-kits.show', $company->id) }}" wire:navigate class="card-action primary" style="flex:1;">
|
||
<flux:icon.arrow-right class="size-3" />
|
||
{{ __('Firma öffnen') }}
|
||
</a>
|
||
<a href="{{ route('me.press-releases.create') }}" wire:navigate class="card-action">
|
||
<flux:icon.plus class="size-3" />
|
||
{{ __('Neue PM') }}
|
||
</a>
|
||
</div>
|
||
</div>
|
||
@endforeach
|
||
|
||
{{-- Add-Tile am Ende des Grids, nur auf der letzten Seite --}}
|
||
@if ($pressKits->currentPage() === $pressKits->lastPage())
|
||
<a href="{{ route('me.press-kits.create') }}" wire:navigate class="add-tile" data-testid="add-tile">
|
||
<span class="ico">
|
||
<flux:icon.plus class="size-5" />
|
||
</span>
|
||
<span class="lbl">{{ __('Neue Firma anlegen') }}</span>
|
||
<span class="sub">
|
||
{{ __('Stammdaten und Boilerplate. Die Anlage benötigt keine separate Freigabe.') }}
|
||
</span>
|
||
</a>
|
||
@endif
|
||
</div>
|
||
|
||
@else
|
||
{{-- Listen-Ansicht --}}
|
||
<div class="panel overflow-hidden" data-state="list">
|
||
<div class="overflow-x-auto">
|
||
<table class="list">
|
||
<colgroup>
|
||
<col style="width:88px;" />
|
||
<col />
|
||
<col style="width:190px;" />
|
||
<col style="width:140px;" />
|
||
<col style="width:110px;" />
|
||
<col style="width:80px;" />
|
||
<col style="width:100px;" />
|
||
<col style="width:130px;" />
|
||
<col style="width:56px;" />
|
||
</colgroup>
|
||
<thead>
|
||
<tr>
|
||
<th></th>
|
||
<th>{{ __('Firma') }}</th>
|
||
<th>{{ __('Portal') }}</th>
|
||
<th>{{ __('Rolle') }}</th>
|
||
<th>{{ __('Status') }}</th>
|
||
<th style="text-align:right;">{{ __('PMs') }}</th>
|
||
<th style="text-align:right;">{{ __('Kontakte') }}</th>
|
||
<th>{{ __('Letzte PM') }}</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@foreach ($pressKits as $company)
|
||
<tr wire:key="firm-row-{{ $company->id }}" data-testid="firm-row-{{ $company->id }}">
|
||
<td>
|
||
<span class="mini-logo {{ $company->panel_logo_url ? '' : $company->panel_logo_variant }}">
|
||
@if ($company->panel_logo_url)
|
||
<img src="{{ $company->panel_logo_url }}" alt="{{ $company->name }}" loading="lazy" />
|
||
@else
|
||
{{ $company->panel_logo_initials }}
|
||
@endif
|
||
</span>
|
||
</td>
|
||
<td>
|
||
<a href="{{ route('me.press-kits.show', $company->id) }}" wire:navigate class="row-title">
|
||
{{ $company->name }}
|
||
</a>
|
||
@if (filled($company->panel_meta_line))
|
||
<div class="row-sub">{{ $company->panel_meta_line }}</div>
|
||
@endif
|
||
</td>
|
||
<td>
|
||
<div class="flex flex-wrap items-center gap-1">
|
||
@if ($company->portal === \App\Enums\Portal::Both)
|
||
<span class="portal-pill pe"><span class="pdot"></span>presseecho</span>
|
||
<span class="portal-pill bp"><span class="pdot"></span>businessportal24</span>
|
||
@elseif ($company->portal === \App\Enums\Portal::Presseecho)
|
||
<span class="portal-pill pe"><span class="pdot"></span>presseecho</span>
|
||
@elseif ($company->portal === \App\Enums\Portal::Businessportal24)
|
||
<span class="portal-pill bp"><span class="pdot"></span>businessportal24</span>
|
||
@endif
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<span class="role-pill {{ $company->panel_is_admin ? 'admin' : '' }}">
|
||
{{ $company->panel_role_label }}
|
||
</span>
|
||
</td>
|
||
<td>
|
||
@if ($company->is_active)
|
||
<span class="badge ok dot">{{ __('Aktiv') }}</span>
|
||
@else
|
||
<span class="badge err dot">{{ __('Inaktiv') }}</span>
|
||
@endif
|
||
</td>
|
||
<td style="text-align:right;">
|
||
<span class="row-num">{{ $company->press_releases_count }}</span>
|
||
</td>
|
||
<td style="text-align:right;">
|
||
<span class="row-num">{{ $company->contacts_count }}</span>
|
||
</td>
|
||
<td>
|
||
<span class="row-num">
|
||
{{ $company->panel_last_press_release_date }}
|
||
</span>
|
||
</td>
|
||
<td style="text-align:right;">
|
||
<div class="firm-list-actions flex items-center justify-end gap-1">
|
||
<flux:button
|
||
size="sm"
|
||
variant="filled"
|
||
icon="eye"
|
||
href="{{ route('me.press-kits.show', $company->id) }}"
|
||
wire:navigate
|
||
aria-label="{{ __('Firma öffnen') }}"
|
||
title="{{ __('Firma öffnen') }}"
|
||
/>
|
||
<flux:button
|
||
size="sm"
|
||
variant="filled"
|
||
icon="document-plus"
|
||
href="{{ route('me.press-releases.create') }}"
|
||
wire:navigate
|
||
aria-label="{{ __('Neue Pressemitteilung') }}"
|
||
title="{{ __('Neue Pressemitteilung') }}"
|
||
/>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
@endforeach
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
@endif
|
||
|
||
</article>
|
||
|
||
{{-- Pagination --}}
|
||
@if ($pressKits->hasPages())
|
||
<div class="px-1">
|
||
{{ $pressKits->links('components.portal.pagination', ['scrollTo' => '[data-state-host]']) }}
|
||
</div>
|
||
@endif
|
||
|
||
{{-- ============== ROLLEN-LEGENDE ============== --}}
|
||
<article class="panel-warm p-5">
|
||
<div class="grid items-start gap-6" style="grid-template-columns:auto 1fr;">
|
||
<div class="min-w-[180px]">
|
||
<div class="section-eyebrow">{{ __('Rollen pro Firma') }}</div>
|
||
<p class="text-[12px] leading-[1.55] mt-3 m-0 max-w-[220px] text-[color:var(--color-ink-3)]">
|
||
{{ __('Mehrere Personen können einer Firma zugeordnet sein. Die Rolle steuert, was im Backend möglich ist.') }}
|
||
</p>
|
||
</div>
|
||
|
||
<div class="grid gap-4" style="grid-template-columns:repeat(3,1fr);">
|
||
<div>
|
||
<span class="role-pill admin" style="margin-bottom:8px;">{{ __('Owner') }}</span>
|
||
<ul class="text-[11.5px] leading-[1.7] mt-2 list-none p-0 m-0 space-y-0.5 text-[color:var(--color-ink-2)]">
|
||
<li>{{ __('Stammdaten & Boilerplate') }}</li>
|
||
<li>{{ __('Pressekontakte verwalten') }}</li>
|
||
<li>{{ __('PMs erstellen, einreichen, archivieren') }}</li>
|
||
<li>{{ __('Weitere Mitglieder einladen') }}</li>
|
||
</ul>
|
||
</div>
|
||
<div>
|
||
<span class="role-pill admin" style="margin-bottom:8px;">{{ __('Verantwortlich') }}</span>
|
||
<ul class="text-[11.5px] leading-[1.7] mt-2 list-none p-0 m-0 space-y-0.5 text-[color:var(--color-ink-2)]">
|
||
<li>{{ __('Stammdaten & Boilerplate') }}</li>
|
||
<li>{{ __('Pressekontakte verwalten') }}</li>
|
||
<li>{{ __('PMs erstellen & einreichen') }}</li>
|
||
<li class="text-[color:var(--color-ink-4)]">{{ __('keine Mitglieder-Verwaltung') }}</li>
|
||
</ul>
|
||
</div>
|
||
<div>
|
||
<span class="role-pill" style="margin-bottom:8px;">
|
||
{{ __('Mitglied') }}
|
||
<span class="text-[color:var(--color-ink-4)] font-normal">· {{ __('bald erweitert') }}</span>
|
||
</span>
|
||
<ul class="text-[11.5px] leading-[1.7] mt-2 list-none p-0 m-0 space-y-0.5 text-[color:var(--color-ink-2)]">
|
||
<li>{{ __('PMs einsehen') }}</li>
|
||
<li>{{ __('Stammdaten lesen') }}</li>
|
||
<li class="text-[color:var(--color-ink-4)]">{{ __('keine Bearbeitung') }}</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
</div>
|