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

291 lines
18 KiB
PHP

@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
{{-- ── What the customer has ─────────────────────────────────────
The template's four cards, in its order and its form: label with a
chevron, the figure and its unit on one line, a line of context
underneath, the visual on the right or a bar across the foot.
Three of the template's visuals have no source yet storage
consumption is not sampled, instance_traffic keeps one row per
period rather than a daily series, and monitoring_targets records
a state but no uptime figure. Those cards carry the form and state
what is true instead of drawing a chart at an invented level. This
is the sheet a customer forwards to their auditor. --}}
<section class="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
@if ($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
</x-ui.metric>
@if ($traffic)
@php $state = $traffic->state(); @endphp
<x-ui.metric
:label="__('dashboard.traffic.title')"
:value="Bytes::human($traffic->usedBytes)"
:unit="__('dashboard.traffic.of', ['total' => Bytes::human($traffic->quotaBytes)])"
:foot="__('dashboard.traffic.resets', ['days' => $traffic->daysUntilReset()])"
:href="route('billing')"
class="animate-rise [animation-delay:140ms]"
>
<div class="mt-4 h-1.5 overflow-hidden rounded-pill bg-surface-hover">
<div @class([
'h-full rounded-pill',
'bg-accent' => $state === 'ok',
'bg-warning' => $state === 'warn' || $state === 'critical',
'bg-danger' => $state === 'exhausted',
])
style="width: {{ min(100, $traffic->percent()) }}%"></div>
</div>
@if ($state !== 'ok')
{{-- The offer belongs next to the warning: someone
reading that they are nearly out should not have to
go looking for the way to fix it. --}}
<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
{{-- Where the template shows availability. There is no uptime
figure to show, so this card carries the thing this product is
actually about and can prove: when the last backup ran and
whether it was verified. --}}
@php $lastBackup = collect($proofs)->firstWhere('key', 'backup'); @endphp
@if ($lastBackup)
<x-ui.metric
:label="__('dashboard.master.last_backup')"
:value="$lastBackup['at']?->locale($locale)->isoFormat('HH:mm') ?? '—'"
:unit="$lastBackup['at']?->locale($locale)->isoFormat('DD.MM.')"
:foot="__('dashboard.proofs.state.'.$lastBackup['state']).' · '.($lastBackup['note'] ?? '')"
:href="route('backups')"
class="animate-rise [animation-delay:180ms]"
/>
@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>