CluPilotCloud/resources/views/livewire/users.blade.php

191 lines
14 KiB
PHP

<div class="space-y-6">
<div class="flex flex-wrap items-end justify-between gap-4 animate-rise">
<div>
<h1 class="text-2xl font-bold tracking-tight text-ink">{{ __('users.title') }}</h1>
<p class="mt-1 text-sm text-muted">{{ __('users.subtitle') }}</p>
</div>
<div class="text-right">
<p class="font-mono text-lg font-semibold text-ink">{{ $used }} / {{ $limit }}</p>
<p class="text-xs text-muted">{{ __('users.seats_used') }}</p>
<p class="text-xs text-muted">{{ __('users.seats_note') }}</p>
</div>
</div>
{{-- Anlegen. Ausdruecklich NICHT einladen: hier entsteht nur die Zeile,
die Einladung verschickt der Knopf in der Tabelle. Ein Inhaber soll
sein Team vorbereiten koennen, ohne dass jemand eine Mail bekommt. --}}
<form wire:submit="addSeat" class="grid grid-cols-1 gap-3 rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:60ms] sm:grid-cols-[1fr_1fr_auto_auto] sm:items-end">
<div>
<label class="text-sm font-medium text-body" for="inviteEmail">{{ __('users.invite_email') }}</label>
<input id="inviteEmail" type="email" wire:model="inviteEmail" class="mt-1.5 w-full rounded-md border border-line-strong bg-surface px-3 py-2 text-sm text-ink" />
@error('inviteEmail')<p class="mt-1 text-xs text-danger">{{ $message }}</p>@enderror
</div>
<div>
<label class="text-sm font-medium text-body" for="inviteName">{{ __('users.invite_name') }}</label>
<input id="inviteName" type="text" wire:model="inviteName" class="mt-1.5 w-full rounded-md border border-line-strong bg-surface px-3 py-2 text-sm text-ink" />
</div>
<div>
<label class="text-sm font-medium text-body" for="inviteRole">{{ __('users.role') }}</label>
<select id="inviteRole" wire:model="inviteRole" class="mt-1.5 rounded-md border border-line-strong bg-surface px-3 py-2 text-sm text-body">
<option value="admin">{{ __('users.role_admin') }}</option>
<option value="member">{{ __('users.role_member') }}</option>
<option value="readonly">{{ __('users.role_readonly') }}</option>
</select>
{{-- Der ehrliche Satz zu „Nur Lesen": die Rolle nimmt den eigenen
Speicherplatz weg, hebt aber eine Freigabe nicht auf. --}}
<p class="mt-1 text-xs text-muted">{{ __('users.role_readonly_means') }}</p>
</div>
<x-ui.button variant="primary" type="submit" wire:loading.attr="disabled" wire:target="addSeat">
<x-ui.icon name="plus" class="size-4" />{{ __('users.add') }}
</x-ui.button>
</form>
{{-- Seats --}}
<div class="overflow-hidden rounded-lg border border-line bg-surface shadow-xs animate-rise [animation-delay:120ms]">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-line bg-surface-2 text-left text-xs font-semibold text-muted">
<th class="px-4 py-3 font-semibold">{{ __('users.col_person') }}</th>
<th class="px-4 py-3 font-semibold">{{ __('users.role') }}</th>
<th class="px-4 py-3 font-semibold">{{ __('users.col_status') }}</th>
<th class="px-4 py-3 text-right font-semibold">{{ __('users.col_actions') }}</th>
</tr>
</thead>
<tbody>
@foreach ($seats as $seat)
<tr wire:key="seat-{{ $seat->uuid }}" class="border-b border-line last:border-0 hover:bg-surface-hover">
<td class="px-4 py-3">
<p class="font-medium text-ink">{{ $seat->name ?: $seat->email }}</p>
@if ($seat->name)<p class="text-xs text-muted">{{ $seat->email }}</p>@endif
</td>
<td class="px-4 py-3">
@if ($seat->role === 'owner')
<span class="inline-flex items-center gap-1.5 rounded-pill bg-accent-subtle px-2.5 py-0.5 text-xs font-semibold text-accent-text">{{ __('users.role_owner') }}</span>
@else
<select wire:change="setRole('{{ $seat->uuid }}', $event.target.value)" class="rounded-md border border-line-strong bg-surface px-2 py-1 text-xs text-body">
@foreach (['admin', 'member', 'readonly'] as $r)
<option value="{{ $r }}" @selected($seat->role === $r)>{{ __('users.role_'.$r) }}</option>
@endforeach
</select>
@endif
</td>
{{-- Was der Inhaber WILL (`status`) und was in der
Nextcloud WIRKLICH ist (`nc_state`) stehen in
einer Zelle, und der wahre Zustand geht vor:
solange der Auftrag laeuft oder gescheitert
ist, waere „Eingeladen" eine Behauptung ueber
etwas, das die Warteschlange noch gar nicht
erledigt hat. Ohne diese Spalte drueckt der
Inhaber wieder und wieder, weil nichts
sichtbar geschieht. --}}
<td class="px-4 py-3">
@if ($seat->status === 'revoked')
{{-- Entzogen geht allem anderen vor und ist
endgueltig. Ein „wird eingerichtet "
oder ein „Nochmal versuchen" an dieser
Zeile las sich wie ein Weg zurueck
den es nicht gibt und nicht geben
soll. --}}
<x-ui.badge status="suspended">{{ __('users.status_revoked') }}</x-ui.badge>
<p class="mt-1 text-xs text-muted">{{ __('users.revoked_is_final') }}</p>
@elseif ($seat->nc_state === \App\Models\Seat::STATE_NONE)
<x-ui.badge status="info">{{ __('users.state_none') }}</x-ui.badge>
@elseif ($seat->nc_state === \App\Models\Seat::STATE_PENDING)
<x-ui.badge status="info">{{ __('users.state_pending') }}</x-ui.badge>
@elseif ($seat->nc_state === \App\Models\Seat::STATE_FAILED)
<x-ui.badge status="failed">{{ __('users.state_failed') }}</x-ui.badge>
{{-- Der Grund im Klartext, nicht der Code:
'guest_failed' sagt einem Inhaber
nichts, und ein Fehlschlag ohne Grund
ist eine Sackgasse. --}}
<p class="mt-1 text-xs text-muted">{{ __('users.error_'.($seat->nc_error ?: 'unexpected')) }}</p>
<button type="button" wire:click="retry('{{ $seat->uuid }}')" class="mt-1 text-xs underline">
{{ __('users.retry') }}
</button>
@else
@php $sb = ['active' => 'active', 'invited' => 'provisioning', 'revoked' => 'suspended'][$seat->status] ?? 'info'; @endphp
<x-ui.badge :status="$sb">{{ __('users.status_'.$seat->status) }}</x-ui.badge>
@endif
</td>
<td class="px-4 py-3">
<div class="flex items-center justify-end gap-1.5">
@if ($seat->status === 'revoked')
{{-- Kein Knopf. Kein Wiedereinsetzen: der
richtige Weg zurueck ist ein NEUER
Sitz, und der geht durch die
Platzpruefung. Gesagt statt leer
gelassen, wie bei der Inhaber-Zeile
eine leere Zelle liesse jemanden nach
einem Knopf suchen, der absichtlich
fehlt. --}}
<span class="cursor-help text-xs text-muted" title="{{ __('users.revoked_is_final') }}">{{ __('users.revoked_final') }}</span>
@else
{{-- Einladen ist der zweite Vorgang und
hat deshalb seinen eigenen Knopf. Er
steht nur an einer Zeile, die noch
nichts in der Nextcloud hat; ist der
Auftrag unterwegs, gibt es nichts zu
druecken, und bei einem Fehlschlag
steht „Nochmal versuchen" schon in der
Zustandsspalte. Nie an der
Inhaber-Zeile: dieses Konto legt die
Bereitstellung an, nicht das Panel. --}}
@if ($seat->role !== 'owner' && $seat->nc_state === \App\Models\Seat::STATE_NONE)
<button type="button" wire:click="sendInvite('{{ $seat->uuid }}')"
wire:loading.attr="disabled" wire:target="sendInvite"
class="rounded-md border border-line px-2.5 py-1.5 text-xs font-semibold text-muted hover:bg-surface-hover">{{ __('users.invite') }}</button>
@elseif ($seat->role !== 'owner' && $seat->nc_state === \App\Models\Seat::STATE_SYNCED && $seat->status === 'invited')
<button type="button" wire:click="sendInvite('{{ $seat->uuid }}')"
wire:loading.attr="disabled" wire:target="sendInvite"
class="rounded-md border border-line px-2.5 py-1.5 text-xs font-semibold text-muted hover:bg-surface-hover">{{ __('users.resend') }}</button>
@endif
{{-- Rename is offered on every row, the owner
included: a row you can do nothing to reads
as a broken table, not as a protected one.
It opens a modal, never the row itself —
see R20. --}}
<button type="button"
x-on:click="$dispatch('openModal', { component: 'edit-seat', arguments: { uuid: '{{ $seat->uuid }}' } })"
aria-label="{{ __('users.edit') }}" title="{{ __('users.edit') }}"
class="grid size-9 place-items-center rounded-md border border-line text-muted hover:border-ink hover:text-ink">
<x-ui.icon name="pencil" class="size-4" />
</button>
@if ($seat->role === 'owner')
{{-- Said, not hidden. The owner keeps their
own access; an empty cell would leave
somebody hunting for a button that is
deliberately absent. --}}
<span class="cursor-help text-xs text-muted" title="{{ __('users.owner_no_actions') }}">{{ __('users.owner_protected') }}</span>
@else
{{-- Pause first, remove second. Somebody
leaving needs their access stopped
today; deleting the seat also throws
away who held it, which is the half
an audit asks about. --}}
<button type="button" wire:click="suspend('{{ $seat->uuid }}')"
aria-label="{{ $seat->status === 'suspended' ? __('users.reactivate') : __('users.suspend') }}"
title="{{ $seat->status === 'suspended' ? __('users.reactivate') : __('users.suspend') }}"
class="grid size-9 place-items-center rounded-md border border-line text-muted hover:border-warning hover:text-warning">
<x-ui.icon :name="$seat->status === 'suspended' ? 'unlock' : 'lock'" class="size-4" />
</button>
<button type="button"
x-on:click="$dispatch('openModal', { component: 'confirm-revoke-seat', arguments: { uuid: '{{ $seat->uuid }}' } })"
aria-label="{{ __('users.revoke') }}" title="{{ __('users.revoke') }}"
class="grid size-9 place-items-center rounded-md border border-line text-muted hover:border-danger hover:text-danger">
<x-ui.icon name="trash-2" class="size-4" />
</button>
@endif
@endif
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>