141 lines
8.1 KiB
PHP
141 lines
8.1 KiB
PHP
<?php
|
|
|
|
use App\Models\Invitation;
|
|
use App\Models\TeamMember;
|
|
use Livewire\Attributes\Layout;
|
|
use Livewire\Volt\Component;
|
|
|
|
new #[Layout('layouts.app')] class extends Component {
|
|
public function with(): array
|
|
{
|
|
$members = TeamMember::query()->with('user')->orderBy('role')->get();
|
|
$invitations = Invitation::query()
|
|
->with('invitedBy')
|
|
->whereNull('accepted_at')
|
|
->orderByDesc('sent_at')
|
|
->get();
|
|
|
|
return [
|
|
'members' => $members,
|
|
'invitations' => $invitations,
|
|
'totalMembers' => $members->count(),
|
|
'adminCount' => $members->whereIn('role', ['owner', 'admin'])->count(),
|
|
'pendingInvites' => $invitations->count(),
|
|
'twoFaCoverage' => $members->count() ? (int) round($members->where('two_fa_enabled')->count() / $members->count() * 100) : 0,
|
|
];
|
|
}
|
|
}; ?>
|
|
|
|
<div>
|
|
<div class="clu-topbar">
|
|
<div>
|
|
<h1 class="clu-page-title">Team</h1>
|
|
<div class="clu-page-sub">{{ $totalMembers }} mitglieder · {{ $pendingInvites }} einladungen offen</div>
|
|
</div>
|
|
<span class="clu-status-chip"><span class="clu-pulse-dot"></span> {{ $twoFaCoverage }}% mit 2FA</span>
|
|
<div class="spacer"></div>
|
|
<button class="clu-btn-primary" style="height:36px;padding:0 14px;">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 5v14M5 12h14"/></svg>
|
|
Einladen
|
|
</button>
|
|
</div>
|
|
|
|
<div class="grid gap-[14px] mb-[18px]" style="grid-template-columns:repeat(auto-fit,minmax(220px,1fr));">
|
|
<div class="clu-metric">
|
|
<div class="clu-metric-row"><span class="clu-metric-label">Mitglieder</span>
|
|
<span class="clu-metric-ico"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="8" r="3.5"/><path d="M3 20v-1c0-3 3-5 6-5s6 2 6 5v1"/></svg></span>
|
|
</div>
|
|
<div class="clu-metric-value">{{ $totalMembers }}</div>
|
|
<div class="clu-metric-delta">aktiv</div>
|
|
</div>
|
|
<div class="clu-metric">
|
|
<div class="clu-metric-row"><span class="clu-metric-label">Admins</span>
|
|
<span class="clu-metric-ico"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3l8 4v5c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7l8-4z"/></svg></span>
|
|
</div>
|
|
<div class="clu-metric-value">{{ $adminCount }}</div>
|
|
<div class="clu-metric-delta">owner + admin</div>
|
|
</div>
|
|
<div class="clu-metric warning">
|
|
<div class="clu-metric-row"><span class="clu-metric-label">Einladungen offen</span>
|
|
<span class="clu-metric-ico"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 5h18v14H3z"/><path d="M3 5l9 9 9-9"/></svg></span>
|
|
</div>
|
|
<div class="clu-metric-value">{{ $pendingInvites }}</div>
|
|
<div class="clu-metric-delta warn">warten auf Annahme</div>
|
|
</div>
|
|
<div class="clu-metric success">
|
|
<div class="clu-metric-row"><span class="clu-metric-label">2FA Coverage</span>
|
|
<span class="clu-metric-ico"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M9 12l2 2 4-4"/></svg></span>
|
|
</div>
|
|
<div class="clu-metric-value">{{ $twoFaCoverage }}<span class="unit">%</span></div>
|
|
<div class="clu-metric-delta">aktiviert</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="clu-card mb-[18px]">
|
|
<div class="clu-card-head"><h3>Mitglieder</h3></div>
|
|
<div class="clu-card-body tight" style="overflow-x:auto;">
|
|
<table class="clu-table">
|
|
<thead>
|
|
<tr><th>Name</th><th>Rolle</th><th>Scope</th><th>2FA</th><th>Letzte Aktivität</th><th></th></tr>
|
|
</thead>
|
|
<tbody>
|
|
@forelse ($members as $m)
|
|
<tr>
|
|
<td>
|
|
<div class="clu-site-cell">
|
|
<div class="clu-avatar" style="background:linear-gradient(135deg,#5a8bff,#8a63ff);width:28px;height:28px;border-radius:8px;font-size:11px;">{{ mb_strtoupper(mb_substr($m->user->name ?? '?', 0, 2)) }}</div>
|
|
<div class="clu-site-meta">
|
|
<span class="clu-site-domain">{{ $m->user->name ?? '—' }}</span>
|
|
<span class="clu-site-server">{{ $m->user->email ?? '—' }}</span>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td><span class="clu-pill @if($m->role === 'owner') accent @elseif($m->role === 'admin') warning @else muted @endif">{{ strtoupper($m->role) }}</span></td>
|
|
<td class="font-mono text-[11.5px]">{{ $m->scope ?? '—' }}</td>
|
|
<td>
|
|
@if ($m->two_fa_enabled)
|
|
<span class="clu-health">aktiv</span>
|
|
@else
|
|
<span class="clu-health warn">fehlt</span>
|
|
@endif
|
|
</td>
|
|
<td class="font-mono">{{ $m->last_active_at?->diffForHumans() ?? '—' }}</td>
|
|
<td><div class="clu-row-actions">
|
|
<button title="Bearbeiten" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="5" cy="12" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="19" cy="12" r="1.5"/></svg></button>
|
|
</div></td>
|
|
</tr>
|
|
@empty
|
|
<tr><td colspan="6" style="text-align:center;padding:40px;color:var(--color-muted);">Noch keine Mitglieder.</td></tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="clu-card">
|
|
<div class="clu-card-head"><h3>Offene Einladungen</h3><span class="meta">{{ $pendingInvites }} pending</span></div>
|
|
<div class="clu-card-body tight" style="overflow-x:auto;">
|
|
<table class="clu-table">
|
|
<thead><tr><th>Email</th><th>Rolle</th><th>Eingeladen von</th><th>Gesendet</th><th>Läuft ab</th><th></th></tr></thead>
|
|
<tbody>
|
|
@forelse ($invitations as $i)
|
|
<tr>
|
|
<td class="font-mono">{{ $i->email }}</td>
|
|
<td><span class="clu-pill muted">{{ strtoupper($i->role) }}</span></td>
|
|
<td>{{ $i->invitedBy->name ?? '—' }}</td>
|
|
<td class="font-mono">{{ $i->sent_at?->diffForHumans() ?? '—' }}</td>
|
|
<td class="font-mono">{{ $i->expires_at?->diffForHumans() ?? '—' }}</td>
|
|
<td><div class="clu-row-actions">
|
|
<button title="Erneut senden" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 1 0 9-9"/><path d="M3 4v5h5"/></svg></button>
|
|
<button title="Zurückziehen" type="button"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M15 9l-6 6M9 9l6 6"/></svg></button>
|
|
</div></td>
|
|
</tr>
|
|
@empty
|
|
<tr><td colspan="6" style="text-align:center;padding:40px;color:var(--color-muted);">Keine offenen Einladungen.</td></tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|