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

302 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

@php
use App\Support\Bytes;
use Illuminate\Support\Number;
$locale = app()->getLocale();
$money = fn (int $cents, string $currency) => Number::currency($cents / 100, in: $currency, locale: $locale);
@endphp
<div class="space-y-5">
{{-- Page head. The same pill the public site uses to label a section, so a
customer meets one vocabulary on both sides of the login. --}}
<header class="flex flex-wrap items-end gap-4 animate-rise">
<div class="min-w-0 flex-1">
<p class="font-mono text-[11px] uppercase tracking-[0.07em] text-muted">
{{ __('dashboard.sheet', ['when' => $asOf->locale($locale)->isoFormat('LL, LT')]) }}
</p>
<h1 class="mt-2.5 text-2xl font-bold tracking-tight text-ink sm:text-3xl">
{{ $instance !== null ? __('dashboard.title_running') : __('dashboard.title_pending') }}
</h1>
</div>
@if ($instance !== null)
<div class="flex flex-wrap items-center gap-2.5">
<span @class([
'inline-flex items-center gap-2 rounded-pill border px-3.5 py-1.5 text-xs font-semibold',
'border-success-border bg-success-bg text-success' => $instance->status === 'active',
'border-warning-border bg-warning-bg text-warning' => $instance->status !== 'active',
])>
<span class="size-1.5 rounded-pill bg-current" aria-hidden="true"></span>
{{ __('dashboard.instance_status.'.$instance->status) }}
</span>
@if ($domain)
<x-ui.button variant="ink" :href="'https://'.$domain" target="_blank" rel="noopener">
<x-ui.icon name="external-link" class="size-4" />{{ __('dashboard.cloud.open') }}
</x-ui.button>
@endif
</div>
@endif
</header>
{{-- Live provisioning progress (only while a run is in flight) --}}
<livewire:customer-provisioning />
@if ($instance === null)
{{-- Said plainly rather than papered over with an empty record: a page
of dashes reads as broken, and "we are still setting it up" and
"you have not ordered yet" are different things. --}}
<section class="rounded-lg border border-line bg-surface p-8 shadow-xs animate-rise">
<h2 class="text-lg font-semibold text-ink">{{ __('dashboard.no_instance_label') }}</h2>
<p class="mt-2 max-w-prose text-md text-muted">{{ __('dashboard.no_instance_body') }}</p>
<div class="mt-6 flex flex-wrap gap-2.5">
<x-ui.button variant="ink" :href="route('billing')" wire:navigate>{{ __('dashboard.no_instance_cta') }}</x-ui.button>
<x-ui.button variant="secondary" :href="route('support')" wire:navigate>{{ __('dashboard.nav.support') }}</x-ui.button>
</div>
</section>
@else
{{-- ── The four cards, in the template's order and form ──────────
Speicher · Benutzer · Datenvolumen · Verfügbarkeit. Figure, unit
beside it, a line of context, the visual — ring, bar, sparkline.
Every number below is measured; nothing is drawn at a level nobody
read. --}}
<section class="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
@if ($disk)
<x-ui.metric
:label="__('dashboard.master.storage')"
:value="Number::format($disk['used'] / 1024 ** 3, precision: 0, locale: $locale)"
:unit="'/ '.Number::format($disk['total'] / 1024 ** 3, precision: 0, locale: $locale).' GB'"
:foot="trim(__('dashboard.master.storage_used', ['percent' => Number::format($disk['percent'], precision: 0, locale: $locale)])
.($disk['week_delta'] !== null ? ' · '.__('dashboard.master.storage_week', ['amount' => ($disk['week_delta'] >= 0 ? '+' : '').Bytes::human(abs($disk['week_delta']))]) : ''))"
:href="route('cloud')"
class="animate-rise [animation-delay:60ms]"
>
<x-slot:visual><x-ui.ring :percent="$disk['percent']" :size="58" /></x-slot:visual>
</x-ui.metric>
@elseif ($instance->quota_gb)
<x-ui.metric
:label="__('dashboard.master.storage')"
:value="$instance->quota_gb"
unit="GB"
:foot="__('dashboard.master.storage_note')"
:href="route('cloud')"
class="animate-rise [animation-delay:60ms]"
/>
@endif
<x-ui.metric
:label="__('dashboard.master.users')"
:value="$seats['used']"
:unit="$seats['total'] !== null ? __('dashboard.master.of_seats', ['total' => $seats['total']]) : null"
:href="route('users')"
class="animate-rise [animation-delay:100ms]"
>
@if ($seats['total'])
<div class="mt-4 h-1.5 overflow-hidden rounded-pill bg-surface-hover">
{{-- R4: width is data, not decoration. --}}
<div class="h-full rounded-pill bg-muted"
style="width: {{ min(100, (int) round($seats['used'] / max(1, $seats['total']) * 100)) }}%"></div>
</div>
@endif
@if ($seatBreakdown !== [])
<p class="mt-2.5 text-xs text-muted">
{{ collect($seatBreakdown)
->map(fn (int $n, string $role) => trans_choice('users.role_count.'.$role, $n, ['count' => $n]))
->join(' · ') }}
</p>
@endif
</x-ui.metric>
@if ($traffic)
@php $state = $traffic->state(); @endphp
<x-ui.metric
:label="__('dashboard.traffic.label')"
:value="Number::format($traffic->usedBytes / 1024 ** 3, precision: 0, locale: $locale)"
:unit="'GB / '.Bytes::human($traffic->quotaBytes)"
:foot="__('dashboard.traffic.resets', ['days' => $traffic->daysUntilReset()])"
:href="route('billing')"
class="animate-rise [animation-delay:140ms]"
>
@if (count($trend) > 2)
{{-- Orange because this is the metric with an action
attached: when it runs out, something can be done
about it. Observed figures stay grey. --}}
<x-slot:visual><x-ui.spark :points="$trend" area /></x-slot:visual>
@endif
@if ($state !== 'ok')
<a href="{{ route('billing') }}" wire:navigate class="mt-3 inline-flex items-center gap-1.5 text-xs font-semibold text-accent-text hover:underline">
<x-ui.icon name="plus" class="size-3.5" />{{ __('dashboard.traffic.buy') }}
</a>
@endif
</x-ui.metric>
@endif
{{-- Availability, counted from our own monitoring answers. Absent
rather than 100 % when nothing has been checked: reporting an
unmonitored instance as perfect is the single most dishonest
number this page could carry. --}}
@if ($availability !== null)
<x-ui.metric
:label="__('dashboard.master.availability')"
:value="Number::format($availability, precision: 2, locale: $locale)"
unit="%"
:foot="__('dashboard.master.availability_note')"
:href="route('cloud')"
class="animate-rise [animation-delay:180ms]"
>
@if (count($availabilityTrend) > 2)
<x-slot:visual><x-ui.spark :points="$availabilityTrend" tone="muted" /></x-slot:visual>
@endif
</x-ui.metric>
@elseif ($location)
<x-ui.metric
:label="__('dashboard.master.location')"
:value="$location['name']"
:foot="__('dashboard.master.location_note')"
class="animate-rise [animation-delay:180ms]"
/>
@endif
</section>
<section class="grid gap-4 lg:grid-cols-[minmax(0,1.5fr)_minmax(0,1fr)] lg:items-start">
{{-- ── The proof register ────────────────────────────────────── --}}
<article class="overflow-hidden rounded-lg border border-line bg-surface shadow-xs animate-rise [animation-delay:200ms]">
<div class="flex items-center justify-between gap-4 border-b border-line px-5 py-4">
<h2 class="text-md font-semibold text-ink">{{ __('dashboard.proofs.label') }}</h2>
<a href="{{ route('backups') }}" wire:navigate class="text-sm font-semibold text-accent-text hover:underline">{{ __('dashboard.proofs.all') }}</a>
</div>
@if ($proofs === [])
<p class="px-5 py-8 text-sm text-muted">{{ __('dashboard.proofs.empty') }}</p>
@else
<div class="overflow-x-auto">
<table class="w-full text-sm">
<tbody>
@foreach ($proofs as $proof)
<tr class="border-b border-line last:border-0">
<td class="px-5 py-4">
<span class="font-semibold text-ink">{{ __('dashboard.proofs.item.'.$proof['key']) }}</span>
<span class="mt-1 block font-mono text-xs text-muted">
{{ $proof['at']?->locale($locale)->isoFormat('DD.MM. HH:mm') ?? '' }}@if ($proof['note']) · {{ $proof['note'] }}@endif
</span>
</td>
<td class="w-px whitespace-nowrap px-5 py-4 text-right">
<span @class([
'inline-flex items-center gap-1.5 rounded-pill border px-2.5 py-1 text-xs font-semibold',
'border-success-border bg-success-bg text-success' => $proof['state'] === 'ok',
'border-info-border bg-info-bg text-info' => $proof['state'] === 'planned',
'border-danger-border bg-danger-bg text-danger' => $proof['state'] === 'attention',
])>
<span class="size-1.5 rounded-pill bg-current" aria-hidden="true"></span>
{{ __('dashboard.proofs.state.'.$proof['state']) }}
</span>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
</article>
<div class="space-y-4">
{{-- ── The contract ───────────────────────────────────────── --}}
<article class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:240ms]">
<h2 class="text-md font-semibold text-ink">{{ __('dashboard.master.label') }}</h2>
<dl class="mt-4">
<div class="flex items-baseline justify-between gap-4 border-b border-line py-2.5">
<dt class="text-sm text-muted">{{ __('dashboard.master.package') }}</dt>
<dd class="text-right text-sm font-semibold text-ink">
{{ __('billing.plan.'.($instance->plan ?: 'team')) }}
@if ($planPrice)
<span class="mt-0.5 block font-mono text-xs font-normal text-muted">
{{ __('dashboard.master.per_'.($planPrice['term'] === 'yearly' ? 'year' : 'month'), ['amount' => $money($planPrice['cents'], $planPrice['currency'])]) }}
</span>
@endif
</dd>
</div>
@if ($contract?->performance)
<div class="flex items-baseline justify-between gap-4 border-b border-line py-2.5">
<dt class="text-sm text-muted">{{ __('dashboard.master.operation') }}</dt>
<dd class="text-right text-sm font-semibold text-ink">{{ __('billing.perf.'.$contract->performance) }}</dd>
</div>
@endif
@if ($domain)
<div class="flex items-baseline justify-between gap-4 py-2.5">
<dt class="text-sm text-muted">{{ __('dashboard.master.address') }}</dt>
<dd class="min-w-0 break-all text-right font-mono text-xs text-ink">{{ $domain }}</dd>
</div>
@endif
</dl>
</article>
@if ($nextInvoice)
<article class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:280ms]">
<h2 class="text-md font-semibold text-ink">{{ __('dashboard.invoice.label') }}</h2>
<dl class="mt-4">
@if ($nextInvoice['addons'] > 0)
{{-- Broken out only when there is something to
break out: a "Module 0,00 €" line on every
other customer's sheet is noise. --}}
<div class="flex items-baseline justify-between gap-4 border-b border-line py-2.5">
<dt class="text-sm text-muted">{{ __('dashboard.invoice.plan') }}</dt>
<dd class="font-mono text-sm text-ink">{{ $money($nextInvoice['plan'], $nextInvoice['currency']) }}</dd>
</div>
<div class="flex items-baseline justify-between gap-4 border-b border-line py-2.5">
<dt class="text-sm text-muted">{{ __('dashboard.invoice.addons') }}</dt>
<dd class="font-mono text-sm text-ink">{{ $money($nextInvoice['addons'], $nextInvoice['currency']) }}</dd>
</div>
@endif
<div class="flex items-baseline justify-between gap-4 border-b border-line py-2.5">
<dt class="text-sm text-muted">{{ __('dashboard.invoice.amount') }}</dt>
<dd class="text-right font-mono text-md font-semibold text-ink">
{{ $money($nextInvoice['total'], $nextInvoice['currency']) }}
<span class="mt-0.5 block text-xs font-normal text-muted">{{ __('dashboard.invoice.net') }}</span>
</dd>
</div>
<div class="flex items-baseline justify-between gap-4 py-2.5">
<dt class="text-sm text-muted">{{ __('dashboard.invoice.due') }}</dt>
<dd class="font-mono text-sm text-ink">{{ $nextInvoice['due']?->locale($locale)->isoFormat('LL') ?? '—' }}</dd>
</div>
</dl>
<x-ui.button variant="secondary" :href="route('invoices')" wire:navigate class="mt-4 w-full">
{{ __('dashboard.invoice.all') }}
</x-ui.button>
</article>
@endif
@if ($openTasks > 0)
{{-- Only while there is something outstanding. A permanently
green checklist is furniture; one with work left on it
is a reason to open this page. --}}
<article class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:320ms]">
<h2 class="text-md font-semibold text-ink">{{ __('dashboard.setup.label') }}</h2>
<p class="mt-2 text-sm text-body">{{ trans_choice('dashboard.setup.open', $openTasks, ['count' => $openTasks]) }}</p>
<a href="{{ route('cloud') }}" wire:navigate class="mt-3 inline-flex items-center gap-1.5 text-sm font-semibold text-accent-text hover:underline">
{{ __('dashboard.setup.continue') }}
</a>
</article>
@endif
</div>
</section>
{{-- ── The three things customers come here to do ─────────────────── --}}
<section class="grid gap-px overflow-hidden rounded-lg border border-line bg-line shadow-xs animate-rise [animation-delay:360ms] sm:grid-cols-3">
@foreach ([
['users', 'users', 'add_user'],
['backups', 'rotate-ccw', 'restore'],
['billing', 'plus', 'grow'],
] as [$route, $icon, $key])
<a href="{{ route($route) }}" wire:navigate class="flex min-h-11 items-center gap-3.5 bg-surface px-5 py-4 transition hover:bg-surface-hover">
<span class="grid size-9 shrink-0 place-items-center rounded bg-accent-subtle text-accent-text">
<x-ui.icon :name="$icon" class="size-[1.05rem]" />
</span>
<span class="min-w-0">
<span class="block text-sm font-semibold text-ink">{{ __('dashboard.quick.'.$key.'_title') }}</span>
<span class="block text-xs text-muted">{{ __('dashboard.quick.'.$key.'_body') }}</span>
</span>
</a>
@endforeach
</section>
@endif
</div>