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

263 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">
<span class="inline-flex items-center gap-2 rounded-pill bg-accent-subtle px-3 py-1.5 text-xs font-semibold text-accent-text">
{{ __('dashboard.sheet', ['when' => $asOf->locale($locale)->isoFormat('LL, LT')]) }}
</span>
<h1 class="mt-3 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 ─────────────────────────────────────── --}}
<section class="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
@if ($instance->quota_gb)
<article class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:60ms]">
<p class="font-mono text-[11.5px] uppercase tracking-[0.07em] text-muted">{{ __('dashboard.master.storage') }}</p>
<p class="mt-2 font-mono text-2xl font-semibold tracking-tight text-ink">{{ __('dashboard.master.gb', ['n' => $instance->quota_gb]) }}</p>
{{-- Consumption is not metered yet. An invented "used"
figure on the sheet a customer forwards to their
auditor is the one thing this page must never do. --}}
<p class="mt-1.5 text-xs text-muted">{{ __('dashboard.master.storage_note') }}</p>
</article>
@endif
<article class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:100ms]">
<p class="font-mono text-[11.5px] uppercase tracking-[0.07em] text-muted">{{ __('dashboard.master.users') }}</p>
<p class="mt-2 font-mono text-2xl font-semibold tracking-tight text-ink">
{{ $seats['total'] !== null
? __('dashboard.master.of_total', ['used' => $seats['used'], 'total' => $seats['total']])
: $seats['used'] }}
</p>
@if ($seats['total'])
<div class="mt-3 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
</article>
@if ($traffic)
@php $state = $traffic->state(); @endphp
<article class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:140ms]">
<p class="font-mono text-[11.5px] uppercase tracking-[0.07em] text-muted">{{ __('dashboard.traffic.title') }}</p>
<p class="mt-2 font-mono text-2xl font-semibold tracking-tight text-ink">{{ Bytes::human($traffic->usedBytes) }}</p>
<div class="mt-3 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>
<p class="mt-2 font-mono text-xs text-muted">{{ __('dashboard.traffic.of', ['total' => Bytes::human($traffic->quotaBytes)]) }}</p>
@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-2.5 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
</article>
@endif
@if ($location)
<article class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:180ms]">
<p class="font-mono text-[11.5px] uppercase tracking-[0.07em] text-muted">{{ __('dashboard.master.location') }}</p>
<p class="mt-2 text-lg font-semibold tracking-tight text-ink">{{ $location['name'] }}</p>
@if ($location['note'])
<p class="mt-1.5 text-xs text-muted">{{ $location['note'] }}</p>
@endif
</article>
@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>